forked from me/e4mc_minecraft
Compare commits
23 Commits
Author | SHA1 | Date |
---|---|---|
|
27f0af2afc | 6 months ago |
|
810adc433a | 7 months ago |
|
ccb7025b74 | 1 year ago |
|
8a681524b2 | 1 year ago |
|
66247df95d | 1 year ago |
|
ceb0bedb79 | 1 year ago |
|
66d842c3ed | 1 year ago |
|
ea1cd44cbc | 1 year ago |
|
14130097c5 | 1 year ago |
|
b8e21f4849 | 1 year ago |
|
6042ea2ede | 1 year ago |
|
dfb6935b7b | 1 year ago |
|
017d5f88f0 | 1 year ago |
|
574ca7dbe4 | 1 year ago |
|
db9293e658 | 1 year ago |
|
b8af07aa4e | 1 year ago |
|
65f33361c2 | 1 year ago |
|
90bf86654d | 1 year ago |
|
914ec34792 | 1 year ago |
|
03bf7973d9 | 1 year ago |
|
0fa970ba64 | 1 year ago |
|
db702f60ff | 1 year ago |
|
82d7d802ff | 1 year ago |
@ -0,0 +1,23 @@
|
|||||||
|
# [e4mc_minecraft](https://e4mc.link)
|
||||||
|
|
||||||
|
[![Modrinth Downloads](https://img.shields.io/modrinth/dt/qANg5Jrr?color=%2300af5c&logo=modrinth&style=for-the-badge)](https://modrinth.com/project/qANg5Jrr)
|
||||||
|
[![Modrinth Followers](https://img.shields.io/modrinth/followers/qANg5Jrr?color=00af5c&logo=modrinth&style=for-the-badge)](https://modrinth.com/project/qANg5Jrr)
|
||||||
|
[![CurseForge Sucks](https://img.shields.io/badge/cuseforge-sucks-f16436?style=for-the-badge)](https://curseforge.com/minecraft/mc-mods/e4mc)
|
||||||
|
|
||||||
|
Open a LAN server to anyone, anywhere, anytime.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
[Modrinth](https://modrinth.com/project/qANg5Jrr)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Open to LAN as normal
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Please contribute
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT](LICENSE)
|
@ -0,0 +1,129 @@
|
|||||||
|
package vg.skye.e4mc_minecraft
|
||||||
|
|
||||||
|
//#if FABRIC==1
|
||||||
|
import net.fabricmc.api.EnvType
|
||||||
|
import net.fabricmc.loader.api.FabricLoader
|
||||||
|
//#else
|
||||||
|
//$$ import net.minecraftforge.fml.loading.FMLLoader
|
||||||
|
//#endif
|
||||||
|
|
||||||
|
import net.minecraft.client.MinecraftClient
|
||||||
|
import net.minecraft.text.ClickEvent
|
||||||
|
import net.minecraft.text.HoverEvent
|
||||||
|
import net.minecraft.text.Text
|
||||||
|
|
||||||
|
//#if MC>=11900
|
||||||
|
import net.minecraft.util.Formatting
|
||||||
|
//#elseif FABRIC==1
|
||||||
|
//$$ import net.minecraft.text.TranslatableText
|
||||||
|
//$$ import net.minecraft.text.LiteralText
|
||||||
|
//$$ import net.minecraft.util.Formatting
|
||||||
|
//#else
|
||||||
|
//$$ import net.minecraft.network.chat.TranslatableComponent
|
||||||
|
//$$ import net.minecraft.network.chat.TextComponent
|
||||||
|
//$$ import net.minecraft.ChatFormatting
|
||||||
|
//#endif
|
||||||
|
|
||||||
|
object ChatHelper {
|
||||||
|
//#if FABRIC==1
|
||||||
|
val isClient = FabricLoader.getInstance().environmentType.equals(EnvType.CLIENT)
|
||||||
|
val isServer = FabricLoader.getInstance().environmentType.equals(EnvType.SERVER)
|
||||||
|
//#else
|
||||||
|
//$$ val isClient = FMLLoader.getDist().isClient
|
||||||
|
//$$ val isServer = FMLLoader.getDist().isDedicatedServer
|
||||||
|
//#endif
|
||||||
|
|
||||||
|
fun sendLiteral(msg: String) {
|
||||||
|
alertUser(createLiteralMessage("[e4mc] $msg"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendError() {
|
||||||
|
if (E4mcClient.HANDLER?.state == QuiclimeHandlerState.STARTED) {
|
||||||
|
E4mcClient.HANDLER?.state = QuiclimeHandlerState.UNHEALTHY
|
||||||
|
}
|
||||||
|
alertUser(createTranslatableMessage("text.e4mc_minecraft.error"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendDomainAssignment(domain: String) {
|
||||||
|
if (isServer) {
|
||||||
|
E4mcClient.LOGGER.warn("e4mc running on Dedicated Server; This works, but isn't recommended as e4mc is designed for short-lived LAN servers")
|
||||||
|
}
|
||||||
|
|
||||||
|
E4mcClient.LOGGER.info("Domain assigned: $domain")
|
||||||
|
alertUser(createDomainAssignedMessage(domain))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun alertUser(message: Text) {
|
||||||
|
if (isClient) {
|
||||||
|
MinecraftClient.getInstance().inGameHud.chatHud.addMessage(
|
||||||
|
message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createLiteralMessage(message: String): Text {
|
||||||
|
//#if MC>=11900
|
||||||
|
return Text.literal(message)
|
||||||
|
//#elseif FABRIC==1
|
||||||
|
//$$ return LiteralText(message)
|
||||||
|
//#else
|
||||||
|
//$$ return TextComponent(message)
|
||||||
|
//#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createTranslatableMessage(key: String, vararg objects: Any?): Text {
|
||||||
|
//#if MC>=11900
|
||||||
|
return Text.translatable(key, *objects)
|
||||||
|
//#elseif FABRIC==1
|
||||||
|
//$$ return TranslatableText(key, *objects)
|
||||||
|
//#else
|
||||||
|
//$$ return TranslatableComponent(key, *objects)
|
||||||
|
//#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createDomainAssignedMessage(domain: String): Text {
|
||||||
|
//#if MC>=11900
|
||||||
|
return Text.translatable(
|
||||||
|
"text.e4mc_minecraft.domainAssigned",
|
||||||
|
Text.literal(domain).styled {
|
||||||
|
it
|
||||||
|
.withClickEvent(ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, domain))
|
||||||
|
.withColor(Formatting.GREEN)
|
||||||
|
.withHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.translatable("chat.copy.click")))
|
||||||
|
}
|
||||||
|
).append(
|
||||||
|
Text.translatable("text.e4mc_minecraft.clickToStop").styled {
|
||||||
|
it
|
||||||
|
.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, "/e4mc stop"))
|
||||||
|
.withColor(Formatting.GRAY)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
//#elseif FABRIC==1
|
||||||
|
//$$ return TranslatableText("text.e4mc_minecraft.domainAssigned", LiteralText(domain).styled {
|
||||||
|
//$$ return@styled it
|
||||||
|
//$$ .withClickEvent(ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, domain))
|
||||||
|
//$$ .withColor(Formatting.GREEN)
|
||||||
|
//$$ .withHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, TranslatableText("chat.copy.click")))
|
||||||
|
//$$ }).append(
|
||||||
|
//$$ TranslatableText("text.e4mc_minecraft.clickToStop").styled {
|
||||||
|
//$$ return@styled it
|
||||||
|
//$$ .withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, "/e4mc stop"))
|
||||||
|
//$$ .withColor(Formatting.GRAY)
|
||||||
|
//$$ }
|
||||||
|
//$$ )
|
||||||
|
//#else
|
||||||
|
//$$ return TranslatableComponent("text.e4mc_minecraft.domainAssigned", TextComponent(domain).withStyle {
|
||||||
|
//$$ return@withStyle it
|
||||||
|
//$$ .withClickEvent(ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, domain))
|
||||||
|
//$$ .withColor(ChatFormatting.GREEN)
|
||||||
|
//$$ .withHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, TranslatableComponent("chat.copy.click")))
|
||||||
|
//$$ }).append(
|
||||||
|
//$$ TranslatableComponent("text.e4mc_minecraft.clickToStop").withStyle {
|
||||||
|
//$$ return@withStyle it
|
||||||
|
//$$ .withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, "/e4mc stop"))
|
||||||
|
//$$ .withColor(ChatFormatting.GRAY)
|
||||||
|
//$$ }
|
||||||
|
//$$ )
|
||||||
|
//#endif
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
package vg.skye.e4mc_minecraft
|
||||||
|
|
||||||
|
import com.mojang.brigadier.CommandDispatcher
|
||||||
|
import net.minecraft.server.command.CommandManager
|
||||||
|
import net.minecraft.server.command.ServerCommandSource
|
||||||
|
import net.minecraft.server.network.ServerPlayerEntity
|
||||||
|
import net.minecraft.text.Text
|
||||||
|
|
||||||
|
object CommandsHelper {
|
||||||
|
private fun getPlayerFromSource(src: ServerCommandSource): ServerPlayerEntity? {
|
||||||
|
return try {
|
||||||
|
src.playerOrThrow
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registerCommandWithDispatcher(dispatcher: CommandDispatcher<ServerCommandSource>) {
|
||||||
|
dispatcher.register(
|
||||||
|
CommandManager.literal("e4mc")
|
||||||
|
.requires { src ->
|
||||||
|
if (src.server.isDedicated) {
|
||||||
|
src.hasPermissionLevel(4)
|
||||||
|
} else {
|
||||||
|
src.server.isHost((getPlayerFromSource(src) ?: return@requires false).gameProfile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.then(
|
||||||
|
CommandManager.literal("stop")
|
||||||
|
.executes { context ->
|
||||||
|
if ((E4mcClient.HANDLER != null) && (E4mcClient.HANDLER?.state != QuiclimeHandlerState.STOPPED)) {
|
||||||
|
E4mcClient.HANDLER!!.stop()
|
||||||
|
sendMessageToSource(ChatHelper.createTranslatableMessage("text.e4mc_minecraft.closeServer"), context.source)
|
||||||
|
} else {
|
||||||
|
sendErrorToSource(ChatHelper.createTranslatableMessage("text.e4mc_minecraft.serverAlreadyClosed"), context.source)
|
||||||
|
}
|
||||||
|
1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(CommandManager.literal("restart")
|
||||||
|
.executes {
|
||||||
|
if ((E4mcClient.HANDLER != null) && (E4mcClient.HANDLER?.state != QuiclimeHandlerState.STARTED)) {
|
||||||
|
E4mcClient.HANDLER?.stop()
|
||||||
|
val handler = QuiclimeHandler()
|
||||||
|
E4mcClient.HANDLER = handler
|
||||||
|
handler.startAsync()
|
||||||
|
}
|
||||||
|
1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendMessageToSource(text: Text, source: ServerCommandSource) {
|
||||||
|
source.sendFeedback(text, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sendErrorToSource(text: Text, source: ServerCommandSource) {
|
||||||
|
source.sendError(text)
|
||||||
|
}
|
||||||
|
}
|
@ -1,235 +0,0 @@
|
|||||||
package vg.skye.e4mc_minecraft
|
|
||||||
|
|
||||||
import com.google.gson.Gson
|
|
||||||
import com.google.gson.JsonObject
|
|
||||||
import io.netty.bootstrap.Bootstrap
|
|
||||||
import io.netty.buffer.ByteBuf
|
|
||||||
import io.netty.channel.*
|
|
||||||
import io.netty.channel.local.LocalAddress
|
|
||||||
import io.netty.channel.local.LocalChannel
|
|
||||||
import io.netty.channel.nio.NioEventLoopGroup
|
|
||||||
//#if FABRIC==1
|
|
||||||
import net.fabricmc.api.EnvType
|
|
||||||
import net.fabricmc.loader.api.FabricLoader
|
|
||||||
//#else
|
|
||||||
//$$ import net.minecraftforge.fml.loading.FMLLoader
|
|
||||||
//#endif
|
|
||||||
import net.minecraft.client.MinecraftClient
|
|
||||||
import net.minecraft.text.ClickEvent
|
|
||||||
import net.minecraft.text.HoverEvent
|
|
||||||
import net.minecraft.text.Text
|
|
||||||
//#if MC>=11900
|
|
||||||
import net.minecraft.util.Formatting
|
|
||||||
//#elseif FABRIC==1
|
|
||||||
//$$ import net.minecraft.text.TranslatableText
|
|
||||||
//$$ import net.minecraft.text.LiteralText
|
|
||||||
//$$ import net.minecraft.util.Formatting
|
|
||||||
//#else
|
|
||||||
//$$ import net.minecraft.network.chat.TranslatableComponent
|
|
||||||
//$$ import net.minecraft.network.chat.TextComponent
|
|
||||||
//$$ import net.minecraft.ChatFormatting
|
|
||||||
//#endif
|
|
||||||
import org.java_websocket.client.WebSocketClient
|
|
||||||
import org.java_websocket.handshake.ServerHandshake
|
|
||||||
import java.lang.Exception
|
|
||||||
import java.net.InetSocketAddress
|
|
||||||
import java.net.URI
|
|
||||||
import java.nio.ByteBuffer
|
|
||||||
import java.util.concurrent.ArrayBlockingQueue
|
|
||||||
|
|
||||||
|
|
||||||
data class DomainAssignedMessage(val DomainAssigned: String)
|
|
||||||
data class ChannelOpenMessage(val ChannelOpen: List<Any>)
|
|
||||||
data class ChannelClosedMessage(val ChannelClosed: Number)
|
|
||||||
|
|
||||||
class E4mcRelayHandler: WebSocketClient(URI("wss://ingress.e4mc.link")) {
|
|
||||||
private val gson = Gson()
|
|
||||||
private val childChannels = mutableMapOf<Int, LocalChannel>()
|
|
||||||
private val messageQueue = mutableMapOf<Int, ArrayBlockingQueue<ByteBuffer>>()
|
|
||||||
private val eventLoopGroup = NioEventLoopGroup()
|
|
||||||
|
|
||||||
override fun onOpen(handshakedata: ServerHandshake?) {
|
|
||||||
// not much to do here
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMessage(message: String?) {
|
|
||||||
E4mcClient.LOGGER.info("WebSocket Text message: {}", message)
|
|
||||||
val json = gson.fromJson(message, JsonObject::class.java)
|
|
||||||
when {
|
|
||||||
json.has("DomainAssigned") -> handleDomainAssigned(json)
|
|
||||||
json.has("ChannelOpen") -> handleChannelOpen(json)
|
|
||||||
json.has("ChannelClosed") -> handleChannelClosed(json)
|
|
||||||
else -> E4mcClient.LOGGER.warn("Unhandled WebSocket Text message: $message")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMessage(bytes: ByteBuffer) {
|
|
||||||
val channelId = bytes.get()
|
|
||||||
val rest = bytes.slice()
|
|
||||||
val channel = childChannels[channelId.toInt()]
|
|
||||||
if (channel == null) {
|
|
||||||
if (messageQueue[channelId.toInt()] == null) {
|
|
||||||
E4mcClient.LOGGER.info("Creating queue for channel: {}", channelId)
|
|
||||||
messageQueue[channelId.toInt()] = ArrayBlockingQueue(8)
|
|
||||||
}
|
|
||||||
messageQueue[channelId.toInt()]!!.add(rest)
|
|
||||||
} else {
|
|
||||||
val byteBuf = channel.alloc().buffer(rest.remaining())
|
|
||||||
byteBuf.writeBytes(rest)
|
|
||||||
channel.writeAndFlush(byteBuf).sync()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClose(code: Int, reason: String?, remote: Boolean) {
|
|
||||||
childChannels.forEach { (_, channel) -> channel.close() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(ex: java.lang.Exception) {
|
|
||||||
ex.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleDomainAssigned(json: JsonObject) {
|
|
||||||
val msg = gson.fromJson(json, DomainAssignedMessage::class.java)
|
|
||||||
|
|
||||||
//#if FABRIC==1
|
|
||||||
val isClient = FabricLoader.getInstance().environmentType.equals(EnvType.CLIENT)
|
|
||||||
val isServer = FabricLoader.getInstance().environmentType.equals(EnvType.SERVER)
|
|
||||||
//#else
|
|
||||||
//$$ val isClient = FMLLoader.getDist().isClient
|
|
||||||
//$$ val isServer = FMLLoader.getDist().isDedicatedServer
|
|
||||||
//#endif
|
|
||||||
|
|
||||||
if (isServer) {
|
|
||||||
E4mcClient.LOGGER.warn("e4mc running on Dedicated Server; This works, but isn't recommended as e4mc is designed for short-lived LAN servers")
|
|
||||||
}
|
|
||||||
E4mcClient.LOGGER.info("Domain assigned: ${msg.DomainAssigned}")
|
|
||||||
|
|
||||||
if (isClient) {
|
|
||||||
alertUser(msg.DomainAssigned)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun alertUser(domain: String) {
|
|
||||||
try {
|
|
||||||
MinecraftClient.getInstance().inGameHud.chatHud.addMessage(
|
|
||||||
createMessage(domain)
|
|
||||||
)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createMessage(domain: String): Text {
|
|
||||||
//#if MC>=11900
|
|
||||||
return Text.translatable(
|
|
||||||
"text.e4mc_minecraft.domainAssigned",
|
|
||||||
Text.literal(domain).styled {
|
|
||||||
it
|
|
||||||
.withClickEvent(ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, domain))
|
|
||||||
.withColor(Formatting.GREEN)
|
|
||||||
.withHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.translatable("chat.copy.click")))
|
|
||||||
}
|
|
||||||
).append(
|
|
||||||
Text.translatable("text.e4mc_minecraft.clickToStop").styled {
|
|
||||||
it
|
|
||||||
.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, "/e4mc stop"))
|
|
||||||
.withColor(Formatting.GRAY)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
//#elseif FABRIC==1
|
|
||||||
//$$ return TranslatableText("text.e4mc_minecraft.domainAssigned", LiteralText(domain).styled {
|
|
||||||
//$$ return@styled it
|
|
||||||
//$$ .withClickEvent(ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, domain))
|
|
||||||
//$$ .withColor(Formatting.GREEN)
|
|
||||||
//$$ .withHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, TranslatableText("chat.copy.click")))
|
|
||||||
//$$ }).append(
|
|
||||||
//$$ TranslatableText("text.e4mc_minecraft.clickToStop").styled {
|
|
||||||
//$$ return@styled it
|
|
||||||
//$$ .withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, "/e4mc stop"))
|
|
||||||
//$$ .withColor(Formatting.GRAY)
|
|
||||||
//$$ }
|
|
||||||
//$$ )
|
|
||||||
//#else
|
|
||||||
//$$ return TranslatableComponent("text.e4mc_minecraft.domainAssigned", TextComponent(domain).withStyle {
|
|
||||||
//$$ return@withStyle it
|
|
||||||
//$$ .withClickEvent(ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, domain))
|
|
||||||
//$$ .withColor(ChatFormatting.GREEN)
|
|
||||||
//$$ .withHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, TranslatableComponent("chat.copy.click")))
|
|
||||||
//$$ }).append(
|
|
||||||
//$$ TranslatableComponent("text.e4mc_minecraft.clickToStop").withStyle {
|
|
||||||
//$$ return@withStyle it
|
|
||||||
//$$ .withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, "/e4mc stop"))
|
|
||||||
//$$ .withColor(ChatFormatting.GRAY)
|
|
||||||
//$$ }
|
|
||||||
//$$ )
|
|
||||||
//#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleChannelOpen(json: JsonObject) {
|
|
||||||
val msg = gson.fromJson(json, ChannelOpenMessage::class.java)
|
|
||||||
val channelId = msg.ChannelOpen[0] as Number
|
|
||||||
val clientInfo = msg.ChannelOpen[1] as String
|
|
||||||
E4mcClient.LOGGER.info("Channel opened: channelId=$channelId, clientInfo=$clientInfo")
|
|
||||||
val host = clientInfo.substringBeforeLast(':')
|
|
||||||
val port = clientInfo.substringAfterLast(':').toInt()
|
|
||||||
val addr = InetSocketAddress(host, port)
|
|
||||||
|
|
||||||
val self = this
|
|
||||||
|
|
||||||
Bootstrap()
|
|
||||||
.channel(LocalChannel::class.java)
|
|
||||||
.handler(object : ChannelInitializer<LocalChannel>() {
|
|
||||||
@Throws(java.lang.Exception::class)
|
|
||||||
override fun initChannel(ch: LocalChannel) {
|
|
||||||
ch.pipeline().addLast(ChildHandler(self, addr, channelId.toInt()))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.group(eventLoopGroup)
|
|
||||||
.connect(LocalAddress("e4mc-relay"))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleChannelClosed(json: JsonObject) {
|
|
||||||
val msg = gson.fromJson(json, ChannelClosedMessage::class.java)
|
|
||||||
val channelId = msg.ChannelClosed.toInt()
|
|
||||||
E4mcClient.LOGGER.info("Closing channel as requested: {}", channelId)
|
|
||||||
childChannels.remove(channelId)?.let {
|
|
||||||
it.pipeline().get(ChildHandler::class.java).isClosedFromServer = true
|
|
||||||
it.close()
|
|
||||||
E4mcClient.LOGGER.info("Channel closed: channelId=$channelId")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ChildHandler(private val parent: E4mcRelayHandler, private val address: InetSocketAddress, private val channelId: Int): SimpleChannelInboundHandler<ByteBuf>() {
|
|
||||||
var isClosedFromServer = false
|
|
||||||
override fun channelActive(ctx: ChannelHandlerContext) {
|
|
||||||
// ctx.writeAndFlush(address)
|
|
||||||
if (parent.messageQueue[channelId] != null) {
|
|
||||||
parent.messageQueue[channelId]!!.forEach { buf ->
|
|
||||||
run {
|
|
||||||
E4mcClient.LOGGER.info("Handling queued buffer: {}", buf)
|
|
||||||
val byteBuf = ctx.alloc().buffer(buf.remaining())
|
|
||||||
byteBuf.writeBytes(buf)
|
|
||||||
ctx.writeAndFlush(byteBuf)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
parent.messageQueue.remove(channelId)
|
|
||||||
}
|
|
||||||
parent.childChannels[channelId] = ctx.channel() as LocalChannel
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun channelRead0(ctx: ChannelHandlerContext, msg: ByteBuf) {
|
|
||||||
val buf = ByteArray(msg.readableBytes() + 1)
|
|
||||||
buf[0] = channelId.toByte()
|
|
||||||
msg.readBytes(buf, 1, msg.readableBytes())
|
|
||||||
parent.send(buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun channelInactive(ctx: ChannelHandlerContext) {
|
|
||||||
E4mcClient.LOGGER.info("Channel closed: {}", channelId)
|
|
||||||
if (!isClosedFromServer) {
|
|
||||||
parent.send(parent.gson.toJson(ChannelClosedMessage(channelId)))
|
|
||||||
}
|
|
||||||
parent.childChannels.remove(channelId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,320 @@
|
|||||||
|
package vg.skye.e4mc_minecraft
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import io.netty.bootstrap.Bootstrap
|
||||||
|
import io.netty.buffer.ByteBuf
|
||||||
|
import io.netty.buffer.Unpooled
|
||||||
|
import io.netty.channel.*
|
||||||
|
import io.netty.channel.ChannelHandler.Sharable
|
||||||
|
import io.netty.channel.local.LocalAddress
|
||||||
|
import io.netty.channel.local.LocalChannel
|
||||||
|
import io.netty.channel.nio.NioEventLoopGroup
|
||||||
|
import io.netty.channel.socket.ChannelInputShutdownReadComplete
|
||||||
|
import io.netty.channel.socket.nio.NioDatagramChannel
|
||||||
|
import io.netty.handler.codec.ByteToMessageCodec
|
||||||
|
import io.netty.incubator.codec.quic.*
|
||||||
|
import org.slf4j.Logger
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.net.Inet4Address
|
||||||
|
import java.net.InetAddress
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.URI
|
||||||
|
import java.net.http.HttpClient
|
||||||
|
import java.net.http.HttpRequest
|
||||||
|
import java.net.http.HttpResponse
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
interface QuiclimeControlMessage
|
||||||
|
|
||||||
|
data class RequestDomainAssignmentMessageServerbound(val kind: String = "request_domain_assignment"): QuiclimeControlMessage
|
||||||
|
|
||||||
|
data class DomainAssignmentCompleteMessageClientbound(val kind: String = "domain_assignment_complete", val domain: String): QuiclimeControlMessage
|
||||||
|
data class RequestMessageBroadcastMessageClientbound(val kind: String = "request_message_broadcast", val message: String): QuiclimeControlMessage
|
||||||
|
|
||||||
|
class QuiclimeControlMessageCodec : ByteToMessageCodec<QuiclimeControlMessage>() {
|
||||||
|
val gson = Gson()
|
||||||
|
|
||||||
|
override fun encode(ctx: ChannelHandlerContext, msg: QuiclimeControlMessage, out: ByteBuf) {
|
||||||
|
val json = gson.toJson(msg).toByteArray()
|
||||||
|
out.writeByte(json.size)
|
||||||
|
out.writeBytes(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun decode(ctx: ChannelHandlerContext, `in`: ByteBuf, out: MutableList<Any>) {
|
||||||
|
val size = `in`.getByte(`in`.readerIndex()).toInt()
|
||||||
|
if (`in`.readableBytes() >= size + 1) {
|
||||||
|
`in`.skipBytes(1)
|
||||||
|
val buf = ByteArray(size)
|
||||||
|
`in`.readBytes(buf)
|
||||||
|
val json = gson.fromJson(buf.decodeToString(), JsonObject::class.java)
|
||||||
|
out.add(gson.fromJson(json, when (json["kind"].asString) {
|
||||||
|
"domain_assignment_complete" -> DomainAssignmentCompleteMessageClientbound::class.java
|
||||||
|
"request_message_broadcast" -> RequestMessageBroadcastMessageClientbound::class.java
|
||||||
|
else -> throw Exception("Invalid message type")
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class QuiclimeToMinecraftHandler(private val toQuiclime: QuicStreamChannel) : ChannelInboundHandlerAdapter() {
|
||||||
|
override fun channelActive(ctx: ChannelHandlerContext) {
|
||||||
|
ctx.read()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
|
||||||
|
toQuiclime.writeAndFlush(msg).addListener {
|
||||||
|
if (it.isSuccess) {
|
||||||
|
ctx.channel().read()
|
||||||
|
} else {
|
||||||
|
ChatHelper.sendError()
|
||||||
|
toQuiclime.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun channelInactive(ctx: ChannelHandlerContext) {
|
||||||
|
QuiclimeHandler.LOGGER.info("channel inactive(from MC): {} (MC: {})", toQuiclime, ctx.channel())
|
||||||
|
if (toQuiclime.isActive) {
|
||||||
|
toQuiclime.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("OVERRIDE_DEPRECATION")
|
||||||
|
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable?) {
|
||||||
|
super.exceptionCaught(ctx, cause)
|
||||||
|
ChatHelper.sendError()
|
||||||
|
this.channelInactive(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class QuiclimeToQuiclimeHandler : ChannelInboundHandlerAdapter() {
|
||||||
|
private var toMinecraft: LocalChannel? = null
|
||||||
|
override fun channelActive(ctx: ChannelHandlerContext) {
|
||||||
|
QuiclimeHandler.LOGGER.info("channel active: {}", ctx.channel())
|
||||||
|
val fut = Bootstrap()
|
||||||
|
.group(ctx.channel().eventLoop())
|
||||||
|
.channel(LocalChannel::class.java)
|
||||||
|
.handler(QuiclimeToMinecraftHandler(ctx.channel() as QuicStreamChannel))
|
||||||
|
.option(ChannelOption.AUTO_READ, false)
|
||||||
|
.connect(LocalAddress("e4mc-relay"))
|
||||||
|
toMinecraft = fut.channel() as LocalChannel
|
||||||
|
fut.addListener {
|
||||||
|
if (it.isSuccess) {
|
||||||
|
ctx.channel().read()
|
||||||
|
} else {
|
||||||
|
ChatHelper.sendError()
|
||||||
|
ctx.channel().close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun channelRead(ctx: ChannelHandlerContext, msg: Any) {
|
||||||
|
if (toMinecraft?.isActive == true) {
|
||||||
|
toMinecraft!!.writeAndFlush(msg).addListener {
|
||||||
|
if (it.isSuccess) {
|
||||||
|
ctx.channel().read();
|
||||||
|
} else {
|
||||||
|
ChatHelper.sendError()
|
||||||
|
(it as ChannelFuture).channel().close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun channelInactive(ctx: ChannelHandlerContext) {
|
||||||
|
QuiclimeHandler.LOGGER.info("channel inactive(from Quiclime): {} (MC: {})", ctx.channel(), toMinecraft)
|
||||||
|
if (toMinecraft?.isActive == true) {
|
||||||
|
toMinecraft!!.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun userEventTriggered(ctx: ChannelHandlerContext, evt: Any) {
|
||||||
|
if (evt === ChannelInputShutdownReadComplete.INSTANCE) {
|
||||||
|
this.channelInactive(ctx)
|
||||||
|
}
|
||||||
|
super.userEventTriggered(ctx, evt)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("OVERRIDE_DEPRECATION")
|
||||||
|
override fun exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable?) {
|
||||||
|
super.exceptionCaught(ctx, cause)
|
||||||
|
ChatHelper.sendError()
|
||||||
|
this.channelInactive(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class BrokerResponse(val id: String, val host: String, val port: Int)
|
||||||
|
|
||||||
|
enum class QuiclimeHandlerState {
|
||||||
|
STARTING,
|
||||||
|
STARTED,
|
||||||
|
UNHEALTHY,
|
||||||
|
STOPPING,
|
||||||
|
STOPPED
|
||||||
|
}
|
||||||
|
|
||||||
|
class QuiclimeHandler {
|
||||||
|
private val group = NioEventLoopGroup()
|
||||||
|
private var datagramChannel: NioDatagramChannel? = null
|
||||||
|
private var quicChannel: QuicChannel? = null
|
||||||
|
|
||||||
|
var state: QuiclimeHandlerState = QuiclimeHandlerState.STARTING
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val LOGGER: Logger = LoggerFactory.getLogger("e4mc-quiclime")
|
||||||
|
fun startAsync() {
|
||||||
|
thread(start = true) {
|
||||||
|
E4mcClient.HANDLER = QuiclimeHandler()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startAsync() {
|
||||||
|
thread(start = true) {
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
try {
|
||||||
|
val httpClient = HttpClient.newHttpClient()
|
||||||
|
val request = HttpRequest
|
||||||
|
.newBuilder(URI("https://broker.e4mc.link/getBestRelay"))
|
||||||
|
.header("Accept", "application/json")
|
||||||
|
.build()
|
||||||
|
LOGGER.info("req: {}", request)
|
||||||
|
val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString())
|
||||||
|
LOGGER.info("resp: {}", response)
|
||||||
|
if (response.statusCode() != 200) {
|
||||||
|
throw Exception()
|
||||||
|
}
|
||||||
|
val relayInfo = Gson().fromJson(response.body(), BrokerResponse::class.java)
|
||||||
|
LOGGER.info("using relay {}", relayInfo.id)
|
||||||
|
val context: QuicSslContext = QuicSslContextBuilder
|
||||||
|
.forClient()
|
||||||
|
.applicationProtocols("quiclime")
|
||||||
|
.build()
|
||||||
|
val codec = QuicClientCodecBuilder()
|
||||||
|
.sslContext(context)
|
||||||
|
.sslEngineProvider {
|
||||||
|
context.newEngine(it.alloc(), relayInfo.host, relayInfo.port)
|
||||||
|
}
|
||||||
|
.initialMaxStreamsBidirectional(512)
|
||||||
|
.maxIdleTimeout(10, TimeUnit.SECONDS)
|
||||||
|
.initialMaxData(4611686018427387903)
|
||||||
|
.initialMaxStreamDataBidirectionalRemote(1250000)
|
||||||
|
.initialMaxStreamDataBidirectionalLocal(1250000)
|
||||||
|
.initialMaxStreamDataUnidirectional(1250000)
|
||||||
|
.build()
|
||||||
|
Bootstrap()
|
||||||
|
.group(group)
|
||||||
|
.channel(NioDatagramChannel::class.java)
|
||||||
|
.handler(codec)
|
||||||
|
.bind(0)
|
||||||
|
.addListener { datagramChannelFuture ->
|
||||||
|
if (!datagramChannelFuture.isSuccess) {
|
||||||
|
ChatHelper.sendError()
|
||||||
|
throw datagramChannelFuture.cause()
|
||||||
|
}
|
||||||
|
datagramChannel = (datagramChannelFuture as ChannelFuture).channel() as NioDatagramChannel
|
||||||
|
QuicChannel.newBootstrap(datagramChannel)
|
||||||
|
.streamHandler(
|
||||||
|
@Sharable
|
||||||
|
object : ChannelInitializer<QuicStreamChannel>() {
|
||||||
|
override fun initChannel(ch: QuicStreamChannel) {
|
||||||
|
ch.pipeline().addLast(QuiclimeToQuiclimeHandler())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.handler(object : ChannelInboundHandlerAdapter() {
|
||||||
|
@Suppress("OVERRIDE_DEPRECATION")
|
||||||
|
override fun exceptionCaught(ctx: ChannelHandlerContext?, cause: Throwable?) {
|
||||||
|
super.exceptionCaught(ctx, cause)
|
||||||
|
ChatHelper.sendError()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun channelInactive(ctx: ChannelHandlerContext) {
|
||||||
|
super.channelInactive(ctx)
|
||||||
|
state = QuiclimeHandlerState.STOPPED
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.streamOption(ChannelOption.AUTO_READ, false)
|
||||||
|
.remoteAddress(InetSocketAddress(InetAddress.getByName(relayInfo.host), relayInfo.port))
|
||||||
|
.connect()
|
||||||
|
.addListener { quicChannelFuture ->
|
||||||
|
if (!quicChannelFuture.isSuccess) {
|
||||||
|
ChatHelper.sendError()
|
||||||
|
throw quicChannelFuture.cause()
|
||||||
|
}
|
||||||
|
quicChannel = quicChannelFuture.get() as QuicChannel
|
||||||
|
quicChannel!!.createStream(QuicStreamType.BIDIRECTIONAL,
|
||||||
|
object : ChannelInitializer<QuicStreamChannel>() {
|
||||||
|
override fun initChannel(ch: QuicStreamChannel) {
|
||||||
|
ch.pipeline().addLast(QuiclimeControlMessageCodec(), object : SimpleChannelInboundHandler<QuiclimeControlMessage>() {
|
||||||
|
override fun channelRead0(ctx: ChannelHandlerContext?, msg: QuiclimeControlMessage?) {
|
||||||
|
when (msg) {
|
||||||
|
is DomainAssignmentCompleteMessageClientbound -> {
|
||||||
|
state = QuiclimeHandlerState.STARTED
|
||||||
|
ChatHelper.sendDomainAssignment(msg.domain)
|
||||||
|
}
|
||||||
|
is RequestMessageBroadcastMessageClientbound -> {
|
||||||
|
ChatHelper.sendLiteral(msg.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}).addListener {
|
||||||
|
if (!it.isSuccess) {
|
||||||
|
ChatHelper.sendError()
|
||||||
|
throw it.cause()
|
||||||
|
}
|
||||||
|
val streamChannel = it.now as QuicStreamChannel
|
||||||
|
LOGGER.info("control channel open: {}", streamChannel)
|
||||||
|
streamChannel
|
||||||
|
.writeAndFlush(RequestDomainAssignmentMessageServerbound())
|
||||||
|
.addListener {
|
||||||
|
LOGGER.info("control channel write complete")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
quicChannel!!.closeFuture().addListener {
|
||||||
|
datagramChannel?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
ChatHelper.sendError()
|
||||||
|
this.stop()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
state = QuiclimeHandlerState.STOPPING
|
||||||
|
if (quicChannel?.close()?.addListener {
|
||||||
|
if (datagramChannel?.close()?.addListener {
|
||||||
|
group.shutdownGracefully().addListener {
|
||||||
|
state = QuiclimeHandlerState.STOPPED
|
||||||
|
}
|
||||||
|
} == null) {
|
||||||
|
group.shutdownGracefully().addListener {
|
||||||
|
state = QuiclimeHandlerState.STOPPED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} == null) {
|
||||||
|
if (datagramChannel?.close()?.addListener {
|
||||||
|
group.shutdownGracefully().addListener {
|
||||||
|
state = QuiclimeHandlerState.STOPPED
|
||||||
|
}
|
||||||
|
} == null) {
|
||||||
|
group.shutdownGracefully().addListener {
|
||||||
|
state = QuiclimeHandlerState.STOPPED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
After Width: | Height: | Size: 12 KiB |
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"text.e4mc_minecraft.domainAssigned": "Lokalt spel startat på domän [%s]",
|
||||||
|
"text.e4mc_minecraft.closeServer": "Lokalt spel är inte publikt öppet längre",
|
||||||
|
"text.e4mc_minecraft.serverAlreadyClosed": "Lokalt spel är inte öppet",
|
||||||
|
"text.e4mc_minecraft.clickToStop": " (Klicka här för att stoppa)",
|
||||||
|
"text.e4mc_minecraft.error": "Ett fel uppstod i e4mc"
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"text.e4mc_minecraft.domainAssigned": "本地遊戲已託管在網域 [%s]",
|
||||||
|
"text.e4mc_minecraft.closeServer": "本地遊戲不再公開託管",
|
||||||
|
"text.e4mc_minecraft.serverAlreadyClosed": "本地遊戲未公開託管",
|
||||||
|
"text.e4mc_minecraft.clickToStop": "(點擊此處停止)",
|
||||||
|
"text.e4mc_minecraft.error": "在 e4mc 中發生了個錯誤"
|
||||||
|
}
|
@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"required": true,
|
"required": true,
|
||||||
"package": "vg.skye.e4mc_minecraft.mixins",
|
"package": "vg.skye.e4mc_minecraft.mixins",
|
||||||
"compatibilityLevel": "JAVA_16",
|
"compatibilityLevel": "JAVA_16",
|
||||||
"injectors": {
|
"injectors": {
|
||||||
"defaultRequire": 1
|
"defaultRequire": 1
|
||||||
},
|
},
|
||||||
"mixins": [
|
"mixins": [
|
||||||
"ServerNetworkIoMixin",
|
"ClientConnectionMixin",
|
||||||
"ClientConnectionMixin"
|
"ServerNetworkIoMixin"
|
||||||
],
|
],
|
||||||
"client": [],
|
"client": [],
|
||||||
"server": []
|
"server": []
|
||||||
}
|
}
|
Loading…
Reference in new issue