Compare commits

...

25 commits
main ... main

Author SHA1 Message Date
me
27f0af2afc Merge pull request 'Added sv_se.json' (#51) from el97/e4mc_minecraft:main into main
Reviewed-on: me/e4mc_minecraft#51
2024-02-06 09:38:55 +01:00
810adc433a Added sv_se.json
Provided a Swedish translation
2024-02-02 18:49:37 +01:00
ccb7025b74
oops 2023-08-21 22:25:35 +09:00
8a681524b2
4.0.1 2023-08-21 21:37:38 +09:00
66247df95d
4.0.0 with bad build system 2023-07-20 21:32:44 +09:00
ceb0bedb79
3.2.0 2023-06-06 17:03:24 +09:00
66d842c3ed
chore: add icon 2023-06-06 17:02:59 +09:00
ea1cd44cbc
chore: rename resource directory 2023-06-06 16:59:49 +09:00
14130097c5
chore: bump deps 2023-06-06 16:58:38 +09:00
b8e21f4849 i18n: Update Traditional Chinese (#21)
Reviewed-on: me/e4mc_minecraft#21
Co-authored-by: notlin4 <notlin4@noreply.localhost>
Co-committed-by: notlin4 <notlin4@noreply.localhost>
2023-06-05 11:08:35 +00:00
6042ea2ede
3.1.0 2023-05-14 21:47:46 +09:00
dfb6935b7b
feat: error handling 2023-05-14 21:47:21 +09:00
me
017d5f88f0 Merge pull request 'touch up the server network io mixin and restrict /e4mc stop' (#19) from BasiqueEvangelist/e4mc_minecraft:some-fixes into main
Reviewed-on: me/e4mc_minecraft#19
2023-05-14 05:54:37 +00:00
574ca7dbe4
chmod +x gradlew 2023-05-14 02:16:43 +03:00
db9293e658
restrict /e4mc stop and touchup mixin 2023-05-14 02:16:34 +03:00
me
b8af07aa4e Merge pull request 'Add Traditional Chinese' (#17) from notlin4/e4mc_minecraft:main into main
Reviewed-on: me/e4mc_minecraft#17
2023-04-24 08:54:21 +00:00
65f33361c2 新增「src/main/resources/assets/e4mc_quilt/lang/zh_tw.json」 2023-04-22 08:00:39 +00:00
90bf86654d
docs: remove a few badges 2023-04-22 13:03:42 +09:00
914ec34792
docs: better? 2023-04-22 13:03:03 +09:00
03bf7973d9
docs: readme 2023-04-22 13:01:52 +09:00
me
0fa970ba64 Merge pull request 'Allow configurable ingress URI' (#14) from pandaninjas/e4mc_minecraft:main into main
Reviewed-on: me/e4mc_minecraft#14
2023-04-22 03:13:52 +00:00
me
db702f60ff Merge pull request 'Simplified Chinese Translation' (#15) from GodGun968/e4mc_minecraft:main into main
Reviewed-on: me/e4mc_minecraft#15
2023-04-22 03:13:36 +00:00
d802ff27d8 更新 'src/main/resources/assets/e4mc_quilt/lang/zh_cn.json' 2023-04-21 09:59:15 +00:00
7f646312b9 Simplified Chinese Translation 2023-04-21 09:57:59 +00:00
82d7d802ff
Allow configurable ingress URI 2023-04-20 22:13:24 -04:00
24 changed files with 623 additions and 325 deletions

View file

@ -4,9 +4,6 @@
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <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="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules"> <option name="modules">
<set> <set>

23
README.md Normal file
View file

@ -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)

View file

@ -5,8 +5,8 @@ plugins {
preprocess { preprocess {
val fabric11904 = createNode("1.19.4-fabric", 11903, "yarn") val fabric11904 = createNode("1.19.4-fabric", 11903, "yarn")
val fabric11802 = createNode("1.18.2-fabric", 11802, "yarn") val fabric11802 = createNode("1.18.2-fabric", 11802, "yarn")
val forge11904 = createNode("1.19.4-forge", 11903, "yarn") val forge11904 = createNode("1.19.4-forge", 11903, "srg")
val forge11802 = createNode("1.18.2-forge", 11802, "yarn") val forge11802 = createNode("1.18.2-forge", 11802, "srg")
fabric11904.link(forge11904) fabric11904.link(forge11904)
forge11904.link(forge11802) forge11904.link(forge11802)

View file

@ -3,5 +3,5 @@ org.gradle.jvmargs=-Xmx2G
mod.name=e4mc mod.name=e4mc
mod.id=e4mc_minecraft mod.id=e4mc_minecraft
mod.version=3.0.0 mod.version=4.0.1
mod.group=vg.skye mod.group=vg.skye

0
gradlew vendored Normal file → Executable file
View file

View file

@ -10,7 +10,6 @@ pluginManagement {
maven("https://repo.essential.gg/repository/maven-public") maven("https://repo.essential.gg/repository/maven-public")
maven("https://server.bbkr.space/artifactory/libs-release/") maven("https://server.bbkr.space/artifactory/libs-release/")
maven("https://jitpack.io/") maven("https://jitpack.io/")
// Snapshots // Snapshots
maven("https://maven.deftu.xyz/snapshots") maven("https://maven.deftu.xyz/snapshots")
mavenLocal() mavenLocal()
@ -25,7 +24,7 @@ pluginManagement {
kotlin("jvm") version(kotlin) kotlin("jvm") version(kotlin)
kotlin("plugin.serialization") version(kotlin) kotlin("plugin.serialization") version(kotlin)
val epgt = "1.10.3" val epgt = "1.17.1"
id("xyz.deftu.gradle.multiversion-root") version(epgt) id("xyz.deftu.gradle.multiversion-root") version(epgt)
} }
} }

View file

@ -5,16 +5,16 @@ import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ServerChannel; import io.netty.channel.ServerChannel;
import io.netty.channel.local.LocalAddress; import io.netty.channel.local.LocalAddress;
import io.netty.channel.local.LocalServerChannel; import io.netty.channel.local.LocalServerChannel;
import io.netty.channel.socket.ServerSocketChannel;
import net.minecraft.server.ServerNetworkIo; import net.minecraft.server.ServerNetworkIo;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import vg.skye.e4mc_minecraft.E4mcClient; import vg.skye.e4mc_minecraft.E4mcClient;
import vg.skye.e4mc_minecraft.E4mcRelayHandler; import vg.skye.e4mc_minecraft.QuiclimeHandler;
import javax.annotation.Nullable; import javax.annotation.Nullable;
import java.io.IOException; import java.io.IOException;
@ -39,15 +39,15 @@ public abstract class ServerNetworkIoMixin {
initializingE4mc.set(false); initializingE4mc.set(false);
} }
} else { } else {
E4mcRelayHandler handler = new E4mcRelayHandler(); QuiclimeHandler handler = new QuiclimeHandler();
E4mcClient.HANDLER = handler; E4mcClient.HANDLER = handler;
handler.connect(); handler.startAsync();
} }
} }
@Redirect(method = "bind", at = @At(value = "INVOKE", target = "Lio/netty/bootstrap/ServerBootstrap;channel(Ljava/lang/Class;)Lio/netty/bootstrap/AbstractBootstrap;", remap = false)) @ModifyArg(method = "bind", at = @At(value = "INVOKE", target = "Lio/netty/bootstrap/ServerBootstrap;channel(Ljava/lang/Class;)Lio/netty/bootstrap/AbstractBootstrap;", remap = false))
private AbstractBootstrap<ServerBootstrap, ServerChannel> redirectChannel(ServerBootstrap instance, Class<? extends ServerSocketChannel> aClass) { private Class<? extends ServerChannel> redirectChannel(Class<? extends ServerChannel> aClass) {
return initializingE4mc.get() ? instance.channel(LocalServerChannel.class) : instance.channel(aClass); return initializingE4mc.get() ? LocalServerChannel.class : aClass;
} }
@Redirect(method = "bind", at = @At(value = "INVOKE", target = "Lio/netty/bootstrap/ServerBootstrap;localAddress(Ljava/net/InetAddress;I)Lio/netty/bootstrap/AbstractBootstrap;", remap = false)) @Redirect(method = "bind", at = @At(value = "INVOKE", target = "Lio/netty/bootstrap/ServerBootstrap;localAddress(Ljava/net/InetAddress;I)Lio/netty/bootstrap/AbstractBootstrap;", remap = false))
@ -58,7 +58,7 @@ public abstract class ServerNetworkIoMixin {
@Inject(method = "stop", at = @At("HEAD")) @Inject(method = "stop", at = @At("HEAD"))
private void stop(CallbackInfo ci) { private void stop(CallbackInfo ci) {
if (E4mcClient.HANDLER != null) { if (E4mcClient.HANDLER != null) {
E4mcClient.HANDLER.close(); E4mcClient.HANDLER.stop();
} }
} }

View 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
}
}

View 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)
}
}

View file

@ -33,11 +33,11 @@ object E4mcClient : ModInitializer {
//#endif //#endif
const val NAME = "e4mc" const val NAME = "e4mc"
const val ID = "e4mc_minecraft" const val ID = "e4mc_minecraft"
const val VERSION = "3.0.0" const val VERSION = "4.0.1"
@JvmField @JvmField
val LOGGER: Logger = LoggerFactory.getLogger("e4mc") val LOGGER: Logger = LoggerFactory.getLogger("e4mc")
@JvmField @JvmField
var HANDLER: E4mcRelayHandler? = null var HANDLER: QuiclimeHandler? = null
//#if FABRIC==1 //#if FABRIC==1
override fun onInitialize() { override fun onInitialize() {
@ -46,56 +46,14 @@ object E4mcClient : ModInitializer {
//#else //#else
//$$ CommandRegistrationCallback.EVENT.register { dispatcher, _ -> //$$ CommandRegistrationCallback.EVENT.register { dispatcher, _ ->
//#endif //#endif
dispatcher.register(literal("e4mc") CommandsHelper.registerCommandWithDispatcher(dispatcher)
.then(
literal("stop")
.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
}
))
} }
} }
//#else //#else
//$$ @SubscribeEvent //$$ @SubscribeEvent
//$$ fun onRegisterCommandEvent(event: RegisterCommandsEvent) { //$$ fun onRegisterCommandEvent(event: RegisterCommandsEvent) {
//$$ val commandDispatcher = event.getDispatcher() //$$ val dispatcher = event.getDispatcher()
//$$ commandDispatcher.register(literal("e4mc") //$$ CommandsHelper.registerCommandWithDispatcher(dispatcher)
//$$ .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
//$$ }
//$$ ))
//$$ } //$$ }
//#endif //#endif
} }

View file

@ -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)
}
}
}

View 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
}
}
}
}
}

View file

@ -20,6 +20,6 @@ side="BOTH"
[[dependencies.${mod_id}]] [[dependencies.${mod_id}]]
modId="minecraft" modId="minecraft"
mandatory=true mandatory=true
versionRange="${mc_version == "1.19.4" ? "[1.19,)" : "[1.17,1.19)"}" versionRange="${mc_version == "1.19.4" ? "[1.19,)" : "[1.18,1.19)"}"
ordering="NONE" ordering="NONE"
side="BOTH" side="BOTH"

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -2,5 +2,6 @@
"text.e4mc_minecraft.domainAssigned": "Local game hosted on domain [%s]", "text.e4mc_minecraft.domainAssigned": "Local game hosted on domain [%s]",
"text.e4mc_minecraft.closeServer": "Local game no longer publicly hosted", "text.e4mc_minecraft.closeServer": "Local game no longer publicly hosted",
"text.e4mc_minecraft.serverAlreadyClosed": "Local game not publicly hosted", "text.e4mc_minecraft.serverAlreadyClosed": "Local game not publicly hosted",
"text.e4mc_minecraft.clickToStop": " (Click here to stop)" "text.e4mc_minecraft.clickToStop": " (Click here to stop)",
"text.e4mc_minecraft.error": "An error occurred in e4mc"
} }

View file

@ -2,5 +2,6 @@
"text.e4mc_minecraft.domainAssigned": "로컬 게임을 도메인 [%s]에서 호스트합니다", "text.e4mc_minecraft.domainAssigned": "로컬 게임을 도메인 [%s]에서 호스트합니다",
"text.e4mc_minecraft.closeServer": "로컬 게임이 더이상 공개되지 않습니다", "text.e4mc_minecraft.closeServer": "로컬 게임이 더이상 공개되지 않습니다",
"text.e4mc_minecraft.serverAlreadyClosed": "로컬 게임이 이미 공개중이 아닙니다", "text.e4mc_minecraft.serverAlreadyClosed": "로컬 게임이 이미 공개중이 아닙니다",
"text.e4mc_minecraft.clickToStop": " (멉추려면 여기를 클릭하세요)" "text.e4mc_minecraft.clickToStop": " (멉추려면 여기를 클릭하세요)",
"text.e4mc_minecraft.error": "e4mc에서 오류가 발생했습니다."
} }

View file

@ -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"
}

View file

@ -0,0 +1,6 @@
{
"text.e4mc_minecraft.domainAssigned": "将本地游戏托管在域名[%s]上",
"text.e4mc_minecraft.closeServer": "不再公开托管本地游戏",
"text.e4mc_minecraft.serverAlreadyClosed": "本地游戏没有被公开托管",
"text.e4mc_minecraft.clickToStop": "(点击这里以停止)"
}

View file

@ -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 中發生了個錯誤"
}

View file

@ -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": []
} }

View file

@ -14,6 +14,7 @@
"sources": "https://git.skye.vg/me/e4mc_minecraft" "sources": "https://git.skye.vg/me/e4mc_minecraft"
}, },
"license": "MIT", "license": "MIT",
"icon": "assets/e4mc_minecraft/icon.png",
"environment": "*", "environment": "*",
"entrypoints": { "entrypoints": {
"main": [ "main": [
@ -30,7 +31,7 @@
"fabric": "*", "fabric": "*",
"fabricloader": ">=0.14.9", "fabricloader": ">=0.14.9",
"fabric-language-kotlin": "*", "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" "java": ">=16"
} }
} }

View file

@ -1,7 +1,6 @@
import com.modrinth.minotaur.dependencies.DependencyType import com.modrinth.minotaur.dependencies.DependencyType
import com.modrinth.minotaur.dependencies.ModDependency import com.modrinth.minotaur.dependencies.ModDependency
import org.gradle.configurationcache.extensions.capitalized 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.CurseRelation
import xyz.deftu.gradle.tools.minecraft.CurseRelationType import xyz.deftu.gradle.tools.minecraft.CurseRelationType
@ -16,27 +15,33 @@ plugins {
} }
val bundle by configurations.creating { 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) configurations.getByName("include").extendsFrom(this)
} else configurations.getByName("shade").extendsFrom(this) } else configurations.getByName("shade").extendsFrom(this)
} }
loomHelper { toolkitLoomHelper {
if (mcData.isForge) { if (mcData.isForge) {
useForgeMixin("e4mc_minecraft.mixins.json", true) useForgeMixin("e4mc_minecraft.mixins.json", true)
} }
} }
releases { java {
withSourcesJar()
}
toolkitReleases {
gameVersions.set(when (mcData.version) { gameVersions.set(when (mcData.version) {
11904 -> listOf("1.19", "1.19.1", "1.19.2", "1.19.3", "1.19.4") 11904 -> listOf("1.19", "1.19.1", "1.19.2", "1.19.3", "1.19.4", "1.20", "1.20.1")
11802 -> listOf("1.17", "1.17.1", "1.18", "1.18.1", "1.18.2") 11802 -> listOf("1.18", "1.18.1", "1.18.2")
else -> listOf() else -> listOf()
}) })
version.set("${modData.version}+${mcData.versionStr}-${mcData.loader.name}")
releaseName.set("[${when (mcData.version) { releaseName.set("[${when (mcData.version) {
11904 -> "1.19-" 11904 -> "1.19-"
11802 -> "1.17-1.18.2" 11802 -> "1.18.x"
else -> mcData.versionStr else -> mcData.versionStr
}}] [${mcData.loader.name.capitalized()}] ${modData.version}") }}] [${mcData.loader.name.capitalized()}] ${modData.version}")
if (mcData.isFabric) { if (mcData.isFabric) {
@ -44,6 +49,7 @@ releases {
} }
modrinth { modrinth {
projectId.set("qANg5Jrr") projectId.set("qANg5Jrr")
useSourcesJar.set(true)
if (mcData.isFabric) { if (mcData.isFabric) {
dependencies.set( dependencies.set(
listOf( listOf(
@ -102,7 +108,24 @@ dependencies {
} else if (mcData.isForge) { } else if (mcData.isForge) {
implementation("thedarkcolour:kotlinforforge:3.8.0") 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:57a52c4") {
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")
// })
}