diff --git a/.github/scripts/server-smoke.py b/.github/scripts/server-smoke.py new file mode 100644 index 00000000..a788d5d6 --- /dev/null +++ b/.github/scripts/server-smoke.py @@ -0,0 +1,163 @@ +import argparse +import json +import queue +import socket +import struct +import subprocess +import sys +import threading +import time +import urllib.request +from pathlib import Path +import shutil + + +MAGIC = bytes.fromhex('00ffff00fefefefefdfdfdfd12345678') +PROTOCOLS = {'26.1': 775, '26.1.1': 775, '26.1.2': 775, '26.2': 776, '26.3': 777, + '26.4-snapshot-7': 1073742169} + + +def varint(value): + value &= 0xffffffff + result = bytearray() + while value > 127: + result.append((value & 127) | 128) + value >>= 7 + return bytes(result + bytes([value])) + + +def read_varint(stream): + value = 0 + for shift in range(0, 35, 7): + part = stream.read(1) + if not part: + raise EOFError('Incomplete VarInt') + value |= (part[0] & 127) << shift + if part[0] < 128: + return value + raise ValueError('VarInt exceeds five bytes') + + +def check_tcp(port, version): + host = b'127.0.0.1' + handshake = b'\x00' + varint(PROTOCOLS[version]) + varint(len(host)) + host + struct.pack('>H', port) + b'\x01' + with socket.create_connection(('127.0.0.1', port), timeout=10) as connection: + connection.sendall(varint(len(handshake)) + handshake + b'\x01\x00') + stream = connection.makefile('rb') + length = read_varint(stream) + assert 0 < length < 1048576, length + assert read_varint(stream) == 0 + size = read_varint(stream) + assert 0 < size < length + status = json.loads(stream.read(size)) + assert status['version']['name'] == version.replace('-snapshot-', ' Snapshot '), status + assert status['version']['protocol'] == PROTOCOLS[version], status + + +def check_udp(port): + # Open Connection Request 1 negotiates the RakNet transport before Minecraft login. + request = (b'\x05' + MAGIC + b'\x09').ljust(1200, b'\x00') + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as connection: + connection.settimeout(10) + connection.connect(('127.0.0.1', port)) + connection.send(request) + reply = connection.recv(8192) + assert len(reply) == 28 and reply[0] == 6, reply.hex() + assert reply[1:17] == MAGIC and reply[25] == 0, reply.hex() + assert 128 <= struct.unpack('>H', reply[26:28])[0] <= 1200 + + +def start_server(folder, log, events): + process = subprocess.Popen(['java', '-Xms256M', '-Xmx1G', '-jar', 'fabric-server-launch.jar', 'nogui'], + cwd=folder, stdin=subprocess.PIPE, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, text=True, encoding='utf-8', errors='replace') + + def collect(): + with log.open('w', encoding='utf-8') as output: + for line in process.stdout: + output.write(line) + output.flush() + events.put(line) + + reader = threading.Thread(target=collect) + reader.start() + return process, reader + + +def check_server(folder, version, native): + port = 25871 + (folder / 'eula.txt').write_text('eula=true\n') + (folder / 'server.properties').write_text( + 'server-ip=127.0.0.1\nserver-port=25871\nonline-mode=false\n' + 'view-distance=2\nsimulation-distance=2\nspawn-chunk-radius=0\n' + 'level-type=minecraft:flat\nmax-players=2\n' + 'use-native-transport=' + str(native).lower() + '\nlevel-name=world-' + str(native).lower() + '\n') + log = folder / ('native.log' if native else 'nio.log') + events = queue.Queue() + process, reader = start_server(folder, log, events) + try: + deadline = time.monotonic() + 180 + while time.monotonic() < deadline: + if process.poll() is not None: + raise RuntimeError('Server exited before readiness: ' + str(process.returncode)) + try: + if 'Done (' in events.get(timeout=1): + break + except queue.Empty: + pass + else: + raise TimeoutError('Server startup exceeded 180 seconds') + for attempt in range(10): + try: + check_tcp(port, version) + break + except (EOFError, OSError): + if attempt == 9: + raise + time.sleep(0.5) + check_udp(port) + dump = subprocess.check_output(['jcmd', str(process.pid), 'Thread.print'], text=True) + (folder / ('native-threads.log' if native else 'nio-threads.log')).write_text(dump) + transport = 'Epoll' if native and sys.platform.startswith('linux') else 'NIO' + assert 'Netty Server ' + transport + ' Raknetify' in dump, transport + process.stdin.write('stop\n') + process.stdin.flush() + assert process.wait(timeout=45) == 0 + reader.join(timeout=10) + text = log.read_text() + for failure in ('Critical injection failure', 'Mixin apply failed', 'NoClassDefFoundError', 'LEAK:'): + assert failure not in text, failure + print('PASS', version, transport, 'startup, TCP status, RakNet UDP response, shutdown', flush=True) + finally: + if process.poll() is None: + try: + process.stdin.write('stop\n') + process.stdin.flush() + process.wait(timeout=45) + except (OSError, subprocess.TimeoutExpired): + process.kill() + process.wait(timeout=10) + reader.join(timeout=10) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--jar', type=Path, required=True) + parser.add_argument('--work-dir', type=Path, required=True) + parser.add_argument('--versions', nargs='+', default=['26.1', '26.1.1', '26.1.2', '26.2', '26.3', '26.4-snapshot-7']) + args = parser.parse_args() + for version in args.versions: + folder = args.work_dir / version + (folder / 'mods').mkdir(parents=True, exist_ok=True) + shutil.copy2(args.jar, folder / 'mods' / args.jar.name) + launcher = folder / 'fabric-server-launch.jar' + if not launcher.exists(): + url = f'https://meta.fabricmc.net/v2/versions/loader/{version}/0.20.1/1.1.1/server/jar' + with urllib.request.urlopen(url, timeout=60) as response: + launcher.write_bytes(response.read()) + check_server(folder, version, True) + check_server(folder, version, False) + + +if __name__ == '__main__': + main() diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7257cbd6..dd6bfc2b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ jobs: matrix: # Use these Java versions java: [ - 21, # Current Java LTS & minimum supported by Minecraft + 25, # Required by Minecraft 26.x ] # and run on both Linux and Windows os: [ubuntu-24.04] @@ -42,6 +42,14 @@ jobs: ${{ runner.os }}-gradle- - name: build run: ./gradlew build + - name: Linux server transports + run: python3 .github/scripts/server-smoke.py --jar fabric-26/build/libs/*-all.jar --work-dir "$RUNNER_TEMP/raknetify-smoke" + - name: capture server smoke logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: Linux-server-smoke-logs + path: ${{ runner.temp }}/raknetify-smoke/**/*.log - name: upload to modrinth and curseforge run: ./gradlew modrinth publishMainPublicationToCurseForge if: github.ref == 'refs/heads/master' @@ -51,7 +59,7 @@ jobs: GITHUB_EVENT_RAW_PATH: ${{ github.event_path }} continue-on-error: true - name: capture build artifacts - if: ${{ runner.os == 'Linux' && matrix.java == '21' }} # Only upload artifacts built from latest java on one OS + if: ${{ runner.os == 'Linux' && matrix.java == '25' }} # Only upload artifacts built from latest java on one OS uses: actions/upload-artifact@v4 with: name: Artifacts diff --git a/.github/workflows/checkMappings.yml b/.github/workflows/checkMappings.yml index c0350d6b..7fc09dc0 100644 --- a/.github/workflows/checkMappings.yml +++ b/.github/workflows/checkMappings.yml @@ -7,7 +7,7 @@ jobs: matrix: # Use these Java versions java: [ - 21, # Current Java LTS & minimum supported by Minecraft + 25, # Required by Minecraft 26.x ] # and run on both Linux and Windows os: [ubuntu-24.04] @@ -49,8 +49,7 @@ jobs: run: ./genMappings.sh - name: fail if mappings changed run: | - git update-index --refresh - if ! git diff-index --quiet HEAD --; then + if ! git diff --exit-code --; then echo "Mappings changed, please commit them" git diff git diff --staged diff --git a/Jenkinsfile b/Jenkinsfile index 5e655622..a2829fab 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -9,7 +9,7 @@ pipeline { } stage('Build') { tools { - jdk "OpenJDK 21" + jdk "OpenJDK 25" } steps { withMaven( diff --git a/build.gradle b/build.gradle index 7f19926f..af5782a1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,11 +1,16 @@ plugins { - id 'fabric-loom' version '1.13-SNAPSHOT' apply false + id 'fabric-loom' version '1.17.21' apply false + id 'net.fabricmc.fabric-loom' version '1.17.21' apply false id 'com.gradleup.shadow' version '9.2.1' apply false - id 'dev.yumi.gradle.licenser' version '2.1.1' apply false + id 'dev.yumi.gradle.licenser' version '4.0.0' apply false id 'com.modrinth.minotaur' version '2.+' apply false id 'io.github.themrmilchmann.curseforge-publish' version '0.8.0' apply false } +ext.remapJarTaskType = net.fabricmc.loom.task.RemapJarTask +ext.curseforgeAlpha = io.github.themrmilchmann.gradle.publish.curseforge.ReleaseType.ALPHA +ext.curseforgeMarkdown = io.github.themrmilchmann.gradle.publish.curseforge.ChangelogFormat.MARKDOWN + subprojects { apply plugin: 'java' apply plugin: 'java-library' @@ -18,8 +23,10 @@ subprojects { } version = project.mod_version + "." + getVersionSuffix() + if (project.name == 'fabric-26') version += '.mc26' license { + include '**/*.java' rule(rootProject.file('HEADER.txt')) } @@ -62,6 +69,12 @@ subprojects { versionType = "alpha" // This is the default -- can also be `beta` or `alpha` // uploadFile = remapShadowJar // With Loom, this MUST be set to `remapJar` instead of `jar`! gameVersions = ["1.18", "1.18.1", "1.18.2", "1.19", "1.19.1", "1.19.2", "1.19.3", "1.19.4", "1.20", "1.20.1", "1.20.2", "1.20.3", "1.20.4", "1.20.5", "1.20.6", "1.21", "1.21.1", "1.21.2", "1.21.3", "1.21.4", "1.21.5", "1.21.6", "1.21.7", "1.21.8", "1.21.9", "1.21.10", "1.21.11"] + def modernVersions = ['26.1', '26.1.1', '26.1.2', '26.2', '26.3'] + if (project.name == 'fabric-26') { + gameVersions = modernVersions + } else if (project.name != 'fabric') { + gameVersions.addAll(modernVersions) + } // loaders = ["fabric"] changelog = com.ishland.buildscript.ParseGItHubActionChangelog.getChangelog() } diff --git a/common/src/main/java/com/ishland/raknetify/common/connection/MultiChannelingStreamingCompression.java b/common/src/main/java/com/ishland/raknetify/common/connection/MultiChannelingStreamingCompression.java index b30ae01c..6c50642b 100644 --- a/common/src/main/java/com/ishland/raknetify/common/connection/MultiChannelingStreamingCompression.java +++ b/common/src/main/java/com/ishland/raknetify/common/connection/MultiChannelingStreamingCompression.java @@ -138,6 +138,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception compressedFrameData.release(); if (payload != null) payload.release(); } + return; } else if (compressedFrameData.getPacketId() == compressedPacketId && compressedFrameData.getReliability().isReliable && compressedFrameData.getReliability().isOrdered && !compressedFrameData.getReliability().isSequenced && inflaters[compressedFrameData.getOrderChannel()] != null) { final int orderChannel = compressedFrameData.getOrderChannel(); final Inflater inflater = inflaters[orderChannel]; diff --git a/common/src/main/java/com/ishland/raknetify/common/connection/MultiChannellingEncryption.java b/common/src/main/java/com/ishland/raknetify/common/connection/MultiChannellingEncryption.java index 9a9b1cf4..b528d55e 100644 --- a/common/src/main/java/com/ishland/raknetify/common/connection/MultiChannellingEncryption.java +++ b/common/src/main/java/com/ishland/raknetify/common/connection/MultiChannellingEncryption.java @@ -57,9 +57,23 @@ public MultiChannellingEncryption(Cipher decryption, Cipher encryption) { this.encryption = new PacketEncryptionManager(Objects.requireNonNull(encryption)); } + // A null manager passes that direction through while the handshake enables the other direction independently. + private MultiChannellingEncryption(PacketEncryptionManager decryption, PacketEncryptionManager encryption) { + this.decryption = decryption; + this.encryption = encryption; + } + + public static MultiChannellingEncryption inbound(Cipher cipher) { + return new MultiChannellingEncryption(new PacketEncryptionManager(Objects.requireNonNull(cipher)), null); + } + + public static MultiChannellingEncryption outbound(Cipher cipher) { + return new MultiChannellingEncryption(null, new PacketEncryptionManager(Objects.requireNonNull(cipher))); + } + @Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { - if (msg instanceof FrameData data) { + if (encryption != null && msg instanceof FrameData data) { data.touch(); final ByteBuf buf = data.createData().skipBytes(1); ByteBuf res = null; @@ -86,7 +100,7 @@ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { - if (msg instanceof FrameData data) { + if (decryption != null && msg instanceof FrameData data) { data.touch(); final ByteBuf buf = data.createData().skipBytes(1); ByteBuf res = null; diff --git a/common/src/main/java/com/ishland/raknetify/common/connection/SynchronizationLayer.java b/common/src/main/java/com/ishland/raknetify/common/connection/SynchronizationLayer.java index 4eec8dd0..df6064d1 100644 --- a/common/src/main/java/com/ishland/raknetify/common/connection/SynchronizationLayer.java +++ b/common/src/main/java/com/ishland/raknetify/common/connection/SynchronizationLayer.java @@ -200,6 +200,7 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception FIELD_RELIABILITY_LAST_RECEIVED_SEQ_ID.set(this.reliabilityHandler, seqId); } finally { byteBuf.release(); + packet.release(); } } return; diff --git a/common/src/main/resources/raknetify-channel-mappings.json b/common/src/main/resources/raknetify-channel-mappings.json index 704573ee..4fe274ac 100644 --- a/common/src/main/resources/raknetify-channel-mappings.json +++ b/common/src/main/resources/raknetify-channel-mappings.json @@ -3612,6 +3612,657 @@ "65": 1 } }, + "775": { + "s2c": { + "0": -2147483648, + "1": 2, + "2": 2, + "3": -1, + "4": 7, + "5": 7, + "6": 7, + "7": 7, + "8": 7, + "9": 1, + "10": 1, + "11": 7, + "12": 7, + "13": 7, + "14": 1, + "15": 1, + "16": 1, + "17": 3, + "18": 3, + "19": 3, + "20": 3, + "21": 1, + "22": 1, + "23": 1, + "24": 3, + "25": 2, + "26": 7, + "27": 7, + "28": 2, + "29": 2, + "30": 1, + "31": 1, + "32": -1, + "33": 1, + "34": 2, + "35": 2, + "36": 2, + "37": 7, + "38": 2, + "39": 1, + "40": 7, + "41": 3, + "42": 2, + "43": 3, + "44": -1, + "45": 7, + "46": 7, + "47": 4, + "48": 7, + "49": 2, + "50": 1, + "51": 4, + "52": 3, + "53": 2, + "54": 2, + "55": 2, + "56": 2, + "57": 2, + "58": 3, + "59": 3, + "60": 7, + "61": -1, + "62": -1, + "63": 3, + "64": 2, + "65": 1, + "66": 2, + "67": 2, + "68": 1, + "69": 1, + "70": 1, + "71": 1, + "72": 1, + "73": 2, + "74": 3, + "75": 3, + "76": 3, + "77": 2, + "78": 2, + "79": 1, + "80": -1, + "81": -1, + "82": 3, + "83": 2, + "84": 7, + "85": -1, + "86": 1, + "87": 1, + "88": 3, + "89": 3, + "90": 3, + "91": 3, + "92": 3, + "93": 2, + "94": 7, + "95": 7, + "96": 3, + "97": 2, + "98": 1, + "99": 2, + "100": 2, + "101": 2, + "102": 2, + "103": 1, + "104": 1, + "105": 3, + "106": 1, + "107": 2, + "108": 3, + "109": 1, + "110": 1, + "111": 7, + "112": 1, + "113": 1, + "114": 1, + "115": 1, + "116": 4, + "117": 4, + "118": 0, + "119": 4, + "120": 1, + "121": 1, + "122": 1, + "123": 3, + "124": 2, + "125": 2, + "126": 7, + "127": 3, + "128": 3, + "129": 0, + "130": 1, + "131": 2, + "132": 2, + "133": 1, + "134": 1, + "135": 2, + "136": -1, + "137": -1, + "138": 1, + "139": 1, + "140": 1 + }, + "c2s": { + "0": 3, + "1": 2, + "2": 3, + "3": 3, + "4": 1, + "5": 2, + "6": 1, + "7": 1, + "8": 1, + "9": 1, + "10": 1, + "11": 7, + "12": 3, + "13": 2, + "14": 1, + "15": 1, + "16": 0, + "17": 1, + "18": 1, + "19": 3, + "20": 3, + "21": 1, + "22": 3, + "23": 1, + "24": 3, + "25": 3, + "26": 2, + "27": 7, + "28": -1, + "29": 1, + "30": 2, + "31": 2, + "32": 2, + "33": 2, + "34": 2, + "35": 2, + "36": 1, + "37": 1, + "38": -1, + "39": 3, + "40": 3, + "41": 1, + "42": 1, + "43": 1, + "44": 7, + "45": -1, + "46": 3, + "47": 3, + "48": 3, + "49": -1, + "50": -1, + "51": 3, + "52": 3, + "53": 3, + "54": 3, + "55": 3, + "56": 3, + "57": 1, + "58": 3, + "59": 3, + "60": 3, + "61": 7, + "62": 3, + "63": 2, + "64": 3, + "65": 3, + "66": 2, + "67": 2, + "68": 1 + } + }, + "776": { + "s2c": { + "0": -2147483648, + "1": 2, + "2": 2, + "3": -1, + "4": 7, + "5": 7, + "6": 7, + "7": 7, + "8": 7, + "9": 1, + "10": 1, + "11": 7, + "12": 7, + "13": 7, + "14": 1, + "15": 1, + "16": 1, + "17": 3, + "18": 3, + "19": 3, + "20": 3, + "21": 1, + "22": 1, + "23": 1, + "24": 3, + "25": 2, + "26": 7, + "27": 7, + "28": 2, + "29": 2, + "30": 1, + "31": 1, + "32": -1, + "33": 1, + "34": 2, + "35": 2, + "36": 2, + "37": 7, + "38": 2, + "39": 1, + "40": 7, + "41": 3, + "42": 2, + "43": 3, + "44": -1, + "45": 7, + "46": 7, + "47": 4, + "48": 7, + "49": 2, + "50": 1, + "51": 4, + "52": 3, + "53": 2, + "54": 2, + "55": 2, + "56": 2, + "57": 2, + "58": 3, + "59": 3, + "60": 7, + "61": -1, + "62": -1, + "63": 3, + "64": 2, + "65": 1, + "66": 2, + "67": 2, + "68": 1, + "69": 1, + "70": 1, + "71": 1, + "72": 1, + "73": 2, + "74": 3, + "75": 3, + "76": 3, + "77": 2, + "78": 2, + "79": 1, + "80": -1, + "81": -1, + "82": 3, + "83": 2, + "84": 7, + "85": -1, + "86": 1, + "87": 1, + "88": 3, + "89": 3, + "90": 3, + "91": 3, + "92": 3, + "93": 2, + "94": 7, + "95": 7, + "96": 3, + "97": 2, + "98": 1, + "99": 2, + "100": 2, + "101": 2, + "102": 2, + "103": 1, + "104": 1, + "105": 3, + "106": 1, + "107": 2, + "108": 3, + "109": 1, + "110": 1, + "111": 7, + "112": 1, + "113": 1, + "114": 1, + "115": 1, + "116": 4, + "117": 4, + "118": 0, + "119": 4, + "120": 1, + "121": 1, + "122": 1, + "123": 3, + "124": 2, + "125": 2, + "126": 7, + "127": 3, + "128": 3, + "129": 0, + "130": 1, + "131": 2, + "132": 2, + "133": 1, + "134": 1, + "135": 2, + "136": -1, + "137": -1, + "138": 1, + "139": 1, + "140": 1 + }, + "c2s": { + "0": 3, + "1": 2, + "2": 3, + "3": 3, + "4": 1, + "5": 2, + "6": 1, + "7": 1, + "8": 1, + "9": 1, + "10": 1, + "11": 7, + "12": 3, + "13": 2, + "14": 1, + "15": 1, + "16": 0, + "17": 1, + "18": 1, + "19": 3, + "20": 3, + "21": 1, + "22": 3, + "23": 1, + "24": 3, + "25": 3, + "26": 2, + "27": 7, + "28": -1, + "29": 1, + "30": 2, + "31": 2, + "32": 2, + "33": 2, + "34": 2, + "35": 2, + "36": 1, + "37": 1, + "38": -1, + "39": 3, + "40": 3, + "41": 1, + "42": 1, + "43": 1, + "44": 7, + "45": -1, + "46": 3, + "47": 3, + "48": 3, + "49": -1, + "50": -1, + "51": 3, + "52": 3, + "53": 3, + "54": 3, + "55": 3, + "56": 3, + "57": 1, + "58": 3, + "59": 3, + "60": 3, + "61": 7, + "62": 3, + "63": 2, + "64": 3, + "65": 3, + "66": 2, + "67": 2, + "68": 1 + } + }, + "777": { + "s2c": { + "0": -2147483648, + "1": 2, + "2": 2, + "3": -1, + "4": 7, + "5": 7, + "6": 7, + "7": 7, + "8": 7, + "9": 1, + "10": 1, + "11": 7, + "12": 7, + "13": 7, + "14": 1, + "15": 1, + "16": 1, + "17": 3, + "18": 3, + "19": 3, + "20": 3, + "21": 1, + "22": 1, + "23": 1, + "24": 3, + "25": 2, + "26": 7, + "27": 7, + "28": 2, + "29": 2, + "30": 1, + "31": 1, + "32": -1, + "33": 1, + "34": 2, + "35": 2, + "36": 2, + "37": 7, + "38": 7, + "39": 2, + "40": 1, + "41": 7, + "42": 3, + "43": 2, + "44": 3, + "45": -1, + "46": 7, + "47": 7, + "48": 4, + "49": 7, + "50": 2, + "51": 1, + "52": 4, + "53": 3, + "54": 2, + "55": 2, + "56": 2, + "57": 2, + "58": 2, + "59": 3, + "60": 3, + "61": 7, + "62": -1, + "63": -1, + "64": 3, + "65": 2, + "66": 1, + "67": 2, + "68": 2, + "69": 1, + "70": 1, + "71": 1, + "72": 1, + "73": 1, + "74": 2, + "75": 3, + "76": 3, + "77": 3, + "78": 2, + "79": 2, + "80": 1, + "81": -1, + "82": -1, + "83": 4, + "84": 3, + "85": 2, + "86": 7, + "87": -1, + "88": 1, + "89": 1, + "90": 3, + "91": 3, + "92": 3, + "93": 3, + "94": 3, + "95": 2, + "96": 7, + "97": 7, + "98": 3, + "99": 2, + "100": 1, + "101": 2, + "102": 2, + "103": 2, + "104": 2, + "105": 1, + "106": 1, + "107": 3, + "108": 1, + "109": 2, + "110": 3, + "111": 1, + "112": 1, + "113": 7, + "114": 1, + "115": 1, + "116": 1, + "117": 1, + "118": 4, + "119": 4, + "120": 0, + "121": 4, + "122": 1, + "123": 2, + "124": 1, + "125": 1, + "126": 3, + "127": 2, + "128": 2, + "129": 7, + "130": 3, + "131": 3, + "132": 0, + "133": 1, + "134": 2, + "135": 2, + "136": 1, + "137": 1, + "138": 2, + "139": -1, + "140": -1, + "141": 1, + "142": 1, + "143": 1 + }, + "c2s": { + "0": 3, + "1": 2, + "2": 3, + "3": 3, + "4": 1, + "5": 2, + "6": 1, + "7": 1, + "8": 1, + "9": 1, + "10": 1, + "11": 7, + "12": 3, + "13": 2, + "14": 1, + "15": 1, + "16": 0, + "17": 1, + "18": 1, + "19": 3, + "20": 3, + "21": 1, + "22": 3, + "23": 1, + "24": 3, + "25": 3, + "26": 2, + "27": 7, + "28": -1, + "29": 1, + "30": 2, + "31": 2, + "32": 2, + "33": 2, + "34": 2, + "35": 2, + "36": 1, + "37": 1, + "38": -1, + "39": 3, + "40": 3, + "41": 1, + "42": 1, + "43": 1, + "44": 7, + "45": -1, + "46": 2, + "47": 3, + "48": 3, + "49": 3, + "50": -1, + "51": -1, + "52": 3, + "53": 3, + "54": 3, + "55": 3, + "56": 3, + "57": 3, + "58": 1, + "59": 3, + "60": 3, + "61": 3, + "62": 7, + "63": 3, + "64": 3, + "65": 3, + "66": 2, + "67": 2, + "68": 1 + } + }, "1073741953": { "s2c": { "0": -2147483648, @@ -3787,6 +4438,225 @@ "51": 1, "52": 2 } + }, + "1073742169": { + "s2c": { + "0": -2147483648, + "1": 2, + "2": 2, + "3": -1, + "4": 7, + "5": 7, + "6": 7, + "7": 7, + "8": 7, + "9": 1, + "10": 1, + "11": 7, + "12": 7, + "13": 7, + "14": 1, + "15": 1, + "16": 1, + "17": 3, + "18": 3, + "19": 3, + "20": 3, + "21": 1, + "22": 1, + "23": 1, + "24": 3, + "25": 2, + "26": 7, + "27": 7, + "28": 2, + "29": 2, + "30": 1, + "31": 1, + "32": -1, + "33": 1, + "34": 2, + "35": 2, + "36": 2, + "37": 7, + "38": 7, + "39": 2, + "40": 1, + "41": 7, + "42": 3, + "43": 2, + "44": 3, + "45": -1, + "46": 7, + "47": 7, + "48": 4, + "49": 7, + "50": 2, + "51": 1, + "52": 4, + "53": 3, + "54": 2, + "55": 2, + "56": 2, + "57": 2, + "58": 2, + "59": 3, + "60": 3, + "61": 7, + "62": -1, + "63": -1, + "64": 3, + "65": 2, + "66": 1, + "67": 2, + "68": 2, + "69": 1, + "70": 1, + "71": 1, + "72": 1, + "73": 1, + "74": 2, + "75": 3, + "76": 3, + "77": 3, + "78": 2, + "79": 2, + "80": 1, + "81": -1, + "82": -1, + "83": 4, + "84": 3, + "85": 2, + "86": 7, + "87": -1, + "88": 1, + "89": 1, + "90": 3, + "91": 3, + "92": 3, + "93": 3, + "94": 3, + "95": 2, + "96": 7, + "97": 7, + "98": 3, + "99": 2, + "100": 1, + "101": 2, + "102": 2, + "103": 2, + "104": 2, + "105": 1, + "106": 1, + "107": 3, + "108": 1, + "109": 2, + "110": 3, + "111": 1, + "112": 1, + "113": 7, + "114": 1, + "115": 1, + "116": 1, + "117": 1, + "118": 4, + "119": 4, + "120": 0, + "121": 4, + "122": 1, + "123": 2, + "124": 1, + "125": 1, + "126": 3, + "127": 2, + "128": 2, + "129": 7, + "130": 3, + "131": 3, + "132": 0, + "133": 1, + "134": 2, + "135": 2, + "136": 1, + "137": 1, + "138": 2, + "139": -1, + "140": -1, + "141": 1, + "142": 1, + "143": 1 + }, + "c2s": { + "0": 3, + "1": 2, + "2": 3, + "3": 3, + "4": 1, + "5": 2, + "6": 1, + "7": 1, + "8": 1, + "9": 1, + "10": 1, + "11": 7, + "12": 3, + "13": 2, + "14": 1, + "15": 1, + "16": 0, + "17": 1, + "18": 1, + "19": 3, + "20": 3, + "21": 1, + "22": 3, + "23": 1, + "24": 3, + "25": 3, + "26": 2, + "27": 7, + "28": -1, + "29": 1, + "30": 2, + "31": 2, + "32": 2, + "33": 2, + "34": 2, + "35": 2, + "36": 1, + "37": 1, + "38": -1, + "39": 3, + "40": 3, + "41": 1, + "42": 1, + "43": 1, + "44": 7, + "45": -1, + "46": 2, + "47": 3, + "48": 3, + "49": 3, + "50": -1, + "51": -1, + "52": 3, + "53": 3, + "54": 3, + "55": 3, + "56": 3, + "57": 3, + "58": 1, + "59": 3, + "60": 3, + "61": 3, + "62": 7, + "63": 3, + "64": 3, + "65": 3, + "66": 2, + "67": 2, + "68": 1 + } } } } \ No newline at end of file diff --git a/fabric-26/build.gradle b/fabric-26/build.gradle new file mode 100644 index 00000000..302a1e6c --- /dev/null +++ b/fabric-26/build.gradle @@ -0,0 +1,94 @@ +apply plugin: 'net.fabricmc.fabric-loom' +apply plugin: 'com.gradleup.shadow' +apply plugin: 'com.modrinth.minotaur' +apply plugin: 'io.github.themrmilchmann.curseforge-publish' + +base { + archivesName = project.archives_base_name + '-fabric-26' +} + +group = project.maven_group + ".fabric" + +configurations { + shadowInclude +} + +dependencies { + minecraft "com.mojang:minecraft:${project.modern_minecraft_version}" + implementation "net.fabricmc:fabric-loader:${project.loader_version}" + shadowInclude implementation(project(':common')) +} + +processResources { + inputs.property 'version', project.version + inputs.property 'minecraft', '>=26.1 <=26.3' + filesMatching('fabric.mod.json') { + expand version: project.version, minecraft: inputs.properties.minecraft, + loader: '>=0.19.5', java: '>=25' + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_25 + targetCompatibility = JavaVersion.VERSION_25 + withSourcesJar() +} + +loom { + accessWidenerPath = file('src/main/resources/raknetify-fabric.accesswidener') +} + +shadowJar { + dependencies { + exclude(dependency('it.unimi.dsi:fastutil')) + } + exclude 'META-INF/LICENSE.txt', 'META-INF/NOTICE.txt' + minimize() + archiveClassifier = 'all' + configurations = [project.configurations.shadowInclude] + from '../LICENSE' +} + +assemble.dependsOn(shadowJar) + +jar { + from "../LICENSE" +} + +modrinth { + versionNumber = project.version + versionName = project.version + " devbuild" + uploadFile = shadowJar + loaders = ["fabric", "quilt"] +} + +curseforge { + apiToken = System.getenv("CURSEFORGE_TOKEN") + publications { + register("main") { + projectId = "631457" + + gameVersion("minecraft-26-1", "26-1") + gameVersion("minecraft-26-1", "26-1-1") + gameVersion("minecraft-26-1", "26-1-2") + gameVersion("minecraft-26-2", "26-2") + gameVersion("minecraft-26-3", "26-3") + gameVersion("java", "java-25") + gameVersion("modloader", "fabric") + gameVersion("environment", "server") + gameVersion("environment", "client") + + artifacts.register("main") { + from(shadowJar) + + displayName = project.version + " devbuild" + releaseType = rootProject.curseforgeAlpha + + changelog { + format = rootProject.curseforgeMarkdown + from(com.ishland.buildscript.ParseGItHubActionChangelog.getChangelog()) + } + } + } + } +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/RaknetifyFabric.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/RaknetifyFabric.java new file mode 100644 index 00000000..9d943afe --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/RaknetifyFabric.java @@ -0,0 +1,274 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.ishland.raknetify.common.data.ProtocolMultiChannelMappings; +import com.ishland.raknetify.fabric.common.client.DebugHudUtil1_21_9; +import com.ishland.raknetify.fabric.common.util.MinecraftVersions; +import com.ishland.raknetify.fabric.common.connection.RakNetMultiChannel; +import com.ishland.raknetify.common.util.NetworkInterfaceListener; +import com.ishland.raknetify.fabric.common.util.FieldSignatureParser; +import com.ishland.raknetify.fabric.mixin.access.IPacketCodecDispatcher; +import io.netty.buffer.ByteBuf; +import io.netty.util.ResourceLeakDetector; +import io.netty.util.internal.SystemPropertyUtil; +import it.unimi.dsi.fastutil.ints.AbstractInt2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2IntArrayMap; +import it.unimi.dsi.fastutil.ints.Int2IntMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.fabricmc.api.EnvType; +import net.fabricmc.api.ModInitializer; +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.entrypoint.PreLaunchEntrypoint; +import net.minecraft.SharedConstants; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.PacketType; +import net.minecraft.network.protocol.common.CommonPacketTypes; +import net.minecraft.network.protocol.cookie.CookiePacketTypes; +import net.minecraft.network.protocol.game.GamePacketTypes; +import net.minecraft.network.protocol.game.GameProtocols; +import net.minecraft.network.protocol.game.ServerGamePacketListener; +import net.minecraft.network.protocol.ping.PingPacketTypes; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.FieldNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.spongepowered.asm.mixin.MixinEnvironment; +import org.spongepowered.asm.service.MixinService; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class RaknetifyFabric implements ModInitializer, PreLaunchEntrypoint { + + private static final boolean SAVE_CHANNEL_MAPPINGS = Boolean.getBoolean("raknetify.saveChannelMappings"); + private static final boolean EXIT_AFTER_SAVE_CHANNEL_MAPPINGS = Boolean.getBoolean("raknetify.saveChannelMappings.exit"); + + @Override + public void onPreLaunch() { + } + + @Override + public void onInitialize() { + + if (FabricLoader.getInstance().isDevelopmentEnvironment()) + auditMixins(); + + handleMappings(); + + if (FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT && !MinecraftVersions.IS_26_4_SNAPSHOT) { + DebugHudUtil1_21_9.init(); + } + + // If new property name is present, use it + String levelStr = SystemPropertyUtil.get("io.netty.leakDetection.level", ResourceLeakDetector.Level.SIMPLE.name()); + ResourceLeakDetector.Level level = ResourceLeakDetector.Level.SIMPLE; + try { + level = ResourceLeakDetector.Level.valueOf(levelStr.trim().toUpperCase(Locale.ENGLISH)); + } catch (IllegalArgumentException ignored) { + } + System.out.println("Raknetify: Using leak detector level %s".formatted(level)); + ResourceLeakDetector.setLevel(level); + } + + private static void handleMappings() { + RakNetMultiChannel.init(); + NetworkInterfaceListener.init(); + + Int2IntArrayMap s2c = new Int2IntArrayMap(); + Int2IntArrayMap c2s = new Int2IntArrayMap(); + // Inspect registration order only: these codecs must never encode or decode with the null registry context. + final var c2sPacketTypes = ((IPacketCodecDispatcher, PacketType>>) + GameProtocols.SERVERBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(null), null).codec()).getById() + .stream().map(byteBufPacketPacketTypePacketType -> byteBufPacketPacketTypePacketType.type()).toList(); + final var s2cPacketTypes = ((IPacketCodecDispatcher, PacketType>>) + GameProtocols.CLIENTBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(null)).codec()).getById() + .stream().map(byteBufPacketPacketTypePacketType -> byteBufPacketPacketTypePacketType.type()).toList(); + + record FieldNodeClazzPair(FieldNode fieldNode, Class clazz) { + } + + // Generic field signatures link the codec's PacketType entries to the class-based channel rules. + Map, Class> vanillaPacketTypes = new Object2ObjectOpenHashMap<>(); + for (FieldNodeClazzPair fieldNodeClazzPair : Stream.of(GamePacketTypes.class, CommonPacketTypes.class, CookiePacketTypes.class, PingPacketTypes.class) + .flatMap(clazz -> { + try { + return MixinService.getService().getBytecodeProvider().getClassNode(Type.getInternalName(clazz)).fields.stream() + .map(fieldNode -> new FieldNodeClazzPair(fieldNode, clazz)); + } catch (ClassNotFoundException | IOException e) { + throw new RuntimeException(e); + } + }) + .toList()) { + final FieldNode field = fieldNodeClazzPair.fieldNode(); + final List parameterGenerics = FieldSignatureParser.parse(field.signature); + for (Type type : parameterGenerics) { + try { + final Class clazz = Class.forName(type.getClassName()); + if (Packet.class.isAssignableFrom(clazz)) { + final PacketType actualType; + try { + actualType = (PacketType) fieldNodeClazzPair.clazz().getField(field.name).get(null); + } catch (IllegalAccessException | NoSuchFieldException e) { + throw new RuntimeException(e); + } + vanillaPacketTypes.put(actualType, clazz); + } + } catch (ClassNotFoundException ignored) { + } + } + } + + for (int i = 0, s2cPacketTypesSize = s2cPacketTypes.size(); i < s2cPacketTypesSize; i++) { + PacketType> s2cPacketType = s2cPacketTypes.get(i); + final Class clazz = vanillaPacketTypes.get(s2cPacketType); + if (clazz == null) { + System.out.println("Skipping unmapped packet type: " + s2cPacketType); + continue; + } + s2c.put(i, RakNetMultiChannel.getPacketChannelOverride(clazz, false)); + // System.out.println("Mapped packet type: " + s2cPacketType + " to channel " + RakNetMultiChannel.getPacketChannelOverride(clazz)); + } + + for (int i = 0, c2sPacketTypesSize = c2sPacketTypes.size(); i < c2sPacketTypesSize; i++) { + PacketType> c2sPacketType = c2sPacketTypes.get(i); + final Class clazz = vanillaPacketTypes.get(c2sPacketType); + if (clazz == null) { + System.out.println("Skipping unmapped packet type: " + c2sPacketType); + continue; + } + c2s.put(i, RakNetMultiChannel.getPacketChannelOverride(clazz, false)); + // System.out.println("Mapped packet type: " + c2sPacketType + " to channel " + RakNetMultiChannel.getPacketChannelOverride(clazz)); + } + + if (SAVE_CHANNEL_MAPPINGS) { + auditMixins(); + final Gson gson = new GsonBuilder() + .setPrettyPrinting() + .create(); + ProtocolMultiChannelMappings mappings = new ProtocolMultiChannelMappings(); + // Preserve proxy mappings for versions no longer supported by Fabric. + for (int protocol : new int[]{755, 756}) { + final ProtocolMultiChannelMappings.VersionMapping retained = ProtocolMultiChannelMappings.INSTANCE.mappings.get(protocol); + if (retained != null) mappings.mappings.put(protocol, retained); + } + Path path = Path.of("channelMappings.json"); + try { + mappings = gson.fromJson(Files.readString(path), ProtocolMultiChannelMappings.class); + } catch (IOException e) { + System.out.println("Error reading previously generated mappings: " + e.toString()); + } + final ProtocolMultiChannelMappings.VersionMapping versionMapping = new ProtocolMultiChannelMappings.VersionMapping(); + versionMapping.c2s = c2s; + versionMapping.s2c = s2c; + mappings.mappings.put(SharedConstants.getProtocolVersion(), versionMapping); + + // reproducible mappings + mappings.mappings = mappings.mappings.int2ObjectEntrySet() + .stream() + .map(entry -> { + final ProtocolMultiChannelMappings.VersionMapping value = entry.getValue(); + value.s2c = value.s2c.int2IntEntrySet().stream() + .sorted(Comparator.comparingInt(Int2IntMap.Entry::getIntKey)) + .collect(Collectors.toMap(Int2IntMap.Entry::getIntKey, Int2IntMap.Entry::getIntValue, (o, o2) -> { + throw new RuntimeException("Unresolvable conflicts"); + }, Int2IntArrayMap::new)); + value.c2s = value.c2s.int2IntEntrySet().stream() + .sorted(Comparator.comparingInt(Int2IntMap.Entry::getIntKey)) + .collect(Collectors.toMap(Int2IntMap.Entry::getIntKey, Int2IntMap.Entry::getIntValue, (o, o2) -> { + throw new RuntimeException("Unresolvable conflicts"); + }, Int2IntArrayMap::new)); + return new AbstractInt2ObjectMap.BasicEntry<>( + entry.getIntKey(), + value + ); + }) + .sorted(Comparator.comparingInt(Int2ObjectMap.Entry::getIntKey)) + .collect(Collectors.toMap(Int2ObjectMap.Entry::getIntKey, Int2ObjectMap.Entry::getValue, (o, o2) -> { + throw new RuntimeException("Unresolvable conflicts"); + }, Int2ObjectArrayMap::new)); + + try { + Files.writeString(path, gson.toJson(mappings), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } catch (IOException e) { + System.out.println("Error writing generated mappings: " + e.toString()); + } + if (EXIT_AFTER_SAVE_CHANNEL_MAPPINGS) System.exit(0); + } + } + + private static void auditMixins() { + Logger auditLogger = LoggerFactory.getLogger("Raknetify Mixin Audit"); + try { + final Class transformerClazz = Class.forName("org.spongepowered.asm.mixin.transformer.MixinTransformer"); + if (transformerClazz.isInstance(MixinEnvironment.getCurrentEnvironment().getActiveTransformer())) { + final Field processorField = transformerClazz.getDeclaredField("processor"); + processorField.setAccessible(true); + final Object processor = processorField.get(MixinEnvironment.getCurrentEnvironment().getActiveTransformer()); + final Class processorClazz = Class.forName("org.spongepowered.asm.mixin.transformer.MixinProcessor"); + final Field configsField = processorClazz.getDeclaredField("configs"); + configsField.setAccessible(true); + final List configs = (List) configsField.get(processor); + final Class configClazz = Class.forName("org.spongepowered.asm.mixin.transformer.MixinConfig"); + final Method getUnhandledTargetsMethod = configClazz.getDeclaredMethod("getUnhandledTargets"); + getUnhandledTargetsMethod.setAccessible(true); + Set unhandled = new HashSet<>(); + for (Object config : configs) { + final Set unhandledTargets = (Set) getUnhandledTargetsMethod.invoke(config); + unhandled.addAll(unhandledTargets); + } + for (String s : unhandled) { + auditLogger.info("Loading class {}", s); + MixinService.getService().getClassProvider().findClass(s, false); + } + for (Object config : configs) { + for (String unhandledTarget : (Set) getUnhandledTargetsMethod.invoke(config)) { + auditLogger.error("{} is already classloaded", unhandledTarget); + } + } + } + } catch (Throwable t) { + throw new RuntimeException("Failed to audit mixins", t); + } + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/client/DebugHudLines26_4.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/client/DebugHudLines26_4.java new file mode 100644 index 00000000..252c81f4 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/client/DebugHudLines26_4.java @@ -0,0 +1,57 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.common.client; + +import net.minecraft.client.gui.components.debug.DebugScreenDisplayer; +import java.lang.reflect.Method; + +// Load this adapter only on the snapshot, where DebugGroup replaces the identifier-based grouping API. +final class DebugHudLines26_4 { + private static final Object GROUP; + private static final Method ADD_LINE; + + static { + try { + // Cache the reflected API once because debug lines are emitted on every HUD update. + Class builderClass = Class.forName("net.minecraft.client.gui.components.debug.DebugGroup$Builder"); + Object builder = builderClass.getMethod("titled", String.class).invoke(null, "Raknetify"); + GROUP = builderClass.getMethod("build").invoke(builder); + ADD_LINE = DebugScreenDisplayer.class.getMethod("addToGroup", GROUP.getClass(), String.class); + } catch (ReflectiveOperationException exception) { + throw new ExceptionInInitializerError(exception); + } + } + + private DebugHudLines26_4() { + } + + static void addLine(DebugScreenDisplayer lines, String text) { + try { + ADD_LINE.invoke(lines, GROUP, text); + } catch (ReflectiveOperationException exception) { + throw new IllegalStateException("Cannot display Raknetify debug information", exception); + } + } +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/client/DebugHudUtil.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/client/DebugHudUtil.java new file mode 100644 index 00000000..a34d3fb5 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/client/DebugHudUtil.java @@ -0,0 +1,116 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.common.client; + +import com.ishland.raknetify.common.connection.MetricsSynchronizationHandler; +import com.ishland.raknetify.common.connection.MultiChannelingStreamingCompression; +import com.ishland.raknetify.common.connection.SimpleMetricsLogger; +import com.ishland.raknetify.fabric.mixin.access.IClientConnection; +import io.netty.channel.Channel; +import network.ycc.raknet.RakNet; + +import java.util.function.Consumer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.network.Connection; + +public class DebugHudUtil { + + public static void getDebugString(Consumer consumer) { + final ClientPacketListener networkHandler = Minecraft.getInstance().getConnection(); + if (networkHandler != null) { + final Connection connection = networkHandler.getConnection(); + final Channel channel = ((IClientConnection) connection).getChannel(); + if (channel != null) { + if (channel.config() instanceof RakNet.Config config) { + if (config.getMetrics() instanceof SimpleMetricsLogger logger) { + consumer.accept( + "[Raknetify] A: true, MTU: %d, RTT: %.2f/%.2fms" + .formatted(config.getMTU(), + logger.getMeasureRTTns() / 1_000_000.0, + logger.getMeasureRTTnsStdDev() / 1_000_000.0 + )); + final MetricsSynchronizationHandler serverSync = logger.getMetricsSynchronizationHandler(); + if (serverSync != null && serverSync.isRemoteSupported()) { + consumer.accept( + "[Raknetify] C: BUF: %.2fMB; S: BUF: %.2fMB" + .formatted( + logger.getCurrentQueuedBytes() / 1024.0 / 1024.0, + serverSync.getQueuedBytes() / 1024.0 / 1024.0 + )); + } else { + consumer.accept( + "[Raknetify] C: BUF: %.2fMB" + .formatted( + logger.getCurrentQueuedBytes() / 1024.0 / 1024.0 + )); + } + + consumer.accept( + "[Raknetify] C: I: %s, O: %s" + .formatted( + logger.getMeasureTrafficInFormatted(), + logger.getMeasureTrafficOutFormatted() + )); + + consumer.accept( + "[Raknetify] C: ERR: %.4f%%, %d tx, %d rx, BST: %d" + .formatted( + logger.getMeasureErrorRate() * 100.0, + logger.getMeasureTX(), logger.getMeasureRX(), + logger.getMeasureBurstTokens() + config.getDefaultPendingFrameSets() + )); + if (serverSync != null && serverSync.isRemoteSupported()) { + consumer.accept( + "[Raknetify] S: ERR: %.4f%%, %d tx, %d rx, BST: %d" + .formatted( + serverSync.getErrorRate() * 100.0, + serverSync.getTX(), serverSync.getRX(), + serverSync.getBurst() + )); + } + } else { + consumer.accept( + "[Raknetify] A: true, MTU: %d" + .formatted(config.getMTU()) + ); + } + + final MultiChannelingStreamingCompression compression = channel.pipeline().get(MultiChannelingStreamingCompression.class); + if (compression != null && compression.isActive()) { + consumer.accept( + "[Raknetify] CRatio: I: %.2f%%, O: %.2f%%" + .formatted(compression.getInCompressionRatio() * 100, compression.getOutCompressionRatio() * 100) + ); + } + + return; + } + } + } + consumer.accept("[Raknetify] A: false"); + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/client/DebugHudUtil1_21_9.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/client/DebugHudUtil1_21_9.java new file mode 100644 index 00000000..0135f406 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/client/DebugHudUtil1_21_9.java @@ -0,0 +1,68 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.common.client; + +import com.ishland.raknetify.fabric.mixin.access.IDebugHudEntries; +import com.ishland.raknetify.fabric.common.util.MinecraftVersions; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.atomic.AtomicBoolean; +import net.minecraft.client.gui.components.debug.DebugScreenDisplayer; +import net.minecraft.client.gui.components.debug.DebugScreenEntry; +import net.minecraft.resources.Identifier; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.LevelChunk; + +public class DebugHudUtil1_21_9 { + + private static final AtomicBoolean initialized = new AtomicBoolean(false); + + public static final Identifier ID = Identifier.fromNamespaceAndPath("raknetify", "raknetify_debug_info"); + + public static void init() { + if (!initialized.compareAndSet(false, true)) return; + + IDebugHudEntries.invokeRegister(ID, createEntry()); + } + + public static DebugScreenEntry createEntry() { + return new DebugScreenEntry() { + @Override + public void display(DebugScreenDisplayer lines, @Nullable Level world, @Nullable LevelChunk clientChunk, @Nullable LevelChunk chunk) { + if (MinecraftVersions.IS_26_4_SNAPSHOT) { + DebugHudUtil.getDebugString(s -> DebugHudLines26_4.addLine(lines, s)); + } else { + DebugHudUtil.getDebugString(s -> lines.addToGroup(ID, s)); + } + } + + @Override + public boolean isAllowed(boolean reducedDebugInfo) { + return true; + } + }; + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/client/ServerAddressUtil.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/client/ServerAddressUtil.java new file mode 100644 index 00000000..886506c3 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/client/ServerAddressUtil.java @@ -0,0 +1,45 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.common.client; + +import com.ishland.raknetify.fabric.common.util.MinecraftVersions; +import net.minecraft.client.multiplayer.resolver.ServerAddress; + +public final class ServerAddressUtil { + + private ServerAddressUtil() { + } + + public static ServerAddress withAddress(ServerAddress original, String host, int port) { + if (!MinecraftVersions.IS_26_4_SNAPSHOT) return new ServerAddress(host, port); + try { + // Resolve the snapshot copy method at runtime to preserve query properties while compiling against 26.3. + return (ServerAddress) ServerAddress.class.getMethod("withHostAndPort", String.class, int.class) + .invoke(original, host, port); + } catch (ReflectiveOperationException exception) { + throw new IllegalStateException("Cannot preserve server address properties", exception); + } + } +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/compat/package-info.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/compat/package-info.java new file mode 100644 index 00000000..b6c8339c --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/compat/package-info.java @@ -0,0 +1,25 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.common.compat; diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/compat/viafabric/ViaFabricCompatInjector.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/compat/viafabric/ViaFabricCompatInjector.java new file mode 100644 index 00000000..aed6a324 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/compat/viafabric/ViaFabricCompatInjector.java @@ -0,0 +1,92 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.common.compat.viafabric; + +import com.google.common.base.Preconditions; +import io.netty.buffer.ByteBuf; +import io.netty.channel.Channel; +import io.netty.channel.ChannelPipeline; +import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.handler.codec.MessageToMessageEncoder; +import net.fabricmc.loader.api.FabricLoader; + +import java.lang.reflect.Constructor; + +public class ViaFabricCompatInjector { + + public static void inject(Channel channel, boolean isClientSide) { + Preconditions.checkNotNull(channel); + if (FabricLoader.getInstance().isModLoaded("viafabric")) { + try { + final Class userConnectionImplClass = Class.forName("com.viaversion.viaversion.connection.UserConnectionImpl"); + final Constructor userConnectionImplConstructor = userConnectionImplClass.getConstructor(Channel.class, boolean.class); + final Object user = userConnectionImplConstructor.newInstance(channel, isClientSide); + + final Class userConnectionClass = Class.forName("com.viaversion.viaversion.api.connection.UserConnection"); + + final Class protocolPipelineImplClass = Class.forName("com.viaversion.viaversion.protocol.ProtocolPipelineImpl"); + Object protocolPipeline = protocolPipelineImplClass.getConstructor(userConnectionClass).newInstance(user); + + Class protocolPipelineClass = Class.forName("com.viaversion.viaversion.api.protocol.ProtocolPipeline"); + Class protocolClass = Class.forName("com.viaversion.viaversion.api.protocol.Protocol"); + if ((boolean) userConnectionClass.getMethod("isClientSide").invoke(user)) { + Class hostnameParserProtocolClass = Class.forName("com.viaversion.fabric.common.protocol.HostnameParserProtocol"); + Object hostnameParserProtocolInstance = hostnameParserProtocolClass.getField("INSTANCE").get(null); + protocolPipelineClass.getMethod("add", protocolClass).invoke(protocolPipeline, hostnameParserProtocolInstance); + } + + final Class commonTransformerClass = Class.forName("com.viaversion.fabric.common.handler.CommonTransformer"); + final String handlerEncoderName = (String) commonTransformerClass.getField("HANDLER_ENCODER_NAME").get(null); + final String handlerDecoderName = (String) commonTransformerClass.getField("HANDLER_DECODER_NAME").get(null); + + @SuppressWarnings("unchecked") final Class> fabricEncodeHandlerClass = (Class>) Class.forName("com.viaversion.fabric.common.handler.FabricEncodeHandler"); + final MessageToMessageEncoder fabricEncodeHandler = fabricEncodeHandlerClass.getConstructor(userConnectionClass).newInstance(user); + + @SuppressWarnings("unchecked") final Class> fabricDecodeHandlerClass = (Class>) Class.forName("com.viaversion.fabric.common.handler.FabricDecodeHandler"); + final MessageToMessageDecoder fabricDecodeHandler = fabricDecodeHandlerClass.getConstructor(userConnectionClass).newInstance(user); + + channel.pipeline().addBefore(determineEncoderName(channel.pipeline()), handlerEncoderName, fabricEncodeHandler); + channel.pipeline().addBefore(determineDecoderName(channel.pipeline()), handlerDecoderName, fabricDecodeHandler); + } catch (Throwable t) { + //noinspection RedundantStringFormatCall + System.err.println(String.format("Raknetify: Could not inject ViaVersion compatibility into RakNet channel %s: %s", channel, t)); + t.printStackTrace(); + } + } + } + + private static String determineEncoderName(ChannelPipeline pipeline) { + if (pipeline.get("encoder") != null) return "encoder"; + if (pipeline.get("outbound_config") != null) return "outbound_config"; + throw new UnsupportedOperationException(); + } + + private static String determineDecoderName(ChannelPipeline pipeline) { + if (pipeline.get("decoder") != null) return "decoder"; + if (pipeline.get("inbound_config") != null) return "inbound_config"; + throw new UnsupportedOperationException(); + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/MultiChannellingPacketCapture.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/MultiChannellingPacketCapture.java new file mode 100644 index 00000000..9013d4bb --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/MultiChannellingPacketCapture.java @@ -0,0 +1,77 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.common.connection; + +import com.ishland.raknetify.common.connection.RakNetSimpleMultiChannelCodec; +import com.ishland.raknetify.common.connection.multichannel.CustomPayloadChannel; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket; +import net.minecraft.network.protocol.common.ServerboundCustomPayloadPacket; + +@ChannelHandler.Sharable +public class MultiChannellingPacketCapture extends ChannelOutboundHandlerAdapter { + + private Class packetClass = null; + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + this.packetClass = msg.getClass(); + try { + ctx.write(msg, promise); + } finally { + this.packetClass = null; + } + } + + public Class getPacketClass() { + return this.packetClass; + } + + public void setPacketClass(Class packetClass) { + this.packetClass = packetClass; + } + + public RakNetSimpleMultiChannelCodec.OverrideHandler getCaptureBasedHandler() { + return new CaptureBasedHandler(); + } + + public RakNetSimpleMultiChannelCodec.OverrideHandler getCustomPayloadHandler() { + return new CustomPayloadChannel.OverrideHandler(value -> packetClass == ClientboundCustomPayloadPacket.class || packetClass == ServerboundCustomPayloadPacket.class); + } + + private class CaptureBasedHandler implements RakNetSimpleMultiChannelCodec.OverrideHandler { + + @Override + public int getChannelOverride(ByteBuf buf, boolean suppressWarning) { + return RakNetMultiChannel.getPacketChannelOverride(packetClass, suppressWarning); + } + + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetClientConnectionUtil.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetClientConnectionUtil.java new file mode 100644 index 00000000..7886f045 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetClientConnectionUtil.java @@ -0,0 +1,61 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.common.connection; + +import com.ishland.raknetify.common.util.ThreadLocalUtil; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import io.netty.channel.ChannelFuture; +import java.net.InetSocketAddress; +import net.minecraft.network.Connection; +import net.minecraft.server.network.EventLoopGroupHolder; + +public class RakNetClientConnectionUtil { + + private RakNetClientConnectionUtil() { + } + + public static Connection connect(InetSocketAddress address, EventLoopGroupHolder backend, boolean largeMTU, Operation original) { + try { + ThreadLocalUtil.setInitializingRaknet(true); + ThreadLocalUtil.setInitializingRaknetLargeMTU(largeMTU); + return original.call(address, backend, null); + } finally { + ThreadLocalUtil.setInitializingRaknet(false); + ThreadLocalUtil.setInitializingRaknetLargeMTU(false); + } + } + + public static ChannelFuture connect(InetSocketAddress address, EventLoopGroupHolder backend, boolean largeMTU, Operation original, Connection connection) { + try { + ThreadLocalUtil.setInitializingRaknet(true); + ThreadLocalUtil.setInitializingRaknetLargeMTU(largeMTU); + return original.call(address, backend, connection); + } finally { + ThreadLocalUtil.setInitializingRaknet(false); + ThreadLocalUtil.setInitializingRaknetLargeMTU(false); + } + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetCompressionCompatibilityHandler.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetCompressionCompatibilityHandler.java new file mode 100644 index 00000000..2366beee --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetCompressionCompatibilityHandler.java @@ -0,0 +1,49 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.common.connection; + +import com.ishland.raknetify.fabric.common.util.MinecraftVersions; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import net.minecraft.network.protocol.login.ClientboundLoginCompressionPacket; + +public class RakNetCompressionCompatibilityHandler extends ChannelDuplexHandler { + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + // The snapshot renamed this packet, so evaluate the old class reference only on earlier versions. + boolean setupCompression = MinecraftVersions.IS_26_4_SNAPSHOT + ? msg.getClass().getName().equals("net.minecraft.network.protocol.login.ClientboundSetupCompressionPacket") + : msg instanceof ClientboundLoginCompressionPacket; + if (setupCompression) { + promise.trySuccess(); + ctx.write(msg); + ctx.pipeline().remove(this); + return; + } + super.write(ctx, msg, promise); + } +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetFabricChannelEventListener.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetFabricChannelEventListener.java new file mode 100644 index 00000000..d33226d6 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetFabricChannelEventListener.java @@ -0,0 +1,60 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.common.connection; + +import com.ishland.raknetify.common.connection.RakNetSimpleMultiChannelCodec; +import com.ishland.raknetify.common.connection.SynchronizationLayer; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import net.minecraft.network.UnconfiguredPipelineHandler; +import net.minecraft.network.protocol.game.ClientboundCommandsPacket; +import net.minecraft.network.protocol.game.ClientboundLoginPacket; +import net.minecraft.network.protocol.game.ClientboundRespawnPacket; +import net.minecraft.network.protocol.game.ClientboundStartConfigurationPacket; +import net.minecraft.network.protocol.game.ServerboundConfigurationAcknowledgedPacket; + +public class RakNetFabricChannelEventListener extends ChannelDuplexHandler { + + public static final String NAME = "raknetify-fabric-event-listener"; + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (msg instanceof ClientboundRespawnPacket || msg instanceof ClientboundLoginPacket) { + ctx.write(SynchronizationLayer.SYNC_REQUEST_OBJECT); + } + if (msg instanceof ClientboundStartConfigurationPacket || msg instanceof ServerboundConfigurationAcknowledgedPacket) { + ctx.write(SynchronizationLayer.SYNC_REQUEST_OBJECT); + } + if (msg instanceof ClientboundCommandsPacket) { + ctx.write(RakNetSimpleMultiChannelCodec.SIGNAL_START_MULTICHANNEL); + } + if (msg instanceof UnconfiguredPipelineHandler.InboundConfigurationTask || msg instanceof UnconfiguredPipelineHandler.OutboundConfigurationTask) { + ctx.write(RakNetNetworkTransitionUtil.handleTransition(msg), promise); + return; + } + super.write(ctx, msg, promise); + } +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetFabricConnectionUtil.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetFabricConnectionUtil.java new file mode 100644 index 00000000..2bf63b8b --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetFabricConnectionUtil.java @@ -0,0 +1,101 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.common.connection; + +import com.ishland.raknetify.common.Constants; +import com.ishland.raknetify.common.connection.MultiChannelingStreamingCompression; +import com.ishland.raknetify.common.connection.RakNetConnectionUtil; +import com.ishland.raknetify.common.connection.RakNetSimpleMultiChannelCodec; +import com.ishland.raknetify.fabric.common.compat.viafabric.ViaFabricCompatInjector; +import io.netty.channel.Channel; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.epoll.EpollDatagramChannel; +import io.netty.channel.epoll.EpollSocketChannel; +import io.netty.channel.kqueue.KQueueDatagramChannel; +import io.netty.channel.kqueue.KQueueSocketChannel; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import network.ycc.raknet.RakNet; + +public class RakNetFabricConnectionUtil { + + public static final String NAME_RAKNETIFY_MULTI_CHANNEL_PACKET_CATURE = "raknetify-multi-channel-packet-cature"; + + private RakNetFabricConnectionUtil() { + } + + public static void initChannel(Channel channel) { + if (channel.config() instanceof RakNet.Config) { + RakNetConnectionUtil.initChannel(channel); + channel.pipeline().addAfter(MultiChannelingStreamingCompression.NAME, RakNetSimpleMultiChannelCodec.NAME, new RakNetSimpleMultiChannelCodec(Constants.RAKNET_GAME_PACKET_ID)); + } + } + + public static void postInitChannel(Channel channel, boolean isClientSide) { + if (channel.config() instanceof RakNet.Config) { + ViaFabricCompatInjector.inject(channel, isClientSide); + channel.pipeline().replace("timeout", "timeout", new ChannelDuplexHandler()); // no-op + channel.pipeline().replace("splitter", "splitter", new ChannelDuplexHandler()); // no-op + channel.pipeline().replace("prepender", "prepender", new ChannelDuplexHandler()); // no-op + final MultiChannellingPacketCapture handler = new MultiChannellingPacketCapture(); + channel.pipeline().addLast(NAME_RAKNETIFY_MULTI_CHANNEL_PACKET_CATURE, handler); + onPipelineReorder(channel.pipeline()); + channel.pipeline().get(RakNetSimpleMultiChannelCodec.class) + .addHandler(handler.getCustomPayloadHandler()) + .addHandler(handler.getCaptureBasedHandler()); + channel.pipeline().addLast("raknetify-handle-compression-compatibility", new RakNetCompressionCompatibilityHandler()); + channel.pipeline().addBefore("packet_handler", RakNetFabricChannelEventListener.NAME, new RakNetFabricChannelEventListener()); + } + } + + static void onPipelineReorder(ChannelPipeline pipeline) { + if (pipeline.get("encoder") == null) { +// System.out.println("Reordering failed: no encoder"); + return; + } +// System.out.println("Reordering"); + ChannelHandler handler = pipeline.remove(RakNetFabricConnectionUtil.NAME_RAKNETIFY_MULTI_CHANNEL_PACKET_CATURE); + if (handler != null) { + pipeline.addAfter("encoder", RakNetFabricConnectionUtil.NAME_RAKNETIFY_MULTI_CHANNEL_PACKET_CATURE, handler); + } + } + + public static DatagramChannel fromSocketChannel(Class clazz) { + if (clazz == NioSocketChannel.class) { + return new NioDatagramChannel(); + } else if (clazz == EpollSocketChannel.class) { + return new EpollDatagramChannel(); + } else if (clazz == KQueueSocketChannel.class) { + return new KQueueDatagramChannel(); + } else { + throw new UnsupportedOperationException(clazz.getName()); + } + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetMultiChannel.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetMultiChannel.java new file mode 100644 index 00000000..41905f79 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetMultiChannel.java @@ -0,0 +1,551 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.common.connection; + +import com.google.common.collect.Sets; +import net.minecraft.network.protocol.BundleDelimiterPacket; +import net.minecraft.network.protocol.common.ClientboundClearDialogPacket; +import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket; +import net.minecraft.network.protocol.common.ClientboundCustomReportDetailsPacket; +import net.minecraft.network.protocol.common.ClientboundDisconnectPacket; +import net.minecraft.network.protocol.common.ClientboundKeepAlivePacket; +import net.minecraft.network.protocol.common.ClientboundPingPacket; +import net.minecraft.network.protocol.common.ClientboundPostEffectsPacket; +import net.minecraft.network.protocol.common.ClientboundResourcePackPopPacket; +import net.minecraft.network.protocol.common.ClientboundResourcePackPushPacket; +import net.minecraft.network.protocol.common.ClientboundServerLinksPacket; +import net.minecraft.network.protocol.common.ClientboundShowDialogPacket; +import net.minecraft.network.protocol.common.ClientboundStoreCookiePacket; +import net.minecraft.network.protocol.common.ClientboundTransferPacket; +import net.minecraft.network.protocol.common.ClientboundUpdateTagsPacket; +import net.minecraft.network.protocol.common.ServerboundClientInformationPacket; +import net.minecraft.network.protocol.common.ServerboundCustomClickActionPacket; +import net.minecraft.network.protocol.common.ServerboundCustomPayloadPacket; +import net.minecraft.network.protocol.common.ServerboundKeepAlivePacket; +import net.minecraft.network.protocol.common.ServerboundPongPacket; +import net.minecraft.network.protocol.common.ServerboundResourcePackPacket; +import net.minecraft.network.protocol.configuration.ClientboundUpdateEnabledFeaturesPacket; +import net.minecraft.network.protocol.cookie.ClientboundCookieRequestPacket; +import net.minecraft.network.protocol.cookie.ServerboundCookieResponsePacket; +import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; +import net.minecraft.network.protocol.game.ClientboundAddTransientBlockPacket; +import net.minecraft.network.protocol.game.ClientboundAnimatePacket; +import net.minecraft.network.protocol.game.ClientboundAwardStatsPacket; +import net.minecraft.network.protocol.game.ClientboundBlockChangedAckPacket; +import net.minecraft.network.protocol.game.ClientboundBlockDestructionPacket; +import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket; +import net.minecraft.network.protocol.game.ClientboundBlockEventPacket; +import net.minecraft.network.protocol.game.ClientboundBlockUpdatePacket; +import net.minecraft.network.protocol.game.ClientboundBossEventPacket; +import net.minecraft.network.protocol.game.ClientboundBundleDelimiterPacket; +import net.minecraft.network.protocol.game.ClientboundChangeDifficultyPacket; +import net.minecraft.network.protocol.game.ClientboundChunkBatchFinishedPacket; +import net.minecraft.network.protocol.game.ClientboundChunkBatchStartPacket; +import net.minecraft.network.protocol.game.ClientboundChunksBiomesPacket; +import net.minecraft.network.protocol.game.ClientboundClearTitlesPacket; +import net.minecraft.network.protocol.game.ClientboundCommandSuggestionsPacket; +import net.minecraft.network.protocol.game.ClientboundCommandsPacket; +import net.minecraft.network.protocol.game.ClientboundContainerClosePacket; +import net.minecraft.network.protocol.game.ClientboundContainerSetContentPacket; +import net.minecraft.network.protocol.game.ClientboundContainerSetDataPacket; +import net.minecraft.network.protocol.game.ClientboundContainerSetSlotPacket; +import net.minecraft.network.protocol.game.ClientboundCooldownPacket; +import net.minecraft.network.protocol.game.ClientboundCustomChatCompletionsPacket; +import net.minecraft.network.protocol.game.ClientboundDamageEventPacket; +import net.minecraft.network.protocol.game.ClientboundDebugBlockValuePacket; +import net.minecraft.network.protocol.game.ClientboundDebugChunkValuePacket; +import net.minecraft.network.protocol.game.ClientboundDebugEntityValuePacket; +import net.minecraft.network.protocol.game.ClientboundDebugEventPacket; +import net.minecraft.network.protocol.game.ClientboundDebugSamplePacket; +import net.minecraft.network.protocol.game.ClientboundDeleteChatPacket; +import net.minecraft.network.protocol.game.ClientboundDisguisedChatPacket; +import net.minecraft.network.protocol.game.ClientboundEntityEventPacket; +import net.minecraft.network.protocol.game.ClientboundEntityPositionSyncPacket; +import net.minecraft.network.protocol.game.ClientboundExplodePacket; +import net.minecraft.network.protocol.game.ClientboundForgetLevelChunkPacket; +import net.minecraft.network.protocol.game.ClientboundGameEventPacket; +import net.minecraft.network.protocol.game.ClientboundGameRuleValuesPacket; +import net.minecraft.network.protocol.game.ClientboundGameTestHighlightPosPacket; +import net.minecraft.network.protocol.game.ClientboundHurtAnimationPacket; +import net.minecraft.network.protocol.game.ClientboundInitializeBorderPacket; +import net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket; +import net.minecraft.network.protocol.game.ClientboundLevelEventPacket; +import net.minecraft.network.protocol.game.ClientboundLevelParticlesPacket; +import net.minecraft.network.protocol.game.ClientboundLightUpdatePacket; +import net.minecraft.network.protocol.game.ClientboundLoginPacket; +import net.minecraft.network.protocol.game.ClientboundLowDiskSpaceWarningPacket; +import net.minecraft.network.protocol.game.ClientboundMapItemDataPacket; +import net.minecraft.network.protocol.game.ClientboundMerchantOffersPacket; +import net.minecraft.network.protocol.game.ClientboundMountScreenOpenPacket; +import net.minecraft.network.protocol.game.ClientboundMoveEntityPacket; +import net.minecraft.network.protocol.game.ClientboundMoveMinecartPacket; +import net.minecraft.network.protocol.game.ClientboundMoveVehiclePacket; +import net.minecraft.network.protocol.game.ClientboundOpenBookPacket; +import net.minecraft.network.protocol.game.ClientboundOpenScreenPacket; +import net.minecraft.network.protocol.game.ClientboundOpenSignEditorPacket; +import net.minecraft.network.protocol.game.ClientboundPlaceGhostRecipePacket; +import net.minecraft.network.protocol.game.ClientboundPlayerAbilitiesPacket; +import net.minecraft.network.protocol.game.ClientboundPlayerChatPacket; +import net.minecraft.network.protocol.game.ClientboundPlayerCombatEndPacket; +import net.minecraft.network.protocol.game.ClientboundPlayerCombatEnterPacket; +import net.minecraft.network.protocol.game.ClientboundPlayerCombatKillPacket; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; +import net.minecraft.network.protocol.game.ClientboundPlayerLookAtPacket; +import net.minecraft.network.protocol.game.ClientboundPlayerPositionPacket; +import net.minecraft.network.protocol.game.ClientboundPlayerRotationPacket; +import net.minecraft.network.protocol.game.ClientboundProjectilePowerPacket; +import net.minecraft.network.protocol.game.ClientboundRecipeBookAddPacket; +import net.minecraft.network.protocol.game.ClientboundRecipeBookRemovePacket; +import net.minecraft.network.protocol.game.ClientboundRecipeBookSettingsPacket; +import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket; +import net.minecraft.network.protocol.game.ClientboundRemoveMobEffectPacket; +import net.minecraft.network.protocol.game.ClientboundResetScorePacket; +import net.minecraft.network.protocol.game.ClientboundRespawnPacket; +import net.minecraft.network.protocol.game.ClientboundRotateHeadPacket; +import net.minecraft.network.protocol.game.ClientboundSectionBlocksUpdatePacket; +import net.minecraft.network.protocol.game.ClientboundSelectAdvancementsTabPacket; +import net.minecraft.network.protocol.game.ClientboundServerDataPacket; +import net.minecraft.network.protocol.game.ClientboundSetActionBarTextPacket; +import net.minecraft.network.protocol.game.ClientboundSetBorderCenterPacket; +import net.minecraft.network.protocol.game.ClientboundSetBorderLerpSizePacket; +import net.minecraft.network.protocol.game.ClientboundSetBorderSizePacket; +import net.minecraft.network.protocol.game.ClientboundSetBorderWarningDelayPacket; +import net.minecraft.network.protocol.game.ClientboundSetBorderWarningDistancePacket; +import net.minecraft.network.protocol.game.ClientboundSetCameraPacket; +import net.minecraft.network.protocol.game.ClientboundSetChunkCacheCenterPacket; +import net.minecraft.network.protocol.game.ClientboundSetChunkCacheRadiusPacket; +import net.minecraft.network.protocol.game.ClientboundSetCursorItemPacket; +import net.minecraft.network.protocol.game.ClientboundSetDefaultSpawnPositionPacket; +import net.minecraft.network.protocol.game.ClientboundSetDisplayObjectivePacket; +import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket; +import net.minecraft.network.protocol.game.ClientboundSetEntityLinkPacket; +import net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket; +import net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket; +import net.minecraft.network.protocol.game.ClientboundSetExperiencePacket; +import net.minecraft.network.protocol.game.ClientboundSetHealthPacket; +import net.minecraft.network.protocol.game.ClientboundSetHeldSlotPacket; +import net.minecraft.network.protocol.game.ClientboundSetObjectivePacket; +import net.minecraft.network.protocol.game.ClientboundSetPassengersPacket; +import net.minecraft.network.protocol.game.ClientboundSetPlayerInventoryPacket; +import net.minecraft.network.protocol.game.ClientboundSetPlayerTeamPacket; +import net.minecraft.network.protocol.game.ClientboundSetScorePacket; +import net.minecraft.network.protocol.game.ClientboundSetSimulationDistancePacket; +import net.minecraft.network.protocol.game.ClientboundSetSubtitleTextPacket; +import net.minecraft.network.protocol.game.ClientboundSetTimePacket; +import net.minecraft.network.protocol.game.ClientboundSetTitleTextPacket; +import net.minecraft.network.protocol.game.ClientboundSetTitlesAnimationPacket; +import net.minecraft.network.protocol.game.ClientboundSoundEntityPacket; +import net.minecraft.network.protocol.game.ClientboundSoundPacket; +import net.minecraft.network.protocol.game.ClientboundStartConfigurationPacket; +import net.minecraft.network.protocol.game.ClientboundStopSoundPacket; +import net.minecraft.network.protocol.game.ClientboundSwingAnimationPacket; +import net.minecraft.network.protocol.game.ClientboundSystemChatPacket; +import net.minecraft.network.protocol.game.ClientboundTabListPacket; +import net.minecraft.network.protocol.game.ClientboundTagQueryPacket; +import net.minecraft.network.protocol.game.ClientboundTakeItemEntityPacket; +import net.minecraft.network.protocol.game.ClientboundTeleportEntityPacket; +import net.minecraft.network.protocol.game.ClientboundTestInstanceBlockStatus; +import net.minecraft.network.protocol.game.ClientboundTickingStatePacket; +import net.minecraft.network.protocol.game.ClientboundTickingStepPacket; +import net.minecraft.network.protocol.game.ClientboundTrackedWaypointPacket; +import net.minecraft.network.protocol.game.ClientboundUpdateAdvancementsPacket; +import net.minecraft.network.protocol.game.ClientboundUpdateAttributesPacket; +import net.minecraft.network.protocol.game.ClientboundUpdateMobEffectPacket; +import net.minecraft.network.protocol.game.ClientboundUpdateRecipesPacket; +import net.minecraft.network.protocol.game.ServerboundAcceptTeleportationPacket; +import net.minecraft.network.protocol.game.ServerboundAttackPacket; +import net.minecraft.network.protocol.game.ServerboundBlockEntityTagQueryPacket; +import net.minecraft.network.protocol.game.ServerboundChangeDifficultyPacket; +import net.minecraft.network.protocol.game.ServerboundChangeGameModePacket; +import net.minecraft.network.protocol.game.ServerboundChatAckPacket; +import net.minecraft.network.protocol.game.ServerboundChatCommandPacket; +import net.minecraft.network.protocol.game.ServerboundChatCommandSignedPacket; +import net.minecraft.network.protocol.game.ServerboundChatPacket; +import net.minecraft.network.protocol.game.ServerboundChatSessionUpdatePacket; +import net.minecraft.network.protocol.game.ServerboundChunkBatchReceivedPacket; +import net.minecraft.network.protocol.game.ServerboundClientCommandPacket; +import net.minecraft.network.protocol.game.ServerboundClientTickEndPacket; +import net.minecraft.network.protocol.game.ServerboundCommandSuggestionPacket; +import net.minecraft.network.protocol.game.ServerboundConfigurationAcknowledgedPacket; +import net.minecraft.network.protocol.game.ServerboundContainerButtonClickPacket; +import net.minecraft.network.protocol.game.ServerboundContainerClickPacket; +import net.minecraft.network.protocol.game.ServerboundContainerClosePacket; +import net.minecraft.network.protocol.game.ServerboundContainerSlotStateChangedPacket; +import net.minecraft.network.protocol.game.ServerboundDebugSubscriptionRequestPacket; +import net.minecraft.network.protocol.game.ServerboundEditBookPacket; +import net.minecraft.network.protocol.game.ServerboundEntityTagQueryPacket; +import net.minecraft.network.protocol.game.ServerboundInteractPacket; +import net.minecraft.network.protocol.game.ServerboundJigsawGeneratePacket; +import net.minecraft.network.protocol.game.ServerboundLockDifficultyPacket; +import net.minecraft.network.protocol.game.ServerboundMovePlayerPacket; +import net.minecraft.network.protocol.game.ServerboundMoveVehiclePacket; +import net.minecraft.network.protocol.game.ServerboundPaddleBoatPacket; +import net.minecraft.network.protocol.game.ServerboundPickItemFromBlockPacket; +import net.minecraft.network.protocol.game.ServerboundPickItemFromEntityPacket; +import net.minecraft.network.protocol.game.ServerboundPlaceRecipePacket; +import net.minecraft.network.protocol.game.ServerboundPlayerAbilitiesPacket; +import net.minecraft.network.protocol.game.ServerboundPlayerActionPacket; +import net.minecraft.network.protocol.game.ServerboundPlayerCommandPacket; +import net.minecraft.network.protocol.game.ServerboundPlayerInputPacket; +import net.minecraft.network.protocol.game.ServerboundPlayerLoadedPacket; +import net.minecraft.network.protocol.game.ServerboundPunchPacket; +import net.minecraft.network.protocol.game.ServerboundRecipeBookChangeSettingsPacket; +import net.minecraft.network.protocol.game.ServerboundRecipeBookSeenRecipePacket; +import net.minecraft.network.protocol.game.ServerboundRenameItemPacket; +import net.minecraft.network.protocol.game.ServerboundSeenAdvancementsPacket; +import net.minecraft.network.protocol.game.ServerboundSelectBundleItemPacket; +import net.minecraft.network.protocol.game.ServerboundSelectTradePacket; +import net.minecraft.network.protocol.game.ServerboundSetBeaconPacket; +import net.minecraft.network.protocol.game.ServerboundSetCarriedItemPacket; +import net.minecraft.network.protocol.game.ServerboundSetCommandBlockPacket; +import net.minecraft.network.protocol.game.ServerboundSetCommandMinecartPacket; +import net.minecraft.network.protocol.game.ServerboundSetCreativeModeSlotPacket; +import net.minecraft.network.protocol.game.ServerboundSetGameRulePacket; +import net.minecraft.network.protocol.game.ServerboundSetJigsawBlockPacket; +import net.minecraft.network.protocol.game.ServerboundSetStructureBlockPacket; +import net.minecraft.network.protocol.game.ServerboundSetTestBlockPacket; +import net.minecraft.network.protocol.game.ServerboundSignUpdatePacket; +import net.minecraft.network.protocol.game.ServerboundSpectatorActionPacket; +import net.minecraft.network.protocol.game.ServerboundTeleportToEntityPacket; +import net.minecraft.network.protocol.game.ServerboundTestInstanceBlockActionPacket; +import net.minecraft.network.protocol.game.ServerboundUseItemOnPacket; +import net.minecraft.network.protocol.game.ServerboundUseItemPacket; +import net.minecraft.network.protocol.ping.ClientboundPongResponsePacket; +import net.minecraft.network.protocol.ping.ServerboundPingRequestPacket; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import com.ishland.raknetify.fabric.common.util.MinecraftVersions; + +import java.util.Set; + +public class RakNetMultiChannel { + + private RakNetMultiChannel() { + } + + private static final Set> unordered = Set.of( + ClientboundKeepAlivePacket.class, + ServerboundKeepAlivePacket.class, + ClientboundDisconnectPacket.class, + ClientboundPingPacket.class, + ServerboundPongPacket.class, + ClientboundResourcePackPushPacket.class, + ServerboundResourcePackPacket.class, + ClientboundResourcePackPopPacket.class, + ClientboundAwardStatsPacket.class, + ServerboundSeenAdvancementsPacket.class, + ClientboundSelectAdvancementsTabPacket.class, + ServerboundPingRequestPacket.class, + ClientboundPongResponsePacket.class, + ClientboundCustomReportDetailsPacket.class, + ClientboundServerLinksPacket.class + ); + + private static final Set> channel0 = Set.of( + ClientboundStartConfigurationPacket.class, + ServerboundConfigurationAcknowledgedPacket.class, + ClientboundTransferPacket.class + ); + + // Primarily used for interactions independent to world + private static final Set> channel1 = Set.of( + ClientboundLowDiskSpaceWarningPacket.class, + ServerboundSetGameRulePacket.class, + ClientboundGameRuleValuesPacket.class, + ClientboundBossEventPacket.class, + ClientboundClearTitlesPacket.class, + ClientboundSetSubtitleTextPacket.class, + ClientboundSetTitleTextPacket.class, + ClientboundSetTitlesAnimationPacket.class, + ClientboundPlayerCombatKillPacket.class, + ClientboundSystemChatPacket.class, + ClientboundDisguisedChatPacket.class, + ClientboundPlayerChatPacket.class, + ClientboundCustomChatCompletionsPacket.class, + ClientboundDeleteChatPacket.class, + ServerboundChatSessionUpdatePacket.class, + ServerboundChatAckPacket.class, + ClientboundServerDataPacket.class, + ClientboundSetActionBarTextPacket.class, + ClientboundSetExperiencePacket.class, + ClientboundSetHealthPacket.class, + ClientboundCooldownPacket.class, + ClientboundSetDisplayObjectivePacket.class, + ClientboundSetObjectivePacket.class, + ClientboundSetScorePacket.class, + ClientboundResetScorePacket.class, + ClientboundUpdateAdvancementsPacket.class, + ClientboundCommandsPacket.class, + ClientboundCommandSuggestionsPacket.class, + ServerboundCommandSuggestionPacket.class, + ServerboundChatCommandPacket.class, + ServerboundChatCommandSignedPacket.class, + ServerboundContainerButtonClickPacket.class, + ServerboundChatPacket.class, + ServerboundContainerClickPacket.class, + ServerboundPlayerCommandPacket.class, + ServerboundClientInformationPacket.class, + ServerboundChangeDifficultyPacket.class, + ServerboundLockDifficultyPacket.class, + ClientboundPlayerPositionPacket.class, + ServerboundPlayerActionPacket.class, + ServerboundPlayerInputPacket.class, + ServerboundPickItemFromBlockPacket.class, + ServerboundPickItemFromEntityPacket.class, + ClientboundPlayerLookAtPacket.class, + ClientboundSetTimePacket.class, + ClientboundChangeDifficultyPacket.class, + ClientboundSetPlayerTeamPacket.class, + ClientboundUpdateRecipesPacket.class, + ClientboundUpdateTagsPacket.class, + ClientboundPlayerInfoUpdatePacket.class, + ClientboundTabListPacket.class, + ClientboundPlayerInfoRemovePacket.class, + ClientboundCookieRequestPacket.class, + ClientboundStoreCookiePacket.class, + ServerboundCookieResponsePacket.class, + ClientboundDebugSamplePacket.class, + ServerboundDebugSubscriptionRequestPacket.class, + ClientboundTrackedWaypointPacket.class, + ClientboundClearDialogPacket.class, + ClientboundShowDialogPacket.class, + ServerboundCustomClickActionPacket.class + ); + + // Entities related stuff + private static final Set> channel2 = Set.of( + ServerboundAttackPacket.class, + ClientboundPlayerCombatEndPacket.class, + ClientboundPlayerCombatEnterPacket.class, + ClientboundRemoveEntitiesPacket.class, + ClientboundAddEntityPacket.class, + ClientboundAnimatePacket.class, + ClientboundEntityEventPacket.class, + ClientboundMoveEntityPacket.Pos.class, + ClientboundMoveEntityPacket.Rot.class, + ClientboundMoveEntityPacket.PosRot.class, + ClientboundRotateHeadPacket.class, + ClientboundSetEntityDataPacket.class, + ClientboundSetEntityLinkPacket.class, + ClientboundSetEntityMotionPacket.class, + ClientboundSetEquipmentPacket.class, + ClientboundSetPassengersPacket.class, + ClientboundTeleportEntityPacket.class, + ClientboundEntityPositionSyncPacket.class, + ClientboundUpdateAttributesPacket.class, + ClientboundUpdateMobEffectPacket.class, + ClientboundRemoveMobEffectPacket.class, + ClientboundHurtAnimationPacket.class, + ClientboundDamageEventPacket.class, + ClientboundMoveMinecartPacket.class, + ServerboundUseItemOnPacket.class, + ServerboundUseItemPacket.class, + ServerboundInteractPacket.class, + ServerboundMovePlayerPacket.class, + ServerboundMovePlayerPacket.StatusOnly.class, + ServerboundMovePlayerPacket.Pos.class, + ServerboundMovePlayerPacket.PosRot.class, + ServerboundMovePlayerPacket.Rot.class, + ServerboundClientTickEndPacket.class, + ServerboundChangeGameModePacket.class, + ClientboundExplodePacket.class, + ClientboundLoginPacket.class, + ClientboundUpdateEnabledFeaturesPacket.class, + ClientboundGameEventPacket.class, + ClientboundSetDefaultSpawnPositionPacket.class, + ClientboundTakeItemEntityPacket.class, + ClientboundPlayerAbilitiesPacket.class, + ClientboundSetCameraPacket.class, + ClientboundMoveVehiclePacket.class, + ServerboundPaddleBoatPacket.class, + ServerboundMoveVehiclePacket.class, + ClientboundPlayerRotationPacket.class, + ClientboundMoveEntityPacket.class, + ClientboundProjectilePowerPacket.class, + ClientboundDebugEntityValuePacket.class, + ClientboundDebugEventPacket.class + ); + + // Primarily used for interactions dependent to world + private static final Set> channel3 = Set.of( + ServerboundTeleportToEntityPacket.class, + ServerboundAcceptTeleportationPacket.class, + ServerboundBlockEntityTagQueryPacket.class, + ServerboundEntityTagQueryPacket.class, + ClientboundTagQueryPacket.class, + ClientboundContainerClosePacket.class, + ClientboundMountScreenOpenPacket.class, + ClientboundOpenScreenPacket.class, + ClientboundContainerSetDataPacket.class, + ClientboundContainerSetSlotPacket.class, + ClientboundOpenBookPacket.class, + ClientboundContainerSetContentPacket.class, + ClientboundSetHeldSlotPacket.class, + ClientboundMerchantOffersPacket.class, + ClientboundRecipeBookAddPacket.class, + ClientboundRecipeBookRemovePacket.class, + ClientboundRecipeBookSettingsPacket.class, + ServerboundEditBookPacket.class, + ServerboundRecipeBookSeenRecipePacket.class, + ServerboundRecipeBookChangeSettingsPacket.class, + ServerboundRenameItemPacket.class, + ServerboundContainerClosePacket.class, + ServerboundSetCreativeModeSlotPacket.class, + ServerboundPlaceRecipePacket.class, + ServerboundSelectTradePacket.class, + ServerboundSetBeaconPacket.class, + ServerboundSetCommandBlockPacket.class, + ServerboundSetCommandMinecartPacket.class, + ServerboundSetTestBlockPacket.class, + ServerboundTestInstanceBlockActionPacket.class, + ServerboundSetJigsawBlockPacket.class, + ServerboundPlayerAbilitiesPacket.class, + ServerboundSetCarriedItemPacket.class, + ServerboundSetStructureBlockPacket.class, + ClientboundPlaceGhostRecipePacket.class, + ServerboundContainerSlotStateChangedPacket.class, + ClientboundSetCursorItemPacket.class, + ClientboundSetPlayerInventoryPacket.class, + ServerboundSelectBundleItemPacket.class, + ClientboundInitializeBorderPacket.class, + ClientboundSetBorderCenterPacket.class, + ClientboundSetBorderLerpSizePacket.class, + ClientboundSetBorderSizePacket.class, + ClientboundSetBorderWarningDelayPacket.class, + ClientboundSetBorderWarningDistancePacket.class, + ServerboundClientCommandPacket.class, + ClientboundRespawnPacket.class, + ServerboundCustomPayloadPacket.class, + ClientboundCustomPayloadPacket.class, + ClientboundTickingStepPacket.class, + ClientboundTickingStatePacket.class + ); + + // Primarily for packets not very critical to interactions + private static final Set> channel4 = Set.of( + ClientboundMapItemDataPacket.class, + ClientboundSoundEntityPacket.class, + ClientboundSoundPacket.class, + ClientboundStopSoundPacket.class, + ClientboundLevelParticlesPacket.class + ); + + // Used for worlds + private static final Set> channel7 = Set.of( + ServerboundJigsawGeneratePacket.class, + ClientboundOpenSignEditorPacket.class, + ServerboundSignUpdatePacket.class, + ClientboundBlockEventPacket.class, + ClientboundSetChunkCacheCenterPacket.class, + ClientboundSetChunkCacheRadiusPacket.class, + ClientboundSetSimulationDistancePacket.class, + ClientboundForgetLevelChunkPacket.class, + ClientboundBlockUpdatePacket.class, + ClientboundSectionBlocksUpdatePacket.class, + ClientboundLevelEventPacket.class, + ClientboundBlockDestructionPacket.class, + ClientboundLevelChunkWithLightPacket.class, + ClientboundBlockEntityDataPacket.class, + ClientboundLightUpdatePacket.class, + ClientboundChunksBiomesPacket.class, + ClientboundChunkBatchFinishedPacket.class, + ClientboundChunkBatchStartPacket.class, + ServerboundChunkBatchReceivedPacket.class, + ClientboundBlockChangedAckPacket.class, + ServerboundPlayerLoadedPacket.class, + ClientboundTestInstanceBlockStatus.class, + ClientboundDebugBlockValuePacket.class, + ClientboundDebugChunkValuePacket.class, + ClientboundGameTestHighlightPosPacket.class + ); + + private static final Set> unreliable = Set.of(); + + private static final Set> theVoid = Set.of( + BundleDelimiterPacket.class, + ClientboundBundleDelimiterPacket.class + ); + + private static final Object2IntOpenHashMap> classToChannelIdOverride = new Object2IntOpenHashMap<>(); + + static { + classToChannelIdOverride.defaultReturnValue(Integer.MAX_VALUE); + unordered.forEach(clazz -> classToChannelIdOverride.put(clazz, -1)); + channel0.forEach(clazz -> classToChannelIdOverride.put(clazz, 0)); + channel1.forEach(clazz -> classToChannelIdOverride.put(clazz, 1)); + channel2.forEach(clazz -> classToChannelIdOverride.put(clazz, 2)); + channel3.forEach(clazz -> classToChannelIdOverride.put(clazz, 3)); + channel4.forEach(clazz -> classToChannelIdOverride.put(clazz, 4)); + channel7.forEach(clazz -> classToChannelIdOverride.put(clazz, 7)); + unreliable.forEach(clazz -> classToChannelIdOverride.put(clazz, -2)); + theVoid.forEach(clazz -> classToChannelIdOverride.put(clazz, Integer.MIN_VALUE)); + // These older packet names are absent from the 26.3 compile target and must be resolved by name. + if (!MinecraftVersions.AT_LEAST_26_3) { + addEarlierPacket("ServerboundSwingPacket", 2); + } + if (!MinecraftVersions.AT_LEAST_26_2) { + addEarlierPacket("ServerboundSpectateEntityPacket", 3); + } + // Keep newer class references inside their version guards so older runtimes never resolve them. + if (MinecraftVersions.AT_LEAST_26_3) classToChannelIdOverride.put(ServerboundPunchPacket.class, 2); + if (MinecraftVersions.AT_LEAST_26_3) classToChannelIdOverride.put(ClientboundSwingAnimationPacket.class, 2); + if (MinecraftVersions.AT_LEAST_26_2) classToChannelIdOverride.put(ServerboundSpectatorActionPacket.class, 3); + if (MinecraftVersions.AT_LEAST_26_3) classToChannelIdOverride.put(ClientboundPostEffectsPacket.class, 4); + if (MinecraftVersions.AT_LEAST_26_3) classToChannelIdOverride.put(ClientboundAddTransientBlockPacket.class, 7); + } + + private static void addEarlierPacket(String name, int channel) { + try { + Class packet = Class.forName("net.minecraft.network.protocol.game." + name, false, + RakNetMultiChannel.class.getClassLoader()); + classToChannelIdOverride.put(packet, channel); + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Missing expected packet " + name, e); + } + } + + private static final Set> foundUnknownClasses = Sets.newConcurrentHashSet(); + + public static int getPacketChannelOverride(Class clazz, boolean suppressWarning) { + if (clazz == null) { + System.err.println("Raknetify: Warning: Tried to send packet without setting packet class"); + return 0; + } + int channelOverride = classToChannelIdOverride.getInt(clazz); + if (channelOverride == Integer.MAX_VALUE) { + if (!suppressWarning) { + if (foundUnknownClasses.add(clazz)) { + System.err.println("Raknetify: Warning: unknown packet type %s for raknet multi-channel".formatted(clazz.getName())); + } + } + channelOverride = 7; + } + return channelOverride; + } + + public static void init() { + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetNetworkTransitionUtil.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetNetworkTransitionUtil.java new file mode 100644 index 00000000..f09e177a --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetNetworkTransitionUtil.java @@ -0,0 +1,46 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.common.connection; + +import net.minecraft.network.UnconfiguredPipelineHandler; + +public class RakNetNetworkTransitionUtil { + + static Object handleTransition(Object msg) { + if (msg instanceof UnconfiguredPipelineHandler.InboundConfigurationTask transitioner) { + return (UnconfiguredPipelineHandler.InboundConfigurationTask) context -> { + transitioner.run(context); + RakNetFabricConnectionUtil.onPipelineReorder(context.pipeline()); + }; + } else if (msg instanceof UnconfiguredPipelineHandler.OutboundConfigurationTask transitioner) { + return (UnconfiguredPipelineHandler.OutboundConfigurationTask) context -> { + transitioner.run(context); + RakNetFabricConnectionUtil.onPipelineReorder(context.pipeline()); + }; + } + return msg; + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/encryption/PacketEncryptionManagerInterface.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/encryption/PacketEncryptionManagerInterface.java new file mode 100644 index 00000000..95f3e95b --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/connection/encryption/PacketEncryptionManagerInterface.java @@ -0,0 +1,33 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.common.connection.encryption; + +import io.netty.channel.ChannelHandlerContext; + +public interface PacketEncryptionManagerInterface { + + void setContext(ChannelHandlerContext ctx); + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/package-info.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/package-info.java new file mode 100644 index 00000000..130c13a7 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/package-info.java @@ -0,0 +1,25 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.common; diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/quirks/ClientHungerManager.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/quirks/ClientHungerManager.java new file mode 100644 index 00000000..d99f0c10 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/quirks/ClientHungerManager.java @@ -0,0 +1,62 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.common.quirks; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import net.minecraft.world.food.FoodData; +import net.minecraft.world.food.FoodProperties; + +public class ClientHungerManager extends FoodData { + + public static ClientHungerManager from(FoodData hungerManager) { + ClientHungerManager clientHungerManager = new ClientHungerManager(); + for (Field field : FoodData.class.getFields()) { + if ((field.getModifiers() & Modifier.STATIC) == 0 && (field.getModifiers() & Modifier.FINAL) == 0) { + if (!field.trySetAccessible()) { + System.err.println("Failed to set field " + field.getName() + " accessible in HungerManager"); + continue; + } + try { + Object value = field.get(hungerManager); + field.set(clientHungerManager, value); + } catch (Throwable e) { + System.err.println("Failed to copy field " + field.getName() + " from HungerManager to ClientHungerManager"); + e.printStackTrace(); + } + } + } + return clientHungerManager; + } + + public void eat(int food, float saturationModifier) { + // nop + } + + public void eat(FoodProperties foodComponent) { + // nop + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/util/FieldSignatureParser.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/util/FieldSignatureParser.java new file mode 100644 index 00000000..e77d736b --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/util/FieldSignatureParser.java @@ -0,0 +1,56 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.common.util; + +import org.objectweb.asm.Type; +import org.objectweb.asm.signature.SignatureReader; +import org.objectweb.asm.signature.SignatureVisitor; +import org.spongepowered.asm.util.asm.ASM; + +import java.util.ArrayList; +import java.util.List; + +public class FieldSignatureParser extends SignatureVisitor { + + private final List results = new ArrayList<>(); + + private FieldSignatureParser() { + super(ASM.API_VERSION); + } + + public static List parse(String signature) { + if (signature == null || signature.isEmpty()) { + return List.of(); + } + FieldSignatureParser parser = new FieldSignatureParser(); + new SignatureReader(signature).acceptType(parser); + return parser.results; + } + + @Override + public void visitClassType(String name) { + results.add(Type.getObjectType(name)); + } +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/util/MinecraftVersions.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/util/MinecraftVersions.java new file mode 100644 index 00000000..7186a89b --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/util/MinecraftVersions.java @@ -0,0 +1,52 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.common.util; + +import net.fabricmc.loader.api.FabricLoader; +import net.fabricmc.loader.api.Version; +import net.fabricmc.loader.api.VersionParsingException; +import net.fabricmc.loader.api.metadata.version.VersionPredicate; + +public final class MinecraftVersions { + + private static final Version CURRENT = FabricLoader.getInstance().getModContainer("minecraft") + .orElseThrow().getMetadata().getVersion(); + + public static final boolean AT_LEAST_26_2 = matches(">=26.2"); + public static final boolean AT_LEAST_26_3 = matches(">=26.3"); + // Keep this exact snapshot predicate aligned with the Minecraft dependency in fabric.mod.json. + public static final boolean IS_26_4_SNAPSHOT = matches("26.4-alpha.7"); + + private MinecraftVersions() { + } + + private static boolean matches(String predicate) { + try { + return VersionPredicate.parse(predicate).test(CURRENT); + } catch (VersionParsingException exception) { + throw new ExceptionInInitializerError(exception); + } + } +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/util/NetworkStates.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/util/NetworkStates.java new file mode 100644 index 00000000..e378e74b --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/common/util/NetworkStates.java @@ -0,0 +1,41 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.common.util; + +import net.minecraft.network.ConnectionProtocol; + +public class NetworkStates { + + public static String getName(ConnectionProtocol state) { + return switch (state) { + case HANDSHAKING -> "HANDSHAKING"; + case PLAY -> "PLAY"; + case STATUS -> "STATUS"; + case LOGIN -> "LOGIN"; + default -> state.toString(); + }; + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/RaknetifyFabricMixinPlugin.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/RaknetifyFabricMixinPlugin.java new file mode 100644 index 00000000..297d4524 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/RaknetifyFabricMixinPlugin.java @@ -0,0 +1,78 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin; + +import com.llamalad7.mixinextras.MixinExtrasBootstrap; +import com.ishland.raknetify.fabric.common.util.MinecraftVersions; +import org.objectweb.asm.tree.ClassNode; +import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; +import org.spongepowered.asm.mixin.extensibility.IMixinInfo; + +import java.util.List; +import java.util.Set; + +public class RaknetifyFabricMixinPlugin implements IMixinConfigPlugin { + + @Override + public void onLoad(String mixinPackage) { + System.setProperty("raknetserver.maxPacketLoss", String.valueOf(Integer.MAX_VALUE)); + MixinExtrasBootstrap.init(); + } + + @Override + public String getRefMapperConfig() { + return null; + } + + @Override + public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { + return switch (mixinClassName.substring("com.ishland.raknetify.fabric.mixin.".length())) { + case "access.IDebugHudEntries", "compat.krypton.MixinServerLoginNetworkHandler" -> !MinecraftVersions.IS_26_4_SNAPSHOT; + case "client.hud.MixinDebugHudEntries26_4", "common.encryption.MixinClientConnection26_4", + "compat.krypton.MixinServerLoginNetworkHandler26_4" -> MinecraftVersions.IS_26_4_SNAPSHOT; + default -> true; + }; + } + + @Override + public void acceptTargets(Set myTargets, Set otherTargets) { + + } + + @Override + public List getMixins() { + return null; + } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + + } + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) { + + } +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/access/IClientConnection.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/access/IClientConnection.java new file mode 100644 index 00000000..36bbed8d --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/access/IClientConnection.java @@ -0,0 +1,38 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.access; + +import io.netty.channel.Channel; +import net.minecraft.network.Connection; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(Connection.class) +public interface IClientConnection { + + @Accessor + Channel getChannel(); + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/access/IDebugHudEntries.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/access/IDebugHudEntries.java new file mode 100644 index 00000000..483063bc --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/access/IDebugHudEntries.java @@ -0,0 +1,41 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.access; + +import net.minecraft.client.gui.components.debug.DebugScreenEntries; +import net.minecraft.client.gui.components.debug.DebugScreenEntry; +import net.minecraft.resources.Identifier; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(DebugScreenEntries.class) +public interface IDebugHudEntries { + + @Invoker + static Identifier invokeRegister(Identifier id, DebugScreenEntry entry) { + throw new AbstractMethodError(); + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/access/IPacketCodecDispatcher.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/access/IPacketCodecDispatcher.java new file mode 100644 index 00000000..9f576655 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/access/IPacketCodecDispatcher.java @@ -0,0 +1,40 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.access; + +import io.netty.buffer.ByteBuf; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import java.util.List; +import net.minecraft.network.codec.IdDispatchCodec; + +@Mixin(IdDispatchCodec.class) +public interface IPacketCodecDispatcher { + + @Accessor + List> getById(); + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/access/IPacketEncryptionManager.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/access/IPacketEncryptionManager.java new file mode 100644 index 00000000..2843b412 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/access/IPacketEncryptionManager.java @@ -0,0 +1,44 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.access; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; + +import javax.crypto.ShortBufferException; +import net.minecraft.network.CipherBase; + +@Mixin(CipherBase.class) +public interface IPacketEncryptionManager { + + @Invoker + ByteBuf invokeDecipher(ChannelHandlerContext context, ByteBuf buf) throws ShortBufferException; + + @Invoker + void invokeEncipher(ByteBuf buf, ByteBuf result) throws ShortBufferException; + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/access/IWorld.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/access/IWorld.java new file mode 100644 index 00000000..c3dfd5f6 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/access/IWorld.java @@ -0,0 +1,37 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.access; + +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(Level.class) +public interface IWorld { + + @Accessor("isClientSide") + boolean raknetify$isClient(); + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/client/MixinClientPlayNetworkHandler.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/client/MixinClientPlayNetworkHandler.java new file mode 100644 index 00000000..f896526e --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/client/MixinClientPlayNetworkHandler.java @@ -0,0 +1,51 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.client; + +import com.ishland.raknetify.common.connection.RakNetSimpleMultiChannelCodec; +import com.ishland.raknetify.fabric.mixin.access.IClientConnection; +import io.netty.channel.Channel; +import net.minecraft.client.multiplayer.ClientPacketListener; +import net.minecraft.network.protocol.game.ClientboundLoginPacket; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ClientPacketListener.class) +public class MixinClientPlayNetworkHandler { + + @Inject(method = "handleLogin", at = @At("RETURN")) + private void postGameJoin(ClientboundLoginPacket packet, CallbackInfo ci) { + final Channel channel = ((IClientConnection) ((ClientPacketListener) (Object) this).getConnection()).getChannel(); + if (channel == null) { + //noinspection RedundantStringFormatCall + System.err.println("Raknetify: Warning: %s don't have valid channel when logged in, not sending sync packet".formatted(this)); + return; + } + channel.eventLoop().execute(() -> channel.write(RakNetSimpleMultiChannelCodec.SIGNAL_START_MULTICHANNEL)); + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/client/MixinConnectionScreen1.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/client/MixinConnectionScreen1.java new file mode 100644 index 00000000..e90ab91f --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/client/MixinConnectionScreen1.java @@ -0,0 +1,76 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.client; + +import com.ishland.raknetify.fabric.common.connection.RakNetClientConnectionUtil; +import com.ishland.raknetify.common.util.PrefixUtil; +import com.ishland.raknetify.fabric.common.client.ServerAddressUtil; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import io.netty.channel.ChannelFuture; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.net.InetSocketAddress; +import net.minecraft.client.multiplayer.resolver.ServerAddress; +import net.minecraft.network.Connection; +import net.minecraft.server.network.EventLoopGroupHolder; + +@Mixin(targets = "net/minecraft/client/gui/screens/ConnectScreen$1") +public class MixinConnectionScreen1 extends Thread { + + @Mutable + @Shadow + @Final + ServerAddress val$hostAndPort; + + @Unique + private boolean isRaknet = false; + + @Unique + private boolean raknetLargeMTU = false; + + @Inject(method = "*", at = @At("RETURN"), remap = false) + private void onInit(CallbackInfo ci) { + final PrefixUtil.Info info = PrefixUtil.getInfo(this.val$hostAndPort.getHost()); + if (info.useRakNet()) { + this.isRaknet = true; + this.raknetLargeMTU = info.largeMTU(); + this.val$hostAndPort = ServerAddressUtil.withAddress(this.val$hostAndPort, info.stripped(), this.val$hostAndPort.getPort()); + } + } + + @WrapOperation(method = "run()V", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/Connection;connect(Ljava/net/InetSocketAddress;Lnet/minecraft/server/network/EventLoopGroupHolder;Lnet/minecraft/network/Connection;)Lio/netty/channel/ChannelFuture;"), require = 0) + private ChannelFuture connectRaknet(InetSocketAddress address, EventLoopGroupHolder backend, Connection connection, Operation original) { + return this.isRaknet ? RakNetClientConnectionUtil.connect(address, backend, this.raknetLargeMTU, original, connection) : original.call(address, backend, connection); + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/client/MixinMultiplayerServerListPinger.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/client/MixinMultiplayerServerListPinger.java new file mode 100644 index 00000000..06fe464e --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/client/MixinMultiplayerServerListPinger.java @@ -0,0 +1,47 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.client; + +import com.ishland.raknetify.common.util.PrefixUtil; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import org.spongepowered.asm.mixin.Dynamic; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import net.minecraft.client.multiplayer.ServerStatusPinger; +import net.minecraft.client.multiplayer.resolver.ServerAddress; + +@Mixin(ServerStatusPinger.class) +public abstract class MixinMultiplayerServerListPinger { + + @Dynamic + @WrapOperation(method = "pingServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/multiplayer/resolver/ServerAddress;parseString(Ljava/lang/String;)Lnet/minecraft/client/multiplayer/resolver/ServerAddress;")) + private ServerAddress modifyRaknetAddress(String address, Operation original) { + final PrefixUtil.Info info = PrefixUtil.getInfo(address); + return original.call(info.stripped()); + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/client/MixinMultiplayerServerListPinger1.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/client/MixinMultiplayerServerListPinger1.java new file mode 100644 index 00000000..8d501041 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/client/MixinMultiplayerServerListPinger1.java @@ -0,0 +1,71 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.client; + +import com.ishland.raknetify.fabric.mixin.access.IClientConnection; +import com.llamalad7.mixinextras.injector.v2.WrapWithCondition; +import io.netty.channel.Channel; +import network.ycc.raknet.RakNet; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.net.InetSocketAddress; +import net.minecraft.client.multiplayer.ServerData; +import net.minecraft.client.multiplayer.ServerStatusPinger; +import net.minecraft.client.multiplayer.resolver.ServerAddress; +import net.minecraft.network.Connection; +import net.minecraft.network.protocol.status.ClientStatusPacketListener; +import net.minecraft.server.network.EventLoopGroupHolder; + +@Mixin(targets = "net/minecraft/client/multiplayer/ServerStatusPinger$1") +public abstract class MixinMultiplayerServerListPinger1 implements ClientStatusPacketListener { + + @Shadow + @Final + Connection val$connection; // synthetic + + @Shadow + @Final + ServerData val$data; // synthetic + + @Inject(method = "handleStatusResponse(Lnet/minecraft/network/protocol/status/ClientboundStatusResponsePacket;)V", at = @At("RETURN")) + private void setPingImmediately(CallbackInfo ci) { + final Channel channel = ((IClientConnection) val$connection).getChannel(); + if (channel.config() instanceof RakNet.Config config) { + val$data.ping = config.getRTTNanos() / 1_000_000; + } + } + + @WrapWithCondition(method = "onDisconnect", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/multiplayer/ServerStatusPinger;pingLegacyServer(Ljava/net/InetSocketAddress;Lnet/minecraft/client/multiplayer/resolver/ServerAddress;Lnet/minecraft/client/multiplayer/ServerData;Lnet/minecraft/server/network/EventLoopGroupHolder;)V"), require = 0) + private boolean noPingRaknet(ServerStatusPinger instance, InetSocketAddress socketAddress, ServerAddress address, ServerData serverInfo, EventLoopGroupHolder backend) { + final Channel channel = ((IClientConnection) val$connection).getChannel(); + return !(channel.config() instanceof RakNet.Config); + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/client/MixinMultiplayerServerListPinger1_21_11.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/client/MixinMultiplayerServerListPinger1_21_11.java new file mode 100644 index 00000000..4db4044d --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/client/MixinMultiplayerServerListPinger1_21_11.java @@ -0,0 +1,50 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.client; + +import com.ishland.raknetify.common.util.PrefixUtil; +import com.ishland.raknetify.fabric.common.connection.RakNetClientConnectionUtil; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import java.net.InetSocketAddress; +import net.minecraft.client.multiplayer.ServerData; +import net.minecraft.client.multiplayer.ServerStatusPinger; +import net.minecraft.network.Connection; +import net.minecraft.server.network.EventLoopGroupHolder; +import net.minecraft.util.debugchart.LocalSampleLogger; + +@Mixin(ServerStatusPinger.class) +public class MixinMultiplayerServerListPinger1_21_11 { + + @WrapOperation(method = "pingServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/Connection;connectToServer(Ljava/net/InetSocketAddress;Lnet/minecraft/server/network/EventLoopGroupHolder;Lnet/minecraft/util/debugchart/LocalSampleLogger;)Lnet/minecraft/network/Connection;")) + private Connection redirectConnect(InetSocketAddress address, EventLoopGroupHolder backend, LocalSampleLogger packetSizeLog, Operation original, ServerData entry) { + final PrefixUtil.Info info = PrefixUtil.getInfo(entry.ip); + return info.useRakNet() ? RakNetClientConnectionUtil.connect(address, backend, info.largeMTU(), original) : original.call(address, backend, null); + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/client/hud/MixinDebugHudEntries26_4.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/client/hud/MixinDebugHudEntries26_4.java new file mode 100644 index 00000000..3dd519b1 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/client/hud/MixinDebugHudEntries26_4.java @@ -0,0 +1,44 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.client.hud; + +import com.ishland.raknetify.fabric.common.client.DebugHudUtil1_21_9; +import net.minecraft.client.gui.components.debug.DebugScreenEntries; +import net.minecraft.client.gui.components.debug.DebugScreenEntry; +import net.minecraft.core.Registry; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(DebugScreenEntries.class) +public class MixinDebugHudEntries26_4 { + + // Register through the snapshot's registry bootstrap instead of the removed static registration API. + @Inject(method = "bootstrap", at = @At("RETURN")) + private static void registerDebugEntry(Registry registry, CallbackInfoReturnable ci) { + Registry.register(registry, DebugHudUtil1_21_9.ID, DebugHudUtil1_21_9.createEntry()); + } +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/MixinCCConnect.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/MixinCCConnect.java new file mode 100644 index 00000000..c038ba31 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/MixinCCConnect.java @@ -0,0 +1,85 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.common; + +import com.ishland.raknetify.common.Constants; +import com.ishland.raknetify.common.connection.RakNetConnectionUtil; +import com.ishland.raknetify.common.connection.RaknetifyEventLoops; +import com.ishland.raknetify.common.util.ThreadLocalUtil; +import com.ishland.raknetify.fabric.common.connection.RakNetFabricConnectionUtil; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import com.llamalad7.mixinextras.sugar.Share; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; +import io.netty.bootstrap.AbstractBootstrap; +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.FixedRecvByteBufAllocator; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.SocketChannel; +import network.ycc.raknet.RakNet; +import network.ycc.raknet.client.channel.RakNetClientThreadedChannel; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import java.net.InetSocketAddress; +import net.minecraft.network.Connection; + +@Mixin(Connection.class) +public class MixinCCConnect { + + @WrapOperation(method = "connect(Ljava/net/InetSocketAddress;Lnet/minecraft/server/network/EventLoopGroupHolder;Lnet/minecraft/network/Connection;)Lio/netty/channel/ChannelFuture;", at = @At(value = "INVOKE", target = "Lio/netty/bootstrap/Bootstrap;group(Lio/netty/channel/EventLoopGroup;)Lio/netty/bootstrap/AbstractBootstrap;", remap = false), require = 1) + private static AbstractBootstrap redirectGroup(Bootstrap instance, EventLoopGroup eventLoopGroup, Operation> original, InetSocketAddress address, @Share("raknetify$eventLoop") LocalRef raknetify$eventLoop) { + raknetify$eventLoop.set(eventLoopGroup); + return ThreadLocalUtil.isInitializingRaknet() + ? original.call(instance, RaknetifyEventLoops.DEFAULT_CLIENT_EVENT_LOOP_GROUP.get()) + : original.call(instance, eventLoopGroup); + } + + @WrapOperation(method = "connect(Ljava/net/InetSocketAddress;Lnet/minecraft/server/network/EventLoopGroupHolder;Lnet/minecraft/network/Connection;)Lio/netty/channel/ChannelFuture;", at = @At(value = "INVOKE", target = "Lio/netty/bootstrap/Bootstrap;channel(Ljava/lang/Class;)Lio/netty/bootstrap/AbstractBootstrap;", remap = false), require = 1) + private static AbstractBootstrap redirectChannel(Bootstrap instance, Class aClass, Operation> original, InetSocketAddress address, @Share("raknetify$eventLoop") LocalRef raknetify$eventLoop) { + EventLoopGroup eventLoopGroup = raknetify$eventLoop.get(); + return ThreadLocalUtil.isInitializingRaknet() + ? instance.channelFactory(() -> { + final boolean initializingRaknetLargeMTU = ThreadLocalUtil.isInitializingRaknetLargeMTU(); + final RakNetClientThreadedChannel channel = new RakNetClientThreadedChannel(() -> { + final DatagramChannel channel1 = RakNetFabricConnectionUtil.fromSocketChannel(aClass); + channel1.config().setOption(ChannelOption.IP_TOS, RakNetConnectionUtil.DEFAULT_IP_TOS); + if (initializingRaknetLargeMTU) + channel1.config().setRecvByteBufAllocator(new FixedRecvByteBufAllocator(Constants.LARGE_MTU + 512).maxMessagesPerRead(128)); + else + channel1.config().setRecvByteBufAllocator(new FixedRecvByteBufAllocator(Constants.DEFAULT_MTU + 512).maxMessagesPerRead(128)); + return channel1; + }); + RakNet.config(channel).setMTU(initializingRaknetLargeMTU ? Constants.LARGE_MTU : Constants.DEFAULT_MTU); + channel.setProvidedParentEventLoop(eventLoopGroup.next()); + return channel; + }) + : original.call(instance, aClass); + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/MixinClientConnection.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/MixinClientConnection.java new file mode 100644 index 00000000..b0c2ffa8 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/MixinClientConnection.java @@ -0,0 +1,123 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.common; + +import com.ishland.raknetify.common.Constants; +import com.ishland.raknetify.common.util.DebugUtil; +import com.ishland.raknetify.fabric.common.util.NetworkStates; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.AttributeKey; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.net.SocketAddress; +import java.nio.channels.ClosedChannelException; +import net.minecraft.network.Connection; +import net.minecraft.network.ConnectionProtocol; +import net.minecraft.network.PacketListener; +import net.minecraft.network.protocol.PacketFlow; + +@Mixin(Connection.class) +public abstract class MixinClientConnection { + + @Shadow + private Channel channel; + + @Shadow public abstract SocketAddress getRemoteAddress(); + + @Shadow private SocketAddress address; + + @Shadow public abstract PacketFlow getReceiving(); + + @Shadow private volatile @Nullable PacketListener packetListener; + @Unique + private volatile boolean isClosing = false; + + @Redirect(method = {"disconnect(Lnet/minecraft/network/chat/Component;)V", "disconnect(Lnet/minecraft/network/DisconnectionDetails;)V"}, at = @At(value = "INVOKE", target = "Lio/netty/channel/ChannelFuture;awaitUninterruptibly()Lio/netty/channel/ChannelFuture;", remap = false), require = 1) + private ChannelFuture noDisconnectWait(ChannelFuture instance) { + isClosing = true; +// if (instance.channel().eventLoop().inEventLoop()) { +// return instance; // no-op +// } else { +// return instance.awaitUninterruptibly(); +// } + return instance; + } + + @Redirect(method = "*", at = @At(value = "INVOKE", target = "Lio/netty/channel/Channel;isOpen()Z", remap = false)) + private boolean redirectIsOpen(Channel instance) { + return this.channel != null && (this.channel.isOpen() && !this.isClosing); + } + +// @Inject(method = "channelActive", at = @At("HEAD")) +// private void onChannelActive(ChannelHandlerContext ctx, CallbackInfo ci) { +// final Channel channel = ctx.channel(); +// if (channel.config() instanceof RakNet.Config config) { +// if (this.side == NetworkSide.SERVERBOUND) { +// System.out.println(String.format("RakNet connection from %s, mtu %d", channel.remoteAddress(), config.getMTU())); +// } else if (this.side == NetworkSide.CLIENTBOUND) { +// System.out.println(String.format("RakNet conncted to %s, mtu %d", channel.remoteAddress(), config.getMTU())); +// } +// } +// } + + @Inject(method = "exceptionCaught", at = @At("HEAD")) + private void onExceptionCaught(ChannelHandlerContext context, Throwable ex, CallbackInfo ci) { + if (ex instanceof ClosedChannelException) return; + if (Constants.DEBUG) { + System.err.println("Exception caught for connection %s".formatted(this.channel)); + for (String s : DebugUtil.printChannelDetails(this.channel).split("\n")) { + System.err.println(" " + s); + } + ex.printStackTrace(); + } else { + final ConnectionProtocol state; + Object handler = this.channel.attr(AttributeKey.valueOf("protocol")).get(); // pre-1.20.2 + if (handler != null) { + if (handler instanceof ConnectionProtocol state1) { + state = state1; + } else { + System.err.println("Unknown handler type: " + handler.getClass().getName()); + state = null; + } + } else { + final PacketListener packetListener1 = this.packetListener; + state = packetListener1 != null ? packetListener1.protocol() : null; + } + if (state != null && state != ConnectionProtocol.HANDSHAKING) { + System.err.println(String.format("%s %s %s", this.address, NetworkStates.getName(state), ex.toString())); + } + } + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/MixinClientConnection1.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/MixinClientConnection1.java new file mode 100644 index 00000000..0955ca45 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/MixinClientConnection1.java @@ -0,0 +1,48 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.common; + +import com.ishland.raknetify.fabric.common.connection.RakNetFabricConnectionUtil; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(targets = "net/minecraft/network/Connection$1") +public abstract class MixinClientConnection1 extends ChannelInitializer { + + @Inject(method = "initChannel(Lio/netty/channel/Channel;)V", at = @At("HEAD")) + private void onChannelInit(Channel channel, CallbackInfo ci) { + RakNetFabricConnectionUtil.initChannel(channel); + } + + @Inject(method = "initChannel(Lio/netty/channel/Channel;)V", at = @At("RETURN")) + private void postChannelInit(Channel channel, CallbackInfo ci) { + RakNetFabricConnectionUtil.postInitChannel(channel, true); + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/MixinClientConnection1_20_2.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/MixinClientConnection1_20_2.java new file mode 100644 index 00000000..4484a59f --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/MixinClientConnection1_20_2.java @@ -0,0 +1,59 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.common; + +import com.ishland.raknetify.common.connection.SimpleMetricsLogger; +import io.netty.channel.Channel; +import net.minecraft.network.BandwidthDebugMonitor; +import net.minecraft.network.Connection; +import network.ycc.raknet.RakNet; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Connection.class) +public class MixinClientConnection1_20_2 { + + @Shadow private Channel channel; + @Shadow private @Nullable BandwidthDebugMonitor bandwidthDebugMonitor; + + @Unique + private long raknetify$lastBytesIn = 0L; + + @Inject(method = "tick", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/BandwidthDebugMonitor;tick()V")) + private void onPacketLoggerPush(CallbackInfo ci) { + BandwidthDebugMonitor logger = this.bandwidthDebugMonitor; + if (logger != null && this.channel.config() instanceof RakNet.Config config && config.getMetrics() instanceof SimpleMetricsLogger simpleMetricsLogger) { + long bytesIn = simpleMetricsLogger.getBytesIn(); + logger.onReceive((int) (bytesIn - this.raknetify$lastBytesIn)); + this.raknetify$lastBytesIn = bytesIn; + } + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/MixinServerAddress.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/MixinServerAddress.java new file mode 100644 index 00000000..f10e49bf --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/MixinServerAddress.java @@ -0,0 +1,52 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.common; + +import com.ishland.raknetify.common.Constants; +import com.ishland.raknetify.common.util.PrefixUtil; +import com.ishland.raknetify.fabric.common.client.ServerAddressUtil; +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.client.multiplayer.resolver.ServerAddress; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(ServerAddress.class) +public class MixinServerAddress { + + @WrapMethod(method = "parseString") + private static ServerAddress wrapParsing(String address, Operation original) { + PrefixUtil.Info info = PrefixUtil.getInfo(address); + if (info.useRakNet()) { + ServerAddress addr = original.call(info.stripped()); + return ServerAddressUtil.withAddress(addr, + (info.largeMTU() ? Constants.RAKNET_LARGE_MTU_PREFIX : Constants.RAKNET_PREFIX) + addr.getHost(), + addr.getPort() + ); + } else { + return original.call(address); + } + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/encryption/MixinClientConnection.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/encryption/MixinClientConnection.java new file mode 100644 index 00000000..a85a7d54 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/encryption/MixinClientConnection.java @@ -0,0 +1,64 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.common.encryption; + +import com.ishland.raknetify.common.connection.MultiChannelingStreamingCompression; +import com.ishland.raknetify.common.connection.MultiChannellingEncryption; +import io.netty.channel.Channel; +import network.ycc.raknet.RakNet; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import javax.crypto.Cipher; +import net.minecraft.network.Connection; + +@Mixin(Connection.class) +public class MixinClientConnection { + + @Shadow + private Channel channel; + + @Inject(method = "setEncryptionKey", at = @At("HEAD"), cancellable = true) + public void beforeSetupEncryption(Cipher decryptionCipher, Cipher encryptionCipher, CallbackInfo ci) { + if (this.channel.config() instanceof RakNet.Config) { + ci.cancel(); + try { + this.channel.pipeline().remove("decrypt"); + this.channel.pipeline().remove("encrypt"); + } catch (Throwable ignored) { + } + try { + this.channel.pipeline().remove(MultiChannellingEncryption.NAME); + } catch (Throwable ignored) { + } + this.channel.pipeline().addBefore(MultiChannelingStreamingCompression.NAME, MultiChannellingEncryption.NAME, + new MultiChannellingEncryption(decryptionCipher, encryptionCipher)); + } + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/encryption/MixinClientConnection26_4.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/encryption/MixinClientConnection26_4.java new file mode 100644 index 00000000..f7117ae0 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/encryption/MixinClientConnection26_4.java @@ -0,0 +1,61 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.common.encryption; + +import com.ishland.raknetify.common.connection.MultiChannelingStreamingCompression; +import com.ishland.raknetify.common.connection.MultiChannellingEncryption; +import io.netty.channel.Channel; +import net.minecraft.network.Connection; +import network.ycc.raknet.RakNet; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import javax.crypto.Cipher; + +@Mixin(Connection.class) +public class MixinClientConnection26_4 { + @Shadow private Channel channel; + + @Inject(method = "setInboundEncryptionKey", remap = false, at = @At("HEAD"), cancellable = true) + private void enableInboundEncryption(Cipher cipher, CallbackInfo ci) { + if (this.channel.config() instanceof RakNet.Config) { + ci.cancel(); + this.channel.pipeline().addBefore(MultiChannelingStreamingCompression.NAME, + MultiChannellingEncryption.NAME + "-inbound", MultiChannellingEncryption.inbound(cipher)); + } + } + + @Inject(method = "setOutboundEncryptionKey", remap = false, at = @At("HEAD"), cancellable = true) + private void enableOutboundEncryption(Cipher cipher, CallbackInfo ci) { + if (this.channel.config() instanceof RakNet.Config) { + ci.cancel(); + // Outbound traversal runs in reverse, so addBefore encrypts the frames after streaming compression. + this.channel.pipeline().addBefore(MultiChannelingStreamingCompression.NAME, + MultiChannellingEncryption.NAME + "-outbound", MultiChannellingEncryption.outbound(cipher)); + } + } +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/encryption/MixinPacketDecryptor.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/encryption/MixinPacketDecryptor.java new file mode 100644 index 00000000..37bb124f --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/encryption/MixinPacketDecryptor.java @@ -0,0 +1,51 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.common.encryption; + +import com.ishland.raknetify.fabric.common.connection.encryption.PacketEncryptionManagerInterface; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; +import net.minecraft.network.CipherBase; +import net.minecraft.network.CipherDecoder; + +@Mixin(CipherDecoder.class) +public class MixinPacketDecryptor { + + @Shadow @Final private CipherBase cipher; + + @Inject(method = "decode(Lio/netty/channel/ChannelHandlerContext;Lio/netty/buffer/ByteBuf;Ljava/util/List;)V", at = @At("HEAD")) + private void preDecrypt(ChannelHandlerContext ctx, ByteBuf byteBuf, List list, CallbackInfo ci) { + ((PacketEncryptionManagerInterface) this.cipher).setContext(ctx); + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/encryption/MixinPacketEncryptionManager.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/encryption/MixinPacketEncryptionManager.java new file mode 100644 index 00000000..1febdf8d --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/encryption/MixinPacketEncryptionManager.java @@ -0,0 +1,70 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.common.encryption; + +import com.ishland.raknetify.fabric.common.connection.encryption.PacketEncryptionManagerInterface; +import io.netty.channel.ChannelHandlerContext; +import network.ycc.raknet.RakNet; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.ShortBufferException; +import net.minecraft.network.CipherBase; + +@Mixin(CipherBase.class) +public class MixinPacketEncryptionManager implements PacketEncryptionManagerInterface { + + @Unique + private ChannelHandlerContext ctx; + + @Override + public void setContext(ChannelHandlerContext ctx) { + this.ctx = ctx; + } + + @Redirect(method = "decipher", at = @At(value = "INVOKE", target = "Ljavax/crypto/Cipher;update([BII[BI)I")) + private int redirectDecrypt(Cipher instance, byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) throws ShortBufferException, IllegalBlockSizeException, BadPaddingException { + if (ctx.channel().config() instanceof RakNet.Config) { + return instance.doFinal(input, inputOffset, inputLen, output, outputOffset); + } else { + return instance.update(input, inputOffset, inputLen, output, outputOffset); + } + } + + @Redirect(method = "encipher", at = @At(value = "INVOKE", target = "Ljavax/crypto/Cipher;update([BII[B)I")) + private int redirectEncrypt(Cipher instance, byte[] input, int inputOffset, int inputLen, byte[] output) throws ShortBufferException, IllegalBlockSizeException, BadPaddingException { + if (ctx.channel().config() instanceof RakNet.Config) { + return instance.doFinal(input, inputOffset, inputLen, output); + } else { + return instance.update(input, inputOffset, inputLen, output); + } + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/encryption/MixinPacketEncryptor.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/encryption/MixinPacketEncryptor.java new file mode 100644 index 00000000..34803b94 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/encryption/MixinPacketEncryptor.java @@ -0,0 +1,49 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.common.encryption; + +import com.ishland.raknetify.fabric.common.connection.encryption.PacketEncryptionManagerInterface; +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import net.minecraft.network.CipherBase; +import net.minecraft.network.CipherEncoder; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(CipherEncoder.class) +public class MixinPacketEncryptor { + + @Shadow @Final private CipherBase cipher; + + @Inject(method = "encode(Lio/netty/channel/ChannelHandlerContext;Lio/netty/buffer/ByteBuf;Lio/netty/buffer/ByteBuf;)V", at = @At("HEAD")) + private void preEncrypt(ChannelHandlerContext ctx, ByteBuf byteBuf, ByteBuf byteBuf2, CallbackInfo ci) { + ((PacketEncryptionManagerInterface) this.cipher).setContext(ctx); + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/quirks/MixinPlayerEntity.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/quirks/MixinPlayerEntity.java new file mode 100644 index 00000000..575af0c4 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/common/quirks/MixinPlayerEntity.java @@ -0,0 +1,56 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.common.quirks; + +import com.ishland.raknetify.fabric.common.quirks.ClientHungerManager; +import com.ishland.raknetify.fabric.mixin.access.IWorld; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.food.FoodData; +import net.minecraft.world.level.Level; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Player.class) +public abstract class MixinPlayerEntity extends LivingEntity { + + @Shadow protected FoodData foodData; + + protected MixinPlayerEntity(EntityType entityType, Level world) { + super(entityType, world); + } + + @Inject(method = "", at = @At("RETURN"), remap = false) + private void replaceHungerManager(CallbackInfo ci) { + if (((IWorld) this.level()).raknetify$isClient()) { + this.foodData = ClientHungerManager.from(this.foodData); + } + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/compat/fabricapi/MixinServerLoginNetworkAddon.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/compat/fabricapi/MixinServerLoginNetworkAddon.java new file mode 100644 index 00000000..70ce9d32 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/compat/fabricapi/MixinServerLoginNetworkAddon.java @@ -0,0 +1,59 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.compat.fabricapi; + +import com.ishland.raknetify.common.connection.MultiChannelingStreamingCompression; +import com.ishland.raknetify.fabric.mixin.access.IClientConnection; +import net.minecraft.network.Connection; +import net.minecraft.server.MinecraftServer; +import org.spongepowered.asm.mixin.Dynamic; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + + +@Pseudo +@Mixin(targets = "net.fabricmc.fabric.impl.networking.server.ServerLoginNetworkAddon") +public class MixinServerLoginNetworkAddon { + + @Shadow + @Final + private Connection connection; + + @Dynamic("Pseudo") + @Redirect(method = "sendCompressionPacket()V", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;getCompressionThreshold()I")) + private int stopCompressionIfStreamingCompressionExists(MinecraftServer server) { + final MultiChannelingStreamingCompression compression = ((IClientConnection) this.connection).getChannel().pipeline().get(MultiChannelingStreamingCompression.class); + if (compression != null && compression.isActive()) { + System.out.println("Raknetify: Preventing vanilla compression as streaming compression is enabled"); + return -1; + } + return server.getCompressionThreshold(); + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/compat/krypton/MixinServerLoginNetworkHandler.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/compat/krypton/MixinServerLoginNetworkHandler.java new file mode 100644 index 00000000..6a5424c2 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/compat/krypton/MixinServerLoginNetworkHandler.java @@ -0,0 +1,84 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.compat.krypton; + +import com.ishland.raknetify.fabric.mixin.access.IClientConnection; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelPipeline; +import net.minecraft.network.Connection; +import net.minecraft.network.protocol.login.ServerboundKeyPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerLoginPacketListenerImpl; +import net.minecraft.util.Crypt; +import net.minecraft.util.CryptException; +import network.ycc.raknet.RakNet; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; +import java.security.PrivateKey; + +@Mixin(ServerLoginPacketListenerImpl.class) +public class MixinServerLoginNetworkHandler { + + @Shadow + @Final + public Connection connection; + + @Shadow @Final private MinecraftServer server; + + @Shadow @Final private byte[] challenge; + + @Inject(method = "handleKey", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/Connection;setEncryptionKey(Ljavax/crypto/Cipher;Ljavax/crypto/Cipher;)V", shift = At.Shift.AFTER)) + private void afterSetupEncryption(ServerboundKeyPacket packet, CallbackInfo ci) throws CryptException { + final ChannelPipeline pipeline = ((IClientConnection) this.connection).getChannel().pipeline(); + if (((IClientConnection) this.connection).getChannel().config() instanceof RakNet.Config) { + final ChannelHandler decrypt = pipeline.get("decrypt"); + final ChannelHandler encrypt = pipeline.get("encrypt"); + if (decrypt != null && (decrypt.getClass().getName().equals("me.steinborn.krypton.mod.shared.network.pipeline.MinecraftCipherDecoder")) && + encrypt != null && (encrypt.getClass().getName().equals("me.steinborn.krypton.mod.shared.network.pipeline.MinecraftCipherEncoder"))) { + System.out.println("Raknetify: Krypton detected, applying compatibility"); + + pipeline.remove("decrypt"); + pipeline.remove("encrypt"); + + // TODO [VanillaCopy] + PrivateKey privateKey = this.server.getKeyPair().getPrivate(); + + SecretKey secretKey = packet.getSecretKey(privateKey); + Cipher cipher = Crypt.getCipher(2, secretKey); + Cipher cipher2 = Crypt.getCipher(1, secretKey); + + this.connection.setEncryptionKey(cipher, cipher2); + } + } + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/compat/krypton/MixinServerLoginNetworkHandler26_4.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/compat/krypton/MixinServerLoginNetworkHandler26_4.java new file mode 100644 index 00000000..ddd5a52c --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/compat/krypton/MixinServerLoginNetworkHandler26_4.java @@ -0,0 +1,75 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.compat.krypton; + +import com.ishland.raknetify.fabric.mixin.access.IClientConnection; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelPipeline; +import net.minecraft.network.Connection; +import net.minecraft.server.network.ServerLoginPacketListenerImpl; +import net.minecraft.util.Crypt; +import net.minecraft.util.CryptException; +import network.ycc.raknet.RakNet; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import javax.crypto.Cipher; +import javax.crypto.SecretKey; + +@Mixin(ServerLoginPacketListenerImpl.class) +public class MixinServerLoginNetworkHandler26_4 { + + @Shadow + @Final + public Connection connection; + + @Inject(method = "applyConnectionOptions", locals = LocalCapture.CAPTURE_FAILHARD, at = @At(value = "INVOKE", target = "Lnet/minecraft/network/Connection;setEncryptionKey(Ljavax/crypto/Cipher;Ljavax/crypto/Cipher;)V", shift = At.Shift.AFTER)) + private void afterSetupEncryption(byte options, CallbackInfo ci, SecretKey secretKey) throws CryptException { + final ChannelPipeline pipeline = ((IClientConnection) this.connection).getChannel().pipeline(); + if (((IClientConnection) this.connection).getChannel().config() instanceof RakNet.Config) { + final ChannelHandler decrypt = pipeline.get("decrypt"); + final ChannelHandler encrypt = pipeline.get("encrypt"); + if (decrypt != null && (decrypt.getClass().getName().equals("me.steinborn.krypton.mod.shared.network.pipeline.MinecraftCipherDecoder")) && + encrypt != null && (encrypt.getClass().getName().equals("me.steinborn.krypton.mod.shared.network.pipeline.MinecraftCipherEncoder"))) { + System.out.println("Raknetify: Krypton detected, applying compatibility"); + + pipeline.remove("decrypt"); + pipeline.remove("encrypt"); + + Cipher cipher = Crypt.getCipher(2, secretKey); + Cipher cipher2 = Crypt.getCipher(1, secretKey); + + // Re-enter the RakNet hooks to install frame encryption in place of Krypton's byte-stream handlers. + this.connection.setEncryptionKey(cipher, cipher2); + } + } + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/compat/package-info.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/compat/package-info.java new file mode 100644 index 00000000..63d563b6 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/compat/package-info.java @@ -0,0 +1,25 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.compat; diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/compat/qsl/MixinServerLoginNetworkAddon.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/compat/qsl/MixinServerLoginNetworkAddon.java new file mode 100644 index 00000000..4fe4ebac --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/compat/qsl/MixinServerLoginNetworkAddon.java @@ -0,0 +1,59 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.compat.qsl; + +import com.ishland.raknetify.common.connection.MultiChannelingStreamingCompression; +import com.ishland.raknetify.fabric.mixin.access.IClientConnection; +import net.minecraft.network.Connection; +import net.minecraft.server.MinecraftServer; +import org.spongepowered.asm.mixin.Dynamic; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Pseudo; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + + +@Pseudo +@Mixin(targets = "org.quiltmc.qsl.networking.impl.server.ServerLoginNetworkAddon") +public class MixinServerLoginNetworkAddon { + + @Shadow + @Final + private Connection connection; + + @Dynamic("Pseudo") + @Redirect(method = "sendCompressionPacket()V", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;getCompressionThreshold()I")) + private int stopCompressionIfStreamingCompressionExists(MinecraftServer server) { + final MultiChannelingStreamingCompression compression = ((IClientConnection) this.connection).getChannel().pipeline().get(MultiChannelingStreamingCompression.class); + if (compression != null && compression.isActive()) { + System.out.println("Raknetify: Preventing vanilla compression as streaming compression is enabled"); + return -1; + } + return server.getCompressionThreshold(); + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/package-info.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/package-info.java new file mode 100644 index 00000000..77db2af3 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/package-info.java @@ -0,0 +1,25 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin; diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/server/MixinPlayerManager1_20_2.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/server/MixinPlayerManager1_20_2.java new file mode 100644 index 00000000..084302eb --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/server/MixinPlayerManager1_20_2.java @@ -0,0 +1,61 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.server; + +import com.ishland.raknetify.common.connection.RakNetSimpleMultiChannelCodec; +import com.ishland.raknetify.fabric.mixin.access.IClientConnection; +import io.netty.channel.Channel; +import net.minecraft.network.Connection; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.CommonListenerCookie; +import net.minecraft.server.players.PlayerList; +import network.ycc.raknet.RakNet; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(PlayerList.class) +public class MixinPlayerManager1_20_2 { + + @Inject(method = "placeNewPlayer", at = @At("HEAD")) + private void onJoin(Connection connection, ServerPlayer player, CommonListenerCookie clientData, CallbackInfo ci) { + if (((IClientConnection) connection).getChannel().config() instanceof RakNet.Config config) { + System.out.println(String.format("Raknetify: %s logged in via RakNet, mtu %d", player.getName().getString(), config.getMTU())); + } + } + + @Inject(method = "placeNewPlayer", at = @At("RETURN")) + private void postJoin(Connection connection, ServerPlayer player, CommonListenerCookie clientData, CallbackInfo ci) { + final Channel channel = ((IClientConnection) connection).getChannel(); + if (channel == null) { + //noinspection RedundantStringFormatCall + System.err.println("Raknetify: Warning: %s don't have valid channel when logged in, not sending sync packet".formatted(this)); + return; + } + channel.write(RakNetSimpleMultiChannelCodec.SIGNAL_START_MULTICHANNEL); + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/server/MixinServerCommonNetworkHandler.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/server/MixinServerCommonNetworkHandler.java new file mode 100644 index 00000000..3b226096 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/server/MixinServerCommonNetworkHandler.java @@ -0,0 +1,78 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.server; + +import com.ishland.raknetify.fabric.mixin.access.IClientConnection; +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.injector.WrapWithCondition; +import io.netty.channel.Channel; +import net.minecraft.network.Connection; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.common.ServerCommonPacketListener; +import net.minecraft.server.network.ServerCommonPacketListenerImpl; +import net.minecraft.util.Util; +import network.ycc.raknet.RakNet; +import org.objectweb.asm.Opcodes; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ServerCommonPacketListenerImpl.class) +public abstract class MixinServerCommonNetworkHandler implements ServerCommonPacketListener { + + @Shadow @Final protected Connection connection; + + @Shadow private int latency; + + @ModifyExpressionValue(method = "keepConnectionAlive", at = @At(value = "FIELD", target = "Lnet/minecraft/server/network/ServerCommonPacketListenerImpl;keepAliveTime:J", opcode = Opcodes.GETFIELD)) + private long disableKeepAlive(long original) { + if (!(((IClientConnection) this.connection).getChannel().config() instanceof RakNet.Config)) { + return original; + } + return Util.getMillis(); + } + + @WrapWithCondition(method = "handleKeepAlive", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/network/ServerCommonPacketListenerImpl;disconnect(Lnet/minecraft/network/chat/Component;)V")) + private boolean stopTimeoutPlayersOnKeepAlive(ServerCommonPacketListenerImpl instance, Component reason) { + return !(((IClientConnection) this.connection).getChannel().config() instanceof RakNet.Config); + } + + @WrapWithCondition(method = "handleKeepAlive", at = @At(value = "FIELD", target = "Lnet/minecraft/server/network/ServerCommonPacketListenerImpl;latency:I", opcode = Opcodes.PUTFIELD)) + private boolean redirectPingStoring(ServerCommonPacketListenerImpl instance, int value) { + return !(((IClientConnection) this.connection).getChannel().config() instanceof RakNet.Config); + } + + @Inject(method = "keepConnectionAlive", at = @At("HEAD")) + private void onTick(CallbackInfo ci) { + final Channel channel = ((IClientConnection) this.connection).getChannel(); + if (channel != null && channel.config() instanceof RakNet.Config config) { + this.latency = (int) ((config.getRTTNanos() + config.getRTTStdDevNanos()) / 1_000_000); + } + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/server/MixinServerLoginNetworkHandler.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/server/MixinServerLoginNetworkHandler.java new file mode 100644 index 00000000..d93ab432 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/server/MixinServerLoginNetworkHandler.java @@ -0,0 +1,59 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.server; + +import com.ishland.raknetify.common.connection.MultiChannelingStreamingCompression; +import com.ishland.raknetify.fabric.mixin.access.IClientConnection; +import net.minecraft.network.Connection; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerLoginPacketListenerImpl; +import org.spongepowered.asm.mixin.Dynamic; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + + +@Mixin(value = ServerLoginPacketListenerImpl.class, priority = 900) +public class MixinServerLoginNetworkHandler { + + @Shadow @Final public Connection connection; + + @Shadow @Final private MinecraftServer server; + + // Compression setup moved into the constructor in the snapshot, so both locations share this redirect. + @Dynamic + @Redirect(method = {"verifyLoginAndFinishConnectionSetup", ""}, at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;getCompressionThreshold()I"), require = 1) + private int stopCompressionIfStreamingCompressionExists(MinecraftServer server) { + final MultiChannelingStreamingCompression compression = ((IClientConnection) this.connection).getChannel().pipeline().get(MultiChannelingStreamingCompression.class); + if (compression != null && compression.isActive()) { + System.out.println("Raknetify: Preventing vanilla compression as streaming compression is enabled"); + return -1; + } + return server.getCompressionThreshold(); + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/server/MixinServerNetworkIo.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/server/MixinServerNetworkIo.java new file mode 100644 index 00000000..d22c2e13 --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/server/MixinServerNetworkIo.java @@ -0,0 +1,186 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.server; + +import com.ishland.raknetify.common.Constants; +import com.ishland.raknetify.common.connection.RakNetConnectionUtil; +import com.ishland.raknetify.common.connection.RaknetifyEventLoops; +import com.ishland.raknetify.common.util.ThreadLocalUtil; +import com.ishland.raknetify.common.util.NetworkInterfaceListener; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import io.netty.bootstrap.AbstractBootstrap; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.FixedRecvByteBufAllocator; +import io.netty.channel.ServerChannel; +import io.netty.channel.epoll.Epoll; +import io.netty.channel.epoll.EpollDatagramChannel; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.ServerSocketChannel; +import io.netty.channel.socket.nio.NioDatagramChannel; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerConnectionListener; +import network.ycc.raknet.server.channel.RakNetServerChannel; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.SocketAddress; +import java.util.Iterator; +import java.util.List; +import java.util.function.Consumer; + +@Mixin(ServerConnectionListener.class) +public abstract class MixinServerNetworkIo { + + @Unique + private static final int raknetify$portOverride = Integer.getInteger("raknetify.fabric.portOverride", -1); + + @Shadow + @Final + private MinecraftServer server; + + @Shadow + public abstract void startTcpServerListener(@Nullable InetAddress address, int port) throws IOException; + + @Shadow + public volatile boolean running; + + @Shadow + @Final + private List channels; + @Unique + private Consumer raknetify$eventListener = null; + + @Inject(method = "startTcpServerListener", at = @At("HEAD")) + private void bindUdp(InetAddress address, int port, CallbackInfo ci) throws IOException { + if (!ThreadLocalUtil.isInitializingRaknet()) { + try { + ThreadLocalUtil.setInitializingRaknet(true); + final boolean hasPortOverride = raknetify$portOverride > 0 && raknetify$portOverride < 65535; + if (address == null) { + for (NetworkInterface networkInterface : NetworkInterface.networkInterfaces().toList()) { + final Iterator iterator = networkInterface.getInetAddresses().asIterator(); + while (iterator.hasNext()) { + final InetAddress inetAddress = iterator.next(); + System.out.println("Starting raknetify server on %s".formatted(inetAddress)); + try { + startTcpServerListener(inetAddress, hasPortOverride ? raknetify$portOverride : port); + } catch (IOException t) { + System.out.println("**** FAILED TO BIND TO PORT! %s".formatted(t.getMessage())); + } + } + } + + if (this.raknetify$eventListener == null) { + this.raknetify$eventListener = event -> { + if (!this.running) { + NetworkInterfaceListener.removeListener(this.raknetify$eventListener); + return; + } + try { + ThreadLocalUtil.setInitializingRaknet(true); + final InetAddress inetAddress = event.address(); + if (event.added()) { + System.out.println("Starting raknetify server on %s".formatted(inetAddress)); + try { + startTcpServerListener(inetAddress, hasPortOverride ? raknetify$portOverride : port); + } catch (IOException t) { + System.out.println("**** FAILED TO BIND TO PORT! %s".formatted(t.getMessage())); + } catch (Throwable t) { + t.printStackTrace(); + } + } else { + synchronized (this.channels) { + for (Iterator iter = this.channels.iterator(); iter.hasNext(); ) { + ChannelFuture channel = iter.next(); + final SocketAddress socketAddress = channel.channel().localAddress(); + if (socketAddress instanceof InetSocketAddress channelAddress) { + if (inetAddress.equals(channelAddress.getAddress())) { + System.out.println("Stopping raknetify server on %s".formatted(inetAddress)); + channel.channel().close(); + iter.remove(); + } + } + } + } + } + } catch (Throwable e) { + e.printStackTrace(); + } finally { + ThreadLocalUtil.setInitializingRaknet(false); + } + }; + NetworkInterfaceListener.addListener(event -> this.server.submit(() -> raknetify$eventListener.accept(event))); + } + } else { + System.out.println("Starting raknetify server on %s".formatted(address)); + startTcpServerListener(address, hasPortOverride ? raknetify$portOverride : port); + } + } finally { + ThreadLocalUtil.setInitializingRaknet(false); + } + } + } + + @WrapOperation(method = "startTcpServerListener", at = @At(value = "INVOKE", target = "Lio/netty/bootstrap/ServerBootstrap;group(Lio/netty/channel/EventLoopGroup;)Lio/netty/bootstrap/ServerBootstrap;", remap = false)) + private ServerBootstrap redirectGroup(ServerBootstrap instance, EventLoopGroup group, Operation original) { + final boolean useEpoll = Epoll.isAvailable() && this.server.useNativeTransport(); + return ThreadLocalUtil.isInitializingRaknet() + ? original.call(instance, useEpoll ? RaknetifyEventLoops.EPOLL_EVENT_LOOP_GROUP.get() : RaknetifyEventLoops.NIO_EVENT_LOOP_GROUP.get()) + : original.call(instance, group); + } + + @WrapOperation(method = "startTcpServerListener", at = @At(value = "INVOKE", target = "Lio/netty/bootstrap/ServerBootstrap;channel(Ljava/lang/Class;)Lio/netty/bootstrap/AbstractBootstrap;", remap = false)) + private AbstractBootstrap redirectChannel(ServerBootstrap instance, Class aClass, Operation> original) { + final boolean useEpoll = Epoll.isAvailable() && this.server.useNativeTransport(); + return ThreadLocalUtil.isInitializingRaknet() + ? instance.channelFactory(() -> { + RakNetServerChannel channel = new RakNetServerChannel(() -> { + final DatagramChannel channel1 = useEpoll ? new EpollDatagramChannel() : new NioDatagramChannel(); + channel1.config().setOption(ChannelOption.SO_REUSEADDR, true); + channel1.config().setOption(ChannelOption.IP_TOS, RakNetConnectionUtil.DEFAULT_IP_TOS); + channel1.config().setRecvByteBufAllocator(new FixedRecvByteBufAllocator(Constants.LARGE_MTU + 512).maxMessagesPerRead(128)); + return channel1; + }); + channel.setProvidedApplicationEventLoop(RaknetifyEventLoops.DEFAULT_EVENT_LOOP_GROUP.get().next()); + return channel; + }) + : original.call(instance, aClass); + } + +} diff --git a/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/server/MixinServerNetworkIo1.java b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/server/MixinServerNetworkIo1.java new file mode 100644 index 00000000..59d1416d --- /dev/null +++ b/fabric-26/src/main/java/com/ishland/raknetify/fabric/mixin/server/MixinServerNetworkIo1.java @@ -0,0 +1,48 @@ +/* + * This file is a part of the Raknetify project, licensed under MIT. + * + * Copyright (c) 2022-2025 ishland + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package com.ishland.raknetify.fabric.mixin.server; + +import com.ishland.raknetify.fabric.common.connection.RakNetFabricConnectionUtil; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(targets = "net/minecraft/server/network/ServerConnectionListener$1") +public abstract class MixinServerNetworkIo1 extends ChannelInitializer { + + @Inject(method = "initChannel(Lio/netty/channel/Channel;)V", at = @At("HEAD")) + private void onChannelInit(Channel channel, CallbackInfo ci) { + RakNetFabricConnectionUtil.initChannel(channel); + } + + @Inject(method = "initChannel(Lio/netty/channel/Channel;)V", at = @At("RETURN")) + private void postChannelInit(Channel channel, CallbackInfo ci) { + RakNetFabricConnectionUtil.postInitChannel(channel, false); + } + +} diff --git a/fabric-26/src/main/resources/fabric.mod.json b/fabric-26/src/main/resources/fabric.mod.json new file mode 100644 index 00000000..7c4e2880 --- /dev/null +++ b/fabric-26/src/main/resources/fabric.mod.json @@ -0,0 +1,39 @@ +{ + "schemaVersion": 1, + "id": "raknetify", + "version": "${version}", + + "name": "Raknetify (Fabric)", + "description": "A Fabric mod that allows using RakNet as Minecraft networking backend. ", + "authors": [ + "ishland" + ], + "contact": { + }, + + "license": "MIT", + + "environment": "*", + "entrypoints": { + "main": [ + "com.ishland.raknetify.fabric.RaknetifyFabric" + ], + "preLaunch": [ + "com.ishland.raknetify.fabric.RaknetifyFabric" + ] + }, + + "accessWidener" : "raknetify-fabric.accesswidener", + + "mixins": [ + "raknetify-fabric.mixins.json" + ], + + "depends": { + "fabricloader": "${loader}", + "minecraft": ["${minecraft}", "26.4-alpha.7"], + "java": "${java}" + }, + "suggests": { + } +} diff --git a/fabric-26/src/main/resources/raknetify-fabric.accesswidener b/fabric-26/src/main/resources/raknetify-fabric.accesswidener new file mode 100644 index 00000000..49d92342 --- /dev/null +++ b/fabric-26/src/main/resources/raknetify-fabric.accesswidener @@ -0,0 +1,4 @@ +accessWidener v1 official + +accessible class net/minecraft/network/codec/IdDispatchCodec$Entry +accessible method net/minecraft/network/CipherBase (Ljavax/crypto/Cipher;)V diff --git a/fabric-26/src/main/resources/raknetify-fabric.mixins.json b/fabric-26/src/main/resources/raknetify-fabric.mixins.json new file mode 100644 index 00000000..b92e22e3 --- /dev/null +++ b/fabric-26/src/main/resources/raknetify-fabric.mixins.json @@ -0,0 +1,46 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "com.ishland.raknetify.fabric.mixin", + "plugin": "com.ishland.raknetify.fabric.mixin.RaknetifyFabricMixinPlugin", + "compatibilityLevel": "JAVA_25", + "mixinPriority": 1010, + "mixins": [ + "access.IClientConnection", + "access.IPacketCodecDispatcher", + "access.IPacketEncryptionManager", + "access.IWorld", + "common.MixinCCConnect", + "common.MixinClientConnection", + "common.MixinClientConnection1", + "common.MixinClientConnection1_20_2", + "common.encryption.MixinClientConnection", + "common.encryption.MixinClientConnection26_4", + "common.encryption.MixinPacketDecryptor", + "common.encryption.MixinPacketEncryptionManager", + "common.encryption.MixinPacketEncryptor", + "common.quirks.MixinPlayerEntity", + "compat.fabricapi.MixinServerLoginNetworkAddon", + "compat.krypton.MixinServerLoginNetworkHandler", + "compat.krypton.MixinServerLoginNetworkHandler26_4", + "compat.qsl.MixinServerLoginNetworkAddon", + "server.MixinPlayerManager1_20_2", + "server.MixinServerCommonNetworkHandler", + "server.MixinServerLoginNetworkHandler", + "server.MixinServerNetworkIo", + "server.MixinServerNetworkIo1" + ], + "injectors": { + "defaultRequire": 1 + }, + "client": [ + "access.IDebugHudEntries", + "client.hud.MixinDebugHudEntries26_4", + "client.MixinMultiplayerServerListPinger1_21_11", + "common.MixinServerAddress", + "client.MixinClientPlayNetworkHandler", + "client.MixinConnectionScreen1", + "client.MixinMultiplayerServerListPinger", + "client.MixinMultiplayerServerListPinger1" + ] +} diff --git a/fabric/build.gradle b/fabric/build.gradle index 5bac3289..00be4118 100644 --- a/fabric/build.gradle +++ b/fabric/build.gradle @@ -5,6 +5,13 @@ apply plugin: 'com.gradleup.shadow' apply plugin: 'com.modrinth.minotaur' apply plugin: 'io.github.themrmilchmann.curseforge-publish' +def fabricSources = rootProject.file('fabric/src/main') + +sourceSets.main { + java.setSrcDirs([new File(fabricSources, 'java')]) + resources.setSrcDirs([new File(fabricSources, 'resources')]) +} + base { archivesName = project.archives_base_name + "-fabric" } @@ -14,6 +21,10 @@ configurations { shadowInclude } +repositories { + maven { url 'https://repo.codemc.io/repository/relativitymc/' } +} + dependencies { // To change the versions see the gradle.properties file minecraft "com.mojang:minecraft:${project.minecraft_version}" @@ -28,12 +39,47 @@ dependencies { processResources { inputs.property "version", project.version + inputs.property "minecraft", '>=1.18 <26' + inputs.property "java", '>=17' + inputs.property "loader", '>=0.15.2' filesMatching("fabric.mod.json") { - expand "version": project.version + expand "version": project.version, + "minecraft": inputs.properties.minecraft, + "java": inputs.properties.java, + "loader": inputs.properties.loader } } +def packetNames = tasks.register('generatePacketNames') { + def packetSource = new File(fabricSources, 'java/com/ishland/raknetify/fabric/common/connection/RakNetMultiChannel.java') + def output = layout.buildDirectory.file('generated/packetNames/raknetify-packet-names.properties') + inputs.file(packetSource) + inputs.file(provider { loom.mappingConfiguration.tinyMappings.toFile() }) + outputs.file(output) + doLast { + def classes = (packetSource.text =~ 'net/minecraft/class_[0-9_]+(?:\\$class_[0-9_]+)*').collect().toSet() + def names = new TreeMap>() + def mappingFiles = [loom.mappingConfiguration.tinyMappings.toFile()] + mappingFiles.each { mappingFile -> + mappingFile.eachLine('UTF-8') { line -> + def fields = line.split('\t') + if (fields[0] == 'c' && classes.contains(fields[2])) { + def key = fields[2].replace('/', '.') + if (!names.containsKey(key)) names[key] = new TreeSet() + names[key].add(fields[1].replace('/', '.')) + } + } + } + output.get().asFile.with { parentFile.mkdirs() } + output.get().asFile.setText(names.collect { key, value -> "$key=${value.join(',')}\n" }.join(), 'UTF-8') + } +} + +processResources { + from(packetNames) +} + tasks.withType(JavaCompile).configureEach { // Minecraft 1.18 (1.18-pre2) upwards uses Java 17. it.options.release = 17 @@ -47,7 +93,7 @@ java { } loom { - accessWidenerPath = file("src/main/resources/raknetify-fabric.accesswidener") + accessWidenerPath = new File(fabricSources, 'resources/raknetify-fabric.accesswidener') runs { server { vmArgs "-Draknetify.debug=true", "-Dio.netty.leakDetection.level=advanced" @@ -82,7 +128,7 @@ shadowJar { } //noinspection UnnecessaryQualifiedReference -task("remapShadowJar", type: net.fabricmc.loom.task.RemapJarTask, dependsOn: shadowJar) { +task("remapShadowJar", type: rootProject.remapJarTaskType, dependsOn: shadowJar) { input = shadowJar.archiveFile archiveFileName = shadowJar.archiveFileName.get().replaceAll("-dev\\.jar\$", ".jar") addNestedDependencies = true @@ -147,13 +193,13 @@ curseforge { gameVersion("minecraft-1-21", "1-21-9") gameVersion("minecraft-1-21", "1-21-10") gameVersion("minecraft-1-21", "1-21-11") - gameVersion("modloader", "fabric") gameVersion("java", "java-22") gameVersion("java", "java-21") gameVersion("java", "java-20") gameVersion("java", "java-19") gameVersion("java", "java-18") gameVersion("java", "java-17") + gameVersion("modloader", "fabric") gameVersion("environment", "server") gameVersion("environment", "client") @@ -161,10 +207,10 @@ curseforge { from(remapShadowJar) displayName = project.version + " devbuild" - releaseType = io.github.themrmilchmann.gradle.publish.curseforge.ReleaseType.ALPHA + releaseType = rootProject.curseforgeAlpha changelog { - format = io.github.themrmilchmann.gradle.publish.curseforge.ChangelogFormat.MARKDOWN + format = rootProject.curseforgeMarkdown from(com.ishland.buildscript.ParseGItHubActionChangelog.getChangelog()) } } diff --git a/fabric/src/main/java/com/ishland/raknetify/fabric/RaknetifyFabric.java b/fabric/src/main/java/com/ishland/raknetify/fabric/RaknetifyFabric.java index 53c3e53f..9753b64e 100644 --- a/fabric/src/main/java/com/ishland/raknetify/fabric/RaknetifyFabric.java +++ b/fabric/src/main/java/com/ishland/raknetify/fabric/RaknetifyFabric.java @@ -226,6 +226,11 @@ record FieldNodeClazzPair(FieldNode fieldNode, Class clazz) { .setPrettyPrinting() .create(); ProtocolMultiChannelMappings mappings = new ProtocolMultiChannelMappings(); + // Preserve proxy mappings for versions no longer supported by Fabric. + for (int protocol : new int[]{755, 756}) { + final ProtocolMultiChannelMappings.VersionMapping retained = ProtocolMultiChannelMappings.INSTANCE.mappings.get(protocol); + if (retained != null) mappings.mappings.put(protocol, retained); + } Path path = Path.of("channelMappings.json"); try { mappings = gson.fromJson(Files.readString(path), ProtocolMultiChannelMappings.class); diff --git a/fabric/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetMultiChannel.java b/fabric/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetMultiChannel.java index 89f04a06..ec956ce2 100644 --- a/fabric/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetMultiChannel.java +++ b/fabric/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetMultiChannel.java @@ -29,7 +29,10 @@ import net.fabricmc.loader.api.FabricLoader; import net.fabricmc.loader.api.MappingResolver; +import java.io.IOException; +import java.io.InputStream; import java.util.HashSet; +import java.util.Properties; import java.util.Set; public class RakNetMultiChannel { @@ -37,21 +40,34 @@ public class RakNetMultiChannel { private RakNetMultiChannel() { } + private static final Properties packetNames = loadPacketNames(); + + private static Properties loadPacketNames() { + final Properties names = new Properties(); + try (InputStream input = RakNetMultiChannel.class.getResourceAsStream("/raknetify-packet-names.properties")) { + if (input != null) names.load(input); + } catch (IOException e) { + throw new IllegalStateException("Failed to load packet names", e); + } + return names; + } + private static Set> createClassSet(String[] classNames) { final MappingResolver mappingResolver = FabricLoader.getInstance().getMappingResolver(); final boolean isIntermediaryNamespace = mappingResolver.getCurrentRuntimeNamespace().equals("intermediary"); final HashSet> classHashSet = new HashSet<>(); for (String className : classNames) { - final String intermediary = mappingResolver.mapClassName("intermediary", className.replace('/', '.')); - if (!isIntermediaryNamespace && intermediary.equals(className)) - //noinspection RedundantStringFormatCall - System.err.println("Raknetify: Warning: Failed to remap %s for raknet multi-channel".formatted(intermediary)); - try { - classHashSet.add(Class.forName(intermediary, false, RakNetMultiChannel.class.getClassLoader())); - } catch (ClassNotFoundException e) { - if (Boolean.getBoolean("raknetify.printNotFoundClasses")) { - //noinspection RedundantStringFormatCall - System.out.println("Raknetify: Warning: %s not found for raknet multi-channel".formatted(intermediary)); + final String sourceName = className.replace('/', '.'); + final String mappedName = mappingResolver.mapClassName("intermediary", sourceName); + final String names = mappedName.equals(sourceName) && !isIntermediaryNamespace + ? packetNames.getProperty(sourceName, mappedName) : mappedName; + for (String name : names.split(",")) { + try { + classHashSet.add(Class.forName(name, false, RakNetMultiChannel.class.getClassLoader())); + } catch (ClassNotFoundException e) { + if (Boolean.getBoolean("raknetify.printNotFoundClasses")) { + System.out.println("Raknetify: Warning: %s not found for raknet multi-channel".formatted(name)); + } } } } @@ -89,6 +105,9 @@ private static Set> createClassSet(String[] classNames) { // Primarily used for interactions independent to world private static final Set> channel1 = createClassSet(new String[]{ + "net/minecraft/class_1_71", // LowDiskSpaceWarningS2CPacket + "net/minecraft/class_1_130", // SetGameRuleC2SPacket + "net/minecraft/class_1_132", // GameRuleValuesS2CPacket "net/minecraft/class_2629", // BossBarS2C "net/minecraft/class_5888", // ClearTitleS2C "net/minecraft/class_5903", // SubtitleS2C @@ -181,6 +200,9 @@ private static Set> createClassSet(String[] classNames) { // Entities related stuff private static final Set> channel2 = createClassSet(new String[]{ + "net/minecraft/class_1_126", // AttackC2SPacket + "net/minecraft/class_1_1438", // HandSwingC2SPacket + "net/minecraft/class_1_1444", // SwingAnimationS2CPacket "net/minecraft/class_5890", // EndCombatS2C "net/minecraft/class_5891", // EnterCombatS2C "net/minecraft/class_2716", // EntitiesDestroyS2C @@ -250,6 +272,7 @@ private static Set> createClassSet(String[] classNames) { // Primarily used for interactions dependent to world private static final Set> channel3 = createClassSet(new String[]{ + "net/minecraft/class_1_133", // SpectateEntityC2SPacket "net/minecraft/class_2884", // SpectatorTeleportC2S "net/minecraft/class_2793", // TeleportConfirmC2S @@ -320,6 +343,7 @@ private static Set> createClassSet(String[] classNames) { // Primarily for packets not very critical to interactions private static final Set> channel4 = createClassSet(new String[]{ + "net/minecraft/class_1_1290", // PostEffectsS2CPacket "net/minecraft/class_2683", // MapUpdateS2C "net/minecraft/class_2660", // PlaySoundIdS2C "net/minecraft/class_2765", // PlaySoundFromEntityS2C @@ -332,6 +356,7 @@ private static Set> createClassSet(String[] classNames) { // Used for worlds private static final Set> channel7 = createClassSet(new String[]{ + "net/minecraft/class_1_1680", // AddTransientBlockS2CPacket "net/minecraft/class_5194", // JigsawGeneratingC2S "net/minecraft/class_2693", // SignEditorOpenS2C "net/minecraft/class_2877", // UpdateSignC2S diff --git a/fabric/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetNetworkTransitionUtil.java b/fabric/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetNetworkTransitionUtil.java index c43ac423..47bb967c 100644 --- a/fabric/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetNetworkTransitionUtil.java +++ b/fabric/src/main/java/com/ishland/raknetify/fabric/common/connection/RakNetNetworkTransitionUtil.java @@ -30,9 +30,15 @@ public class RakNetNetworkTransitionUtil { static Object handleTransition(Object msg) { if (msg instanceof NetworkStateTransitions.DecoderTransitioner transitioner) { - return transitioner.andThen(context -> RakNetFabricConnectionUtil.onPipelineReorder(context.pipeline())); + return (NetworkStateTransitions.DecoderTransitioner) context -> { + transitioner.run(context); + RakNetFabricConnectionUtil.onPipelineReorder(context.pipeline()); + }; } else if (msg instanceof NetworkStateTransitions.EncoderTransitioner transitioner) { - return transitioner.andThen(context -> RakNetFabricConnectionUtil.onPipelineReorder(context.pipeline())); + return (NetworkStateTransitions.EncoderTransitioner) context -> { + transitioner.run(context); + RakNetFabricConnectionUtil.onPipelineReorder(context.pipeline()); + }; } return msg; } diff --git a/fabric/src/main/java/com/ishland/raknetify/fabric/common/util/MultiVersionUtil.java b/fabric/src/main/java/com/ishland/raknetify/fabric/common/util/MultiVersionUtil.java index 553ef0e2..bb57ffcb 100644 --- a/fabric/src/main/java/com/ishland/raknetify/fabric/common/util/MultiVersionUtil.java +++ b/fabric/src/main/java/com/ishland/raknetify/fabric/common/util/MultiVersionUtil.java @@ -62,6 +62,7 @@ public class MultiVersionUtil { public static final VarHandle ServerPlayNetworkHandler$connection; public static final VarHandle ClientPlayNetworkHandler$connection; + private static final VarHandle ClientConnection$encrypted; public static final VarHandle ServerPlayerEntity$pingMillis1_20_1; public static final Class clazzNetworkStatePacketHandler; public static final VarHandle NetworkStatePacketHandler$backingHandler1_20_2; @@ -77,6 +78,11 @@ public class MultiVersionUtil { try { final MappingResolver resolver = FabricLoader.getInstance().getMappingResolver(); + final Field encrypted = getOrNull(() -> ClientConnection.class.getDeclaredField( + resolver.mapFieldName(INTERMEDIARY, "net.minecraft.class_2535", "field_11647", "Z")), NoSuchFieldException.class); + ClientConnection$encrypted = encrypted == null ? null : MethodHandles + .privateLookupIn(ClientConnection.class, MethodHandles.lookup()).unreflectVarHandle(encrypted); + { final List connFields = tryLocateFields(ServerPlayNetworkHandler.class, ClientConnection.class, false); if (connFields.size() != 1) { @@ -254,6 +260,12 @@ private static List tryLocateFields(Class clazz, Class fieldType, b public static void init() { } + public static void setEncrypted(ClientConnection connection, boolean encrypted) { + if (ClientConnection$encrypted != null) { + ClientConnection$encrypted.set(connection, encrypted); + } + } + public static Class tryLocateClass(String name) { try { return Class.forName(name); diff --git a/fabric/src/main/java/com/ishland/raknetify/fabric/mixin/RaknetifyFabricMixinPlugin.java b/fabric/src/main/java/com/ishland/raknetify/fabric/mixin/RaknetifyFabricMixinPlugin.java index 50b5194d..22f1924e 100644 --- a/fabric/src/main/java/com/ishland/raknetify/fabric/mixin/RaknetifyFabricMixinPlugin.java +++ b/fabric/src/main/java/com/ishland/raknetify/fabric/mixin/RaknetifyFabricMixinPlugin.java @@ -103,6 +103,8 @@ public boolean shouldApplyMixin(String targetClassName, String mixinClassName) { return !AFTER_1_20_1; if (mixinClassName.equals("com.ishland.raknetify.fabric.mixin.access.INetworkState1_20_4")) return !AFTER_1_20_4; + if (mixinClassName.equals("com.ishland.raknetify.fabric.mixin.access.INetworkStateInternalPacketHandler")) + return !AFTER_1_20_4; return true; } diff --git a/fabric/src/main/java/com/ishland/raknetify/fabric/mixin/access/IClientConnection.java b/fabric/src/main/java/com/ishland/raknetify/fabric/mixin/access/IClientConnection.java index 05253d97..01fff8ac 100644 --- a/fabric/src/main/java/com/ishland/raknetify/fabric/mixin/access/IClientConnection.java +++ b/fabric/src/main/java/com/ishland/raknetify/fabric/mixin/access/IClientConnection.java @@ -35,7 +35,4 @@ public interface IClientConnection { @Accessor Channel getChannel(); - @Accessor - void setEncrypted(boolean encrypted); - } diff --git a/fabric/src/main/java/com/ishland/raknetify/fabric/mixin/common/encryption/MixinClientConnection.java b/fabric/src/main/java/com/ishland/raknetify/fabric/mixin/common/encryption/MixinClientConnection.java index 1ce35aa1..255ac128 100644 --- a/fabric/src/main/java/com/ishland/raknetify/fabric/mixin/common/encryption/MixinClientConnection.java +++ b/fabric/src/main/java/com/ishland/raknetify/fabric/mixin/common/encryption/MixinClientConnection.java @@ -26,6 +26,7 @@ import com.ishland.raknetify.common.connection.MultiChannelingStreamingCompression; import com.ishland.raknetify.common.connection.MultiChannellingEncryption; +import com.ishland.raknetify.fabric.common.util.MultiVersionUtil; import io.netty.channel.Channel; import net.minecraft.network.ClientConnection; import net.minecraft.network.encryption.PacketDecryptor; @@ -44,9 +45,6 @@ @Mixin(ClientConnection.class) public class MixinClientConnection { - @Shadow - private boolean encrypted; - @Shadow private Channel channel; @@ -54,7 +52,7 @@ public class MixinClientConnection { public void beforeSetupEncryption(Cipher decryptionCipher, Cipher encryptionCipher, CallbackInfo ci) { if (this.channel.config() instanceof RakNet.Config) { ci.cancel(); - this.encrypted = true; + MultiVersionUtil.setEncrypted((ClientConnection) (Object) this, true); try { this.channel.pipeline().remove("decrypt"); this.channel.pipeline().remove("encrypt"); diff --git a/fabric/src/main/java/com/ishland/raknetify/fabric/mixin/compat/krypton/MixinServerLoginNetworkHandler.java b/fabric/src/main/java/com/ishland/raknetify/fabric/mixin/compat/krypton/MixinServerLoginNetworkHandler.java index 29d07b53..ce9a80f2 100644 --- a/fabric/src/main/java/com/ishland/raknetify/fabric/mixin/compat/krypton/MixinServerLoginNetworkHandler.java +++ b/fabric/src/main/java/com/ishland/raknetify/fabric/mixin/compat/krypton/MixinServerLoginNetworkHandler.java @@ -25,6 +25,7 @@ package com.ishland.raknetify.fabric.mixin.compat.krypton; import com.ishland.raknetify.fabric.mixin.access.IClientConnection; +import com.ishland.raknetify.fabric.common.util.MultiVersionUtil; import io.netty.channel.ChannelHandler; import io.netty.channel.ChannelPipeline; import net.minecraft.network.ClientConnection; @@ -76,7 +77,7 @@ private void afterSetupEncryption(LoginKeyC2SPacket packet, CallbackInfo ci) thr Cipher cipher = NetworkEncryptionUtils.cipherFromKey(2, secretKey); Cipher cipher2 = NetworkEncryptionUtils.cipherFromKey(1, secretKey); - ((IClientConnection) this.connection).setEncrypted(false); + MultiVersionUtil.setEncrypted(this.connection, false); this.connection.setupEncryption(cipher, cipher2); } } diff --git a/fabric/src/main/resources/fabric.mod.json b/fabric/src/main/resources/fabric.mod.json index 8e00798d..d386fb66 100644 --- a/fabric/src/main/resources/fabric.mod.json +++ b/fabric/src/main/resources/fabric.mod.json @@ -30,8 +30,9 @@ ], "depends": { - "fabricloader": ">=0.15.2", - "java": ">=17" + "fabricloader": "${loader}", + "minecraft": "${minecraft}", + "java": "${java}" }, "suggests": { } diff --git a/genMappings.sh b/genMappings.sh index fa7d6197..d6cb662e 100755 --- a/genMappings.sh +++ b/genMappings.sh @@ -1,11 +1,11 @@ #!/bin/bash -loader_version=0.18.4 +loader_version=0.20.1 installer_version=1.1.1 #declare -a vers if [[ -z "$@" ]]; then - vers="1.17 1.17.1 1.18.1 1.18.2 1.19 1.19.2 1.19.3 1.19.4 23w13a_or_b 1.20.1 1.20.2 1.20.4 1.20.6 1.21.1 1.21.3 1.21.4 1.21.5 1.21.6 1.21.8 1.21.10 1.21.11" + vers="1.18.1 1.18.2 1.19 1.19.2 1.19.3 1.19.4 23w13a_or_b 1.20.1 1.20.2 1.20.4 1.20.6 1.21.1 1.21.3 1.21.4 1.21.5 1.21.6 1.21.8 1.21.10 1.21.11 26.1 26.1.1 26.1.2 26.2 26.3 26.4-snapshot-7" else vers=$@ fi @@ -18,7 +18,6 @@ mkdir run-mappingsGen || true cd run-mappingsGen || exit 1 rm -r mods mkdir mods -cp ../fabric/build/libs/raknetify-*-all.jar mods/ rm channelMappings.json || true if [[ -z "${MAPPINGS_GEN_FRESH}" ]]; then cp ../common/src/main/resources/raknetify-channel-mappings.json channelMappings.json @@ -26,15 +25,28 @@ fi for version in $vers do - if [ -e fabric-server-mc"$version".jar ]; then - echo "Using existing fabric-server-mc$version.jar" + rm -f mods/raknetify-*-all.jar + if [[ "$version" == 26.* ]]; then + cp ../fabric-26/build/libs/raknetify-*-all.jar mods/ || exit 1 else - wget -O fabric-server-mc"$version".jar https://meta.fabricmc.net/v2/versions/loader/"$version"/$loader_version/$installer_version/server/jar || exit 1 + cp ../fabric/build/libs/raknetify-*-all.jar mods/ || exit 1 + fi + + launcher="fabric-server-mc${version}-loader${loader_version}-installer${installer_version}.jar" + if [ -e "$launcher" ]; then + echo "Using existing $launcher" + else + wget -O "$launcher" https://meta.fabricmc.net/v2/versions/loader/"$version"/$loader_version/$installer_version/server/jar || exit 1 fi sleep 1 - java -Draknetify.saveChannelMappings=true -Draknetify.saveChannelMappings.exit=true -jar fabric-server-mc"$version".jar || exit 1 + touch .mapping-generation-start + java -Draknetify.saveChannelMappings=true -Draknetify.saveChannelMappings.exit=true -jar "$launcher" || exit 1 + if [ ! channelMappings.json -nt .mapping-generation-start ]; then + echo "Mapping generation did not write output for $version" + exit 1 + fi done cp channelMappings.json ../common/src/main/resources/raknetify-channel-mappings.json diff --git a/gradle.properties b/gradle.properties index 5ad7b7d1..aac5eaf8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,8 @@ gradle-curseforge-publish.interop.neogradle=false # Also don't forget to gen mappings in genMappings.sh minecraft_version=1.21.11 yarn_mappings=1.21.11+build.1 -loader_version=0.17.2 +modern_minecraft_version=26.3 +loader_version=0.19.5 # Mod Properties mod_version = 0.1.0+alpha.5 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7b0b78c6..52ad5e71 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle b/settings.gradle index fb9b7caa..47cc5007 100644 --- a/settings.gradle +++ b/settings.gradle @@ -20,6 +20,7 @@ includeBuild('netty-raknet') { } include "fabric" +include "fabric-26" include "velocity" include "bungee" include "common" diff --git a/velocity/build.gradle b/velocity/build.gradle index d705ccb7..e70e9454 100644 --- a/velocity/build.gradle +++ b/velocity/build.gradle @@ -24,9 +24,9 @@ repositories { url 'https://repo.papermc.io/repository/maven-public/' } ivy { - url 'https://api.papermc.io/v2/projects/' + url 'https://fill-data.papermc.io/v1/objects/af292ea5c9c330fc3b367e48c7d1d448b1b3216f597798bfbef662d002b1343b/' patternLayout { - artifact "[organization]/versions/[module]/builds/[revision]/downloads/[organization]-[module]-[revision](.[ext])" + artifact "[organization]-[module]-[revision](.[ext])" } metadataSources { it.artifact()