forked from me/e4mc_minecraft
Compare commits
25 commits
Author | SHA1 | Date | |
---|---|---|---|
27f0af2afc | |||
810adc433a | |||
ccb7025b74 | |||
8a681524b2 | |||
66247df95d | |||
ceb0bedb79 | |||
66d842c3ed | |||
ea1cd44cbc | |||
14130097c5 | |||
b8e21f4849 | |||
6042ea2ede | |||
dfb6935b7b | |||
017d5f88f0 | |||
574ca7dbe4 | |||
db9293e658 | |||
b8af07aa4e | |||
65f33361c2 | |||
90bf86654d | |||
914ec34792 | |||
03bf7973d9 | |||
0fa970ba64 | |||
db702f60ff | |||
d802ff27d8 | |||
7f646312b9 | |||
82d7d802ff |
24 changed files with 623 additions and 325 deletions
|
@ -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
23
README.md
Normal 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)
|
|
@ -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)
|
||||||
|
|
|
@ -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
0
gradlew
vendored
Normal file → Executable 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
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
|
//#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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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"
|
||||||
|
|
BIN
src/main/resources/assets/e4mc_minecraft/icon.png
Normal file
BIN
src/main/resources/assets/e4mc_minecraft/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
|
@ -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"
|
||||||
}
|
}
|
|
@ -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에서 오류가 발생했습니다."
|
||||||
}
|
}
|
7
src/main/resources/assets/e4mc_minecraft/lang/sv_se.json
Normal file
7
src/main/resources/assets/e4mc_minecraft/lang/sv_se.json
Normal 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"
|
||||||
|
}
|
6
src/main/resources/assets/e4mc_minecraft/lang/zh_cn.json
Normal file
6
src/main/resources/assets/e4mc_minecraft/lang/zh_cn.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"text.e4mc_minecraft.domainAssigned": "将本地游戏托管在域名[%s]上",
|
||||||
|
"text.e4mc_minecraft.closeServer": "不再公开托管本地游戏",
|
||||||
|
"text.e4mc_minecraft.serverAlreadyClosed": "本地游戏没有被公开托管",
|
||||||
|
"text.e4mc_minecraft.clickToStop": "(点击这里以停止)"
|
||||||
|
}
|
7
src/main/resources/assets/e4mc_minecraft/lang/zh_tw.json
Normal file
7
src/main/resources/assets/e4mc_minecraft/lang/zh_tw.json
Normal 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 中發生了個錯誤"
|
||||||
|
}
|
|
@ -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": []
|
||||||
}
|
}
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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")
|
||||||
|
// })
|
||||||
|
}
|
Loading…
Reference in a new issue