forked from me/e4mc_minecraft
4.0.0 with bad build system
This commit is contained in:
parent
ceb0bedb79
commit
66247df95d
12 changed files with 558 additions and 338 deletions
|
@ -4,9 +4,6 @@
|
|||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="delegatedBuild" value="true" />
|
||||
<option name="testRunner" value="GRADLE" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
|
|
|
@ -3,5 +3,5 @@ org.gradle.jvmargs=-Xmx2G
|
|||
|
||||
mod.name=e4mc
|
||||
mod.id=e4mc_minecraft
|
||||
mod.version=3.2.0
|
||||
mod.version=4.0.0
|
||||
mod.group=vg.skye
|
|
@ -10,7 +10,6 @@ pluginManagement {
|
|||
maven("https://repo.essential.gg/repository/maven-public")
|
||||
maven("https://server.bbkr.space/artifactory/libs-release/")
|
||||
maven("https://jitpack.io/")
|
||||
|
||||
// Snapshots
|
||||
maven("https://maven.deftu.xyz/snapshots")
|
||||
mavenLocal()
|
||||
|
@ -25,7 +24,7 @@ pluginManagement {
|
|||
kotlin("jvm") version(kotlin)
|
||||
kotlin("plugin.serialization") version(kotlin)
|
||||
|
||||
val epgt = "1.15.1"
|
||||
val epgt = "1.17.1"
|
||||
id("xyz.deftu.gradle.multiversion-root") version(epgt)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import io.netty.bootstrap.ServerBootstrap;
|
|||
import io.netty.channel.ServerChannel;
|
||||
import io.netty.channel.local.LocalAddress;
|
||||
import io.netty.channel.local.LocalServerChannel;
|
||||
import io.netty.channel.socket.ServerSocketChannel;
|
||||
import net.minecraft.server.ServerNetworkIo;
|
||||
import org.spongepowered.asm.mixin.Mixin;
|
||||
import org.spongepowered.asm.mixin.Shadow;
|
||||
|
@ -15,7 +14,7 @@ import org.spongepowered.asm.mixin.injection.ModifyArg;
|
|||
import org.spongepowered.asm.mixin.injection.Redirect;
|
||||
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||
import vg.skye.e4mc_minecraft.E4mcClient;
|
||||
import vg.skye.e4mc_minecraft.E4mcRelayHandler;
|
||||
import vg.skye.e4mc_minecraft.QuiclimeHandler;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
|
@ -40,9 +39,9 @@ public abstract class ServerNetworkIoMixin {
|
|||
initializingE4mc.set(false);
|
||||
}
|
||||
} else {
|
||||
E4mcRelayHandler handler = new E4mcRelayHandler();
|
||||
QuiclimeHandler handler = new QuiclimeHandler();
|
||||
E4mcClient.HANDLER = handler;
|
||||
handler.connect();
|
||||
handler.startAsync();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,7 +58,7 @@ public abstract class ServerNetworkIoMixin {
|
|||
@Inject(method = "stop", at = @At("HEAD"))
|
||||
private void stop(CallbackInfo ci) {
|
||||
if (E4mcClient.HANDLER != null) {
|
||||
E4mcClient.HANDLER.close();
|
||||
E4mcClient.HANDLER.stop();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
129
src/main/kotlin/vg/skye/e4mc_minecraft/ChatHelper.kt
Normal file
129
src/main/kotlin/vg/skye/e4mc_minecraft/ChatHelper.kt
Normal file
|
@ -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
|
||||
}
|
||||
}
|
61
src/main/kotlin/vg/skye/e4mc_minecraft/CommandsHelper.kt
Normal file
61
src/main/kotlin/vg/skye/e4mc_minecraft/CommandsHelper.kt
Normal file
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -33,11 +33,11 @@ object E4mcClient : ModInitializer {
|
|||
//#endif
|
||||
const val NAME = "e4mc"
|
||||
const val ID = "e4mc_minecraft"
|
||||
const val VERSION = "3.2.0"
|
||||
const val VERSION = "4.0.0"
|
||||
@JvmField
|
||||
val LOGGER: Logger = LoggerFactory.getLogger("e4mc")
|
||||
@JvmField
|
||||
var HANDLER: E4mcRelayHandler? = null
|
||||
var HANDLER: QuiclimeHandler? = null
|
||||
|
||||
//#if FABRIC==1
|
||||
override fun onInitialize() {
|
||||
|
@ -46,63 +46,14 @@ object E4mcClient : ModInitializer {
|
|||
//#else
|
||||
//$$ CommandRegistrationCallback.EVENT.register { dispatcher, _ ->
|
||||
//#endif
|
||||
dispatcher.register(literal("e4mc")
|
||||
.then(
|
||||
literal("stop")
|
||||
.requires { src ->
|
||||
if (src.server.isDedicated) {
|
||||
src.hasPermissionLevel(4)
|
||||
} else {
|
||||
src.server.isHost((src.player ?: return@requires false).gameProfile)
|
||||
}
|
||||
}
|
||||
.executes { context ->
|
||||
if (HANDLER != null) {
|
||||
HANDLER!!.close()
|
||||
HANDLER = null
|
||||
//#if MC>=11904
|
||||
context.source.sendMessage(Text.translatable("text.e4mc_minecraft.closeServer"))
|
||||
//#else
|
||||
//$$ context.source.sendFeedback(TranslatableText("text.e4mc_minecraft.closeServer"), false)
|
||||
//#endif
|
||||
} else {
|
||||
//#if MC>=11904
|
||||
context.source.sendMessage(Text.translatable("text.e4mc_minecraft.serverAlreadyClosed"))
|
||||
//#else
|
||||
//$$ context.source.sendFeedback(TranslatableText("text.e4mc_minecraft.serverAlreadyClosed"), false)
|
||||
//#endif
|
||||
}
|
||||
1
|
||||
}
|
||||
))
|
||||
CommandsHelper.registerCommandWithDispatcher(dispatcher)
|
||||
}
|
||||
}
|
||||
//#else
|
||||
//$$ @SubscribeEvent
|
||||
//$$ fun onRegisterCommandEvent(event: RegisterCommandsEvent) {
|
||||
//$$ val commandDispatcher = event.getDispatcher()
|
||||
//$$ commandDispatcher.register(literal("e4mc")
|
||||
//$$ .then(
|
||||
//$$ literal("stop")
|
||||
//$$ .executes { context ->
|
||||
//$$ if (HANDLER != null) {
|
||||
//$$ HANDLER!!.close()
|
||||
//$$ HANDLER = null
|
||||
//$$ //#if MC>=11904
|
||||
//$$ context.source.sendSuccess(Component.translatable("text.e4mc_minecraft.closeServer"), false)
|
||||
//$$ //#else
|
||||
//$$ //$$ context.source.sendSuccess(TranslatableComponent("text.e4mc_minecraft.closeServer"), false)
|
||||
//$$ //#endif
|
||||
//$$ } else {
|
||||
//$$ //#if MC>=11904
|
||||
//$$ context.source.sendFailure(Component.translatable("text.e4mc_minecraft.serverAlreadyClosed"))
|
||||
//$$ //#else
|
||||
//$$ //$$ context.source.sendFailure(TranslatableComponent("text.e4mc_minecraft.serverAlreadyClosed"))
|
||||
//$$ //#endif
|
||||
//$$ }
|
||||
//$$ 1
|
||||
//$$ }
|
||||
//$$ ))
|
||||
//$$ val dispatcher = event.getDispatcher()
|
||||
//$$ CommandsHelper.registerCommandWithDispatcher(dispatcher)
|
||||
//$$ }
|
||||
//#endif
|
||||
}
|
||||
|
|
|
@ -1,255 +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(System.getProperty("vg.skye.e4mc_minecraft.ingress_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()
|
||||
//#if FABRIC==1
|
||||
val isClient = FabricLoader.getInstance().environmentType.equals(EnvType.CLIENT)
|
||||
//#else
|
||||
//$$ val isClient = FMLLoader.getDist().isClient
|
||||
//#endif
|
||||
if (isClient) {
|
||||
try {
|
||||
MinecraftClient.getInstance().inGameHud.chatHud.addMessage(
|
||||
//#if MC>=11900
|
||||
Text.translatable("text.e4mc_minecraft.error")
|
||||
//#elseif FABRIC==1
|
||||
//$$ TranslatableText("text.e4mc_minecraft.error")
|
||||
//#else
|
||||
//$$ TranslatableComponent("text.e4mc_minecraft.error")
|
||||
//#endif
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
e.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)
|
||||
}
|
||||
}
|
||||
}
|
320
src/main/kotlin/vg/skye/e4mc_minecraft/QuiclimeHandler.kt
Normal file
320
src/main/kotlin/vg/skye/e4mc_minecraft/QuiclimeHandler.kt
Normal file
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +1,14 @@
|
|||
{
|
||||
"required": true,
|
||||
"package": "vg.skye.e4mc_minecraft.mixins",
|
||||
"compatibilityLevel": "JAVA_16",
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
},
|
||||
"mixins": [
|
||||
"ServerNetworkIoMixin",
|
||||
"ClientConnectionMixin"
|
||||
],
|
||||
"client": [],
|
||||
"server": []
|
||||
"required": true,
|
||||
"package": "vg.skye.e4mc_minecraft.mixins",
|
||||
"compatibilityLevel": "JAVA_16",
|
||||
"injectors": {
|
||||
"defaultRequire": 1
|
||||
},
|
||||
"mixins": [
|
||||
"ClientConnectionMixin",
|
||||
"ServerNetworkIoMixin"
|
||||
],
|
||||
"client": [],
|
||||
"server": []
|
||||
}
|
|
@ -31,7 +31,7 @@
|
|||
"fabric": "*",
|
||||
"fabricloader": ">=0.14.9",
|
||||
"fabric-language-kotlin": "*",
|
||||
"minecraft": "${mc_version == "1.19.4" ? ">=1.19" : ">=1.17 <1.19"}",
|
||||
"minecraft": "${mc_version == "1.19.4" ? ">=1.19" : ">=1.18 <1.19"}",
|
||||
"java": ">=16"
|
||||
}
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
import com.modrinth.minotaur.dependencies.DependencyType
|
||||
import com.modrinth.minotaur.dependencies.ModDependency
|
||||
import org.gradle.configurationcache.extensions.capitalized
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
import xyz.deftu.gradle.tools.minecraft.CurseRelation
|
||||
import xyz.deftu.gradle.tools.minecraft.CurseRelationType
|
||||
|
||||
|
@ -16,7 +15,10 @@ plugins {
|
|||
}
|
||||
|
||||
val bundle by configurations.creating {
|
||||
if (mcData.isFabric) {
|
||||
// Fabric imposes a hard limit of 64 on mod IDs
|
||||
// the autogenned mod IDs are far longer than that
|
||||
// thanks, netty!
|
||||
if (false /* mcData.isFabric */) {
|
||||
configurations.getByName("include").extendsFrom(this)
|
||||
} else configurations.getByName("shade").extendsFrom(this)
|
||||
}
|
||||
|
@ -106,7 +108,24 @@ dependencies {
|
|||
} else if (mcData.isForge) {
|
||||
implementation("thedarkcolour:kotlinforforge:3.8.0")
|
||||
}
|
||||
bundle(implementation("org.java-websocket:Java-WebSocket:1.5.3") {
|
||||
exclude(group = "org.slf4j")
|
||||
|
||||
bundle(implementation("com.github.vgskye.netty-incubator-codec-quic:netty-incubator-codec-classes-quic:03cbd5c2c4") {
|
||||
exclude(group = "io.netty")
|
||||
})
|
||||
}
|
||||
// bundle(implementation("io.netty.incubator:netty-incubator-codec-classes-quic:0.0.47.Final")!!)
|
||||
bundle(implementation("io.netty.incubator:netty-incubator-codec-native-quic:0.0.48.Final:linux-x86_64") {
|
||||
exclude(group = "io.netty")
|
||||
})
|
||||
bundle(implementation("io.netty.incubator:netty-incubator-codec-native-quic:0.0.48.Final:windows-x86_64") {
|
||||
exclude(group = "io.netty")
|
||||
})
|
||||
bundle(implementation("io.netty.incubator:netty-incubator-codec-native-quic:0.0.48.Final:osx-x86_64") {
|
||||
exclude(group = "io.netty")
|
||||
})
|
||||
bundle(implementation("io.netty.incubator:netty-incubator-codec-native-quic:0.0.48.Final:linux-aarch_64") {
|
||||
exclude(group = "io.netty")
|
||||
})
|
||||
bundle(implementation("io.netty.incubator:netty-incubator-codec-native-quic:0.0.48.Final:osx-aarch_64") {
|
||||
exclude(group = "io.netty")
|
||||
})
|
||||
}
|
Loading…
Reference in a new issue