diff --git a/src/main/java/org/polyfrost/polyplus/mixin/client/MixinClientPacketListener.java b/src/main/java/org/polyfrost/polyplus/mixin/client/MixinClientPacketListener.java index 16254ce..886f3a6 100644 --- a/src/main/java/org/polyfrost/polyplus/mixin/client/MixinClientPacketListener.java +++ b/src/main/java/org/polyfrost/polyplus/mixin/client/MixinClientPacketListener.java @@ -24,8 +24,10 @@ public class MixinClientPacketListener { /*import net.minecraft.client.network.handler.ClientPlayNetworkHandler; import net.minecraft.network.packet.Packet; import net.minecraft.network.packet.c2s.play.ChatMessageC2SPacket; +import net.minecraft.network.packet.s2c.play.CustomPayloadS2CPacket; import org.polyfrost.polyplus.client.emoji.EmojiRegistry; import org.polyfrost.polyplus.client.network.p2p.P2PSessionManager; +import org.polyfrost.polyplus.client.serverapi.OneClientServerApi; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -49,6 +51,19 @@ public class MixinClientPacketListener { @Inject(method = "closeWorld", at = @At("HEAD")) private void polyplus$onDisconnect(CallbackInfo ci) { P2PSessionManager.onClientDisconnected(); + OneClientServerApi.reset(); + } + + @Inject( + method = "handleCustomPayload", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/network/PacketUtils;ensureOnSameThread(Lnet/minecraft/network/packet/Packet;Lnet/minecraft/network/handler/PacketHandler;Lnet/minecraft/util/BlockableEventLoop;)V", + shift = At.Shift.AFTER + ) + ) + private void polyplus$oneClientServerApi(CustomPayloadS2CPacket packet, CallbackInfo ci) { + OneClientServerApi.onLegacyPayload(packet.getChannel(), packet.getData()); } } *///?} diff --git a/src/main/kotlin/org/polyfrost/polyplus/client/PolyPlusClient.kt b/src/main/kotlin/org/polyfrost/polyplus/client/PolyPlusClient.kt index 3c8418d..a62df52 100644 --- a/src/main/kotlin/org/polyfrost/polyplus/client/PolyPlusClient.kt +++ b/src/main/kotlin/org/polyfrost/polyplus/client/PolyPlusClient.kt @@ -51,6 +51,7 @@ import org.polyfrost.polyplus.client.privacy.PrivacyGate import org.polyfrost.polyplus.client.privacy.RichTextPrivacy import org.polyfrost.polyplus.client.social.FriendsRepository import org.polyfrost.polyplus.client.social.GroupsRepository +import org.polyfrost.polyplus.client.serverapi.OneClientServerApi import org.polyfrost.polyplus.client.social.SessionsRepository import org.polyfrost.polyplus.client.utils.ClientPlatform import org.polyfrost.polyplus.client.utils.runSuspendCatching @@ -174,6 +175,7 @@ object PolyPlusClient { step("login gate") { MinecraftLoginGate.register() } step("session refresh") { SessionRefresh.register() } step("featured servers") { FeaturedServers.warmUp() } + step("oneclient server api") { OneClientServerApi.initialize() } //? if wwaypoints step("wwaypoints compat") { WWaypointsCompat.initialize() } diff --git a/src/main/kotlin/org/polyfrost/polyplus/client/serverapi/OneClientServerApi.kt b/src/main/kotlin/org/polyfrost/polyplus/client/serverapi/OneClientServerApi.kt new file mode 100644 index 0000000..b999016 --- /dev/null +++ b/src/main/kotlin/org/polyfrost/polyplus/client/serverapi/OneClientServerApi.kt @@ -0,0 +1,114 @@ +package org.polyfrost.polyplus.client.serverapi + +import net.fabricmc.loader.api.FabricLoader +import org.apache.logging.log4j.LogManager +import org.polyfrost.polyplus.PolyPlusConstants +import java.util.concurrent.atomic.AtomicBoolean + +//? if > 1.8.9 { +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking +import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry +//?} else { +/*import io.netty.buffer.Unpooled +import net.minecraft.client.Minecraft +import net.minecraft.network.PacketByteBuf +import net.minecraft.network.packet.c2s.play.CustomPayloadC2SPacket +*///?} + +//? if >= 26.1 { +import net.fabricmc.fabric.api.client.networking.v1.ServerboundPlayChannelEvents +//?} else if > 1.8.9 { +/*import net.fabricmc.fabric.api.client.networking.v1.C2SPlayChannelEvents +*///?} + +object OneClientServerApi { + private val LOGGER = LogManager.getLogger("${PolyPlusConstants.NAME}/ServerApi") + private val helloSent = AtomicBoolean(false) + + @Volatile + var serverApiVersion: String? = null + private set + + val isConnected: Boolean + get() = serverApiVersion != null + + fun initialize() { + //? if >= 26.1 { + PayloadTypeRegistry.serverboundPlay().register(ServerApiPayload.TYPE, ServerApiPayload.CODEC) + PayloadTypeRegistry.clientboundPlay().register(ServerApiPayload.TYPE, ServerApiPayload.CODEC) + ServerboundPlayChannelEvents.REGISTER.register { _, _, _, channels -> + if (ServerApiPayload.TYPE.id() in channels) sendHello() + } + //?} else if > 1.8.9 { + /*PayloadTypeRegistry.playC2S().register(ServerApiPayload.TYPE, ServerApiPayload.CODEC) + PayloadTypeRegistry.playS2C().register(ServerApiPayload.TYPE, ServerApiPayload.CODEC) + C2SPlayChannelEvents.REGISTER.register { _, _, _, channels -> + if (ServerApiPayload.TYPE.id() in channels) sendHello() + } + *///?} + + //? if > 1.8.9 { + ClientPlayNetworking.registerGlobalReceiver(ServerApiPayload.TYPE) { payload, _ -> handle(payload.data) } + ClientPlayConnectionEvents.JOIN.register { _, _, _ -> + if (ClientPlayNetworking.canSend(ServerApiPayload.TYPE)) sendHello() + } + ClientPlayConnectionEvents.DISCONNECT.register { _, _ -> reset() } + //?} + } + + //? if = 1.8.9 { + /*@JvmStatic + fun onLegacyPayload(channel: String, data: PacketByteBuf) { + when (channel) { + "REGISTER" -> { + val channels = data.toString(Charsets.UTF_8).split('\u0000') + if (ServerApiProtocol.CHANNEL in channels) sendHello() + } + ServerApiProtocol.CHANNEL -> { + val bytes = ByteArray(data.readableBytes()) + data.getBytes(data.readerIndex(), bytes) + handle(bytes) + } + } + } + *///?} + + @JvmStatic + fun reset() { + helloSent.set(false) + serverApiVersion = null + } + + private fun sendHello() { + if (!helloSent.compareAndSet(false, true)) return + + val minecraftVersion = FabricLoader.getInstance() + .getModContainer("minecraft") + .map { it.metadata.version.friendlyString } + .orElse("unknown") + val hello = ServerApiProtocol.encodeHello(PolyPlusConstants.VERSION, minecraftVersion) + + //? if > 1.8.9 { + ClientPlayNetworking.send(ServerApiPayload(hello)) + //?} else { + /*val connection = Minecraft.getInstance().networkHandler ?: return + connection.sendPacket(CustomPayloadC2SPacket("REGISTER", PacketByteBuf(Unpooled.wrappedBuffer(ServerApiProtocol.CHANNEL.toByteArray(Charsets.UTF_8))))) + connection.sendPacket(CustomPayloadC2SPacket(ServerApiProtocol.CHANNEL, PacketByteBuf(Unpooled.wrappedBuffer(hello)))) + *///?} + } + + private fun handle(data: ByteArray) { + val packet = runCatching { ServerApiProtocol.decode(data) } + .onFailure { LOGGER.warn("Ignoring malformed OneClient server API packet: {}", it.message) } + .getOrNull() + + when (packet) { + is ServerApiProtocol.Welcome -> { + serverApiVersion = packet.serverApiVersion + LOGGER.info("Server supports the OneClient server API {} (protocol {})", packet.serverApiVersion, packet.protocolVersion) + } + null -> Unit + } + } +} diff --git a/src/main/kotlin/org/polyfrost/polyplus/client/serverapi/ServerApiPayload.kt b/src/main/kotlin/org/polyfrost/polyplus/client/serverapi/ServerApiPayload.kt new file mode 100644 index 0000000..da66c9e --- /dev/null +++ b/src/main/kotlin/org/polyfrost/polyplus/client/serverapi/ServerApiPayload.kt @@ -0,0 +1,25 @@ +package org.polyfrost.polyplus.client.serverapi + +//? if > 1.8.9 { +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +import net.minecraft.resources.Identifier + +class ServerApiPayload(val data: ByteArray) : CustomPacketPayload { + override fun type(): CustomPacketPayload.Type = TYPE + + companion object { + @JvmField + val TYPE = CustomPacketPayload.Type( + Identifier.fromNamespaceAndPath(ServerApiProtocol.NAMESPACE, ServerApiProtocol.PATH) + ) + + @JvmField + val CODEC: StreamCodec = StreamCodec.of( + { buf, payload -> buf.writeBytes(payload.data) }, + { buf -> ServerApiPayload(ByteArray(buf.readableBytes()).also { buf.readBytes(it) }) }, + ) + } +} +//?} diff --git a/src/main/kotlin/org/polyfrost/polyplus/client/serverapi/ServerApiProtocol.kt b/src/main/kotlin/org/polyfrost/polyplus/client/serverapi/ServerApiProtocol.kt new file mode 100644 index 0000000..2122b6a --- /dev/null +++ b/src/main/kotlin/org/polyfrost/polyplus/client/serverapi/ServerApiProtocol.kt @@ -0,0 +1,81 @@ +package org.polyfrost.polyplus.client.serverapi + +import java.io.ByteArrayOutputStream + +object ServerApiProtocol { + const val NAMESPACE = "oneclient" + const val PATH = "api" + const val CHANNEL = "$NAMESPACE:$PATH" + const val VERSION = 1 + + private const val SERVERBOUND_HELLO = 0x00 + private const val CLIENTBOUND_WELCOME = 0x00 + private const val MAX_STRING_BYTES = 256 + + sealed interface Clientbound + data class Welcome(val protocolVersion: Int, val serverApiVersion: String) : Clientbound + + fun encodeHello(clientVersion: String, minecraftVersion: String): ByteArray = + Writer() + .varInt(SERVERBOUND_HELLO) + .varInt(VERSION) + .string(clientVersion) + .string(minecraftVersion) + .bytes() + + fun decode(data: ByteArray): Clientbound? { + val reader = Reader(data) + return when (reader.varInt()) { + CLIENTBOUND_WELCOME -> Welcome(reader.varInt(), reader.string()) + else -> null + } + } + + private class Writer { + private val out = ByteArrayOutputStream() + + fun varInt(value: Int): Writer { + var remaining = value + while (remaining and 0x7F.inv() != 0) { + out.write((remaining and 0x7F) or 0x80) + remaining = remaining ushr 7 + } + out.write(remaining) + return this + } + + fun string(value: String): Writer { + val encoded = value.toByteArray(Charsets.UTF_8) + varInt(encoded.size) + out.write(encoded, 0, encoded.size) + return this + } + + fun bytes(): ByteArray = out.toByteArray() + } + + private class Reader(private val data: ByteArray) { + private var position = 0 + + fun varInt(): Int { + var value = 0 + for (shift in 0 until 35 step 7) { + val b = byte() + value = value or ((b and 0x7F) shl shift) + if (b and 0x80 == 0) return value + } + throw IllegalArgumentException("VarInt is too long") + } + + fun string(): String { + val length = varInt() + require(length in 0..MAX_STRING_BYTES && length <= data.size - position) { "Bad string length $length" } + return String(data, position, length, Charsets.UTF_8).also { position += length } + } + + private fun byte(): Int { + require(position < data.size) { "Unexpected end of packet" } + return data[position++].toInt() and 0xFF + } + } +}