diff --git a/orebfuscator-plugin/pom.xml b/orebfuscator-plugin/pom.xml index 6925b175..75b7dbba 100644 --- a/orebfuscator-plugin/pom.xml +++ b/orebfuscator-plugin/pom.xml @@ -67,6 +67,12 @@ ${dependency.netty.version} provided + + io.netty + netty-transport + ${dependency.netty.version} + provided + org.spigotmc spigot-api diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/Orebfuscator.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/Orebfuscator.java index 6522c2c6..0660eab8 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/Orebfuscator.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/Orebfuscator.java @@ -13,6 +13,7 @@ import net.imprex.orebfuscator.api.OrebfuscatorService; import net.imprex.orebfuscator.cache.ObfuscationCache; import net.imprex.orebfuscator.config.OrebfuscatorConfig; +import net.imprex.orebfuscator.injector.OrebfuscatorInjectorManager; import net.imprex.orebfuscator.obfuscation.ObfuscationSystem; import net.imprex.orebfuscator.player.OrebfuscatorPlayerMap; import net.imprex.orebfuscator.proximity.ProximityDirectorThread; @@ -31,6 +32,7 @@ public class Orebfuscator extends JavaPlugin implements Listener { private UpdateSystem updateSystem; private ObfuscationCache obfuscationCache; private ObfuscationSystem obfuscationSystem; + private OrebfuscatorInjectorManager injectorManager; private ProximityDirectorThread proximityThread; private ProximityPacketListener proximityPacketListener; @@ -85,6 +87,7 @@ public void onEnable() { // Load packet listener this.obfuscationSystem.registerChunkListener(); + this.injectorManager = new OrebfuscatorInjectorManager(this); // Store formatted config this.config.store(); @@ -120,6 +123,10 @@ public void onDisable() { this.proximityThread.close(); } + if (this.injectorManager != null) { + this.injectorManager.close(); + } + OrebfuscatorCompatibility.close(); OrebfuscatorNms.close(); diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/OrebfuscatorStatistics.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/OrebfuscatorStatistics.java index d786c008..fdff7044 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/OrebfuscatorStatistics.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/OrebfuscatorStatistics.java @@ -1,13 +1,18 @@ package net.imprex.orebfuscator; import java.util.Objects; -import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.LongAdder; +import java.util.function.DoubleSupplier; import java.util.function.LongSupplier; import com.google.gson.JsonObject; public class OrebfuscatorStatistics { + private static final long MICRO_SCALE = 1000L; + private static final long MILLI_SCALE = 1000L * MICRO_SCALE; + private static final long SECOND_SCALE = 1000L * MILLI_SCALE; + private static String formatPrecent(double percent) { return String.format("%.2f%%", percent * 100); } @@ -34,32 +39,45 @@ private static String formatBytes(long bytes) { } } - private final AtomicLong cacheHitCountMemory = new AtomicLong(0); - private final AtomicLong cacheHitCountDisk = new AtomicLong(0); - private final AtomicLong cacheMissCount = new AtomicLong(0); - private final AtomicLong cacheEstimatedSize = new AtomicLong(0); + private static String formatNanos(double nanos) { + if (nanos > SECOND_SCALE) { + return String.format("%.2f s", nanos / SECOND_SCALE); + } else if (nanos > MILLI_SCALE) { + return String.format("%.2f ms", nanos / MILLI_SCALE); + } else if (nanos > MICRO_SCALE) { + return String.format("%.2f µs", nanos / MICRO_SCALE); + } else { + return String.format("%d ns", nanos); + } + } + + private final LongAdder cacheHitCountMemory = new LongAdder(); + private final LongAdder cacheHitCountDisk = new LongAdder(); + private final LongAdder cacheMissCount = new LongAdder(); + private final LongAdder cacheEstimatedSize = new LongAdder(); private LongSupplier memoryCacheSize = () -> 0; private LongSupplier diskCacheQueueLength = () -> 0; private LongSupplier obfuscationQueueLength = () -> 0; + private DoubleSupplier averagePacketDelay = () -> 0; private LongSupplier obfuscationWaitTime = () -> 0; private LongSupplier obfuscationProcessTime = () -> 0; private LongSupplier proximityWaitTime = () -> 0; private LongSupplier proximityProcessTime = () -> 0; public void onCacheHitMemory() { - this.cacheHitCountMemory.incrementAndGet(); + this.cacheHitCountMemory.increment(); } public void onCacheHitDisk() { - this.cacheHitCountDisk.incrementAndGet(); + this.cacheHitCountDisk.increment(); } public void onCacheMiss() { - this.cacheMissCount.incrementAndGet(); + this.cacheMissCount.increment(); } public void onCacheSizeChange(int delta) { - this.cacheEstimatedSize.addAndGet(delta); + this.cacheEstimatedSize.add(delta); } public void setMemoryCacheSizeSupplier(LongSupplier supplier) { @@ -74,6 +92,10 @@ public void setObfuscationQueueLengthSupplier(LongSupplier supplier) { this.obfuscationQueueLength = Objects.requireNonNull(supplier); } + public void setAveragePacketDelay(DoubleSupplier supplier) { + this.averagePacketDelay = Objects.requireNonNull(supplier); + } + public void setObfuscationWaitTime(LongSupplier supplier) { this.obfuscationWaitTime = Objects.requireNonNull(supplier); } @@ -92,13 +114,14 @@ public void setProximityProcessTime(LongSupplier supplier) { @Override public String toString() { - long cacheHitCountMemory = this.cacheHitCountMemory.get(); - long cacheHitCountDisk = this.cacheHitCountDisk.get(); - long cacheMissCount = this.cacheMissCount.get(); - long cacheEstimatedSize = this.cacheEstimatedSize.get(); + long cacheHitCountMemory = this.cacheHitCountMemory.sum(); + long cacheHitCountDisk = this.cacheHitCountDisk.sum(); + long cacheMissCount = this.cacheMissCount.sum(); + long cacheEstimatedSize = this.cacheEstimatedSize.sum(); long memoryCacheSize = this.memoryCacheSize.getAsLong(); long diskCacheQueueLength = this.diskCacheQueueLength.getAsLong(); long obfuscationQueueLength = this.obfuscationQueueLength.getAsLong(); + double averagePacketDelay = this.averagePacketDelay.getAsDouble(); double totalCacheRequest = (double) (cacheHitCountMemory + cacheHitCountDisk + cacheMissCount); @@ -123,6 +146,7 @@ public String toString() { builder.append(" - memoryCacheEntries: ").append(memoryCacheSize).append('\n'); builder.append(" - diskCacheQueueLength: ").append(diskCacheQueueLength).append('\n'); builder.append(" - obfuscationQueueLength: ").append(obfuscationQueueLength).append('\n'); + builder.append(" - averagePacketDelay: ").append(formatNanos(averagePacketDelay)).append('\n'); long obfuscationWaitTime = this.obfuscationWaitTime.getAsLong(); long obfuscationProcessTime = this.obfuscationProcessTime.getAsLong(); @@ -158,13 +182,14 @@ public String toString() { public JsonObject toJson() { JsonObject object = new JsonObject(); - object.addProperty("cacheHitCountMemory", this.cacheHitCountMemory.get()); - object.addProperty("cacheHitCountDisk", this.cacheHitCountDisk.get()); - object.addProperty("cacheMissCount", this.cacheMissCount.get()); - object.addProperty("cacheEstimatedSize", this.cacheEstimatedSize.get()); + object.addProperty("cacheHitCountMemory", this.cacheHitCountMemory.sum()); + object.addProperty("cacheHitCountDisk", this.cacheHitCountDisk.sum()); + object.addProperty("cacheMissCount", this.cacheMissCount.sum()); + object.addProperty("cacheEstimatedSize", this.cacheEstimatedSize.sum()); object.addProperty("memoryCacheSize", this.memoryCacheSize.getAsLong()); object.addProperty("diskCacheQueueLength", this.diskCacheQueueLength.getAsLong()); object.addProperty("obfuscationQueueLength", this.obfuscationQueueLength.getAsLong()); + object.addProperty("averagePacketDelayNano", this.averagePacketDelay.getAsDouble()); object.addProperty("obfuscationWaitTime", this.obfuscationWaitTime.getAsLong()); object.addProperty("obfuscationProcessTime", this.obfuscationProcessTime.getAsLong()); object.addProperty("proximityWaitTime", this.proximityWaitTime.getAsLong()); diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/injector/AsyncOutboundPacketHandler.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/injector/AsyncOutboundPacketHandler.java new file mode 100644 index 00000000..917d5f66 --- /dev/null +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/injector/AsyncOutboundPacketHandler.java @@ -0,0 +1,146 @@ +package net.imprex.orebfuscator.injector; + +import java.util.ArrayDeque; +import java.util.ConcurrentModificationException; +import java.util.Queue; + +import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.PacketType.Protocol; +import com.comphenix.protocol.injector.packet.PacketRegistry; +import com.comphenix.protocol.utility.MinecraftReflection; + +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelOutboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.channel.EventLoop; +import io.netty.util.concurrent.Promise; +import net.imprex.orebfuscator.util.OFCLogger; + +public class AsyncOutboundPacketHandler extends ChannelOutboundHandlerAdapter { + + private final OrebfuscatorInjector injector; + private final Queue pendingWrites = new ArrayDeque<>(); + + private ChannelHandlerContext context; + + public AsyncOutboundPacketHandler(OrebfuscatorInjector injector) { + this.injector = injector; + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + this.context = ctx; + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + Promise task = null; + + PacketType packetType = getPacketType(msg); + if (packetType != null && this.injector.hasOutboundAsyncListener(packetType)) { + // process packet async if we have any listeners + EventLoop eventLoop = ctx.channel().eventLoop(); + task = this.injector.processOutboundAsync(packetType, msg, eventLoop); + + if (task != null) { + // we can just call flush on completion as netty calls the listener on the + // channel promise (channel) event-loop + task.addListener(future -> this.flushWriteQueue()); + } + } + + if (task != null || !this.pendingWrites.isEmpty()) { + // we also need to delay any other tasks as the en-/decoder is configured by + // a runnable that is written to the channels pipeline + this.pendingWrites.offer(new PendingWrite(msg, promise, task)); + } else { + // write if we don't wait on any previous message + ctx.write(msg, promise); + } + } + + private PacketType getPacketType(Object msg) { + if (!MinecraftReflection.isPacketClass(msg)) { + return null; + } + + PacketType.Protocol protocol = this.injector.getOutboundProtocol(); + if (protocol == Protocol.UNKNOWN) { + OFCLogger.debug("skipping unknown outbound protocol for " + msg.getClass()); + return null; + } + + PacketType packetType = PacketRegistry.getPacketType(protocol, msg.getClass()); + if (packetType == null) { + OFCLogger.debug("skipping unknown outbound packet type for " + msg.getClass()); + return null; + } + + return packetType; + } + + private void flushWriteQueue() { + if (!this.context.executor().inEventLoop()) { + this.context.executor().execute(this::flushWriteQueue); + return; + } + + while (!this.pendingWrites.isEmpty()) { + PendingWrite head = this.pendingWrites.peek(); + if (!head.isDone()) { + return; + } + + if (this.pendingWrites.poll() != head) { + // paranoia check; this should never happen + throw new ConcurrentModificationException(); + } + + head.write(); + } + } + + private class PendingWrite { + + private final long timestamp = System.nanoTime(); + + private Object message; + private final ChannelPromise promise; + private final Promise task; + + public PendingWrite(Object message, ChannelPromise promise, Promise task) { + this.message = message; + this.promise = promise; + this.task = task; + } + + public boolean isDone() { + return task == null || task.isDone(); + } + + public void write() { + if (task != null) { + // packet got cancel; don't write anything + if (task.isCancelled()) { + return; + } else if (task.cause() != null) { + OFCLogger.error("An unknown error occurred while processing outbound packet async: " + + this.message.getClass(), task.cause()); + } else { + Object message = task.getNow(); + if (message != null) { + this.message = message; + } else { + OFCLogger.warn("Async packet processing returned NULL, that shouldn't happen" + task); + } + } + } + + injector.logPacketDelay(System.nanoTime() - timestamp); + + if (!context.isRemoved() && context.channel().isOpen()) { + context.write(this.message, this.promise); + } + } + } +} diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/injector/ChannelProtocolUtil.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/injector/ChannelProtocolUtil.java new file mode 100644 index 00000000..12b51f72 --- /dev/null +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/injector/ChannelProtocolUtil.java @@ -0,0 +1,212 @@ +/* + * This code is adapted from the ProtocolLib project: + * https://github.com/dmulloy2/ProtocolLib/blob/804aa5df11de975337b6947dd59528b432756d20/src/main/java/com/comphenix/protocol/injector/netty/channel/ChannelProtocolUtil.java + * Copyright (C) 2025 ProtocolLib Project (original authors and contributors) + * Licensed under the GNU General Public License v2.0 (GPLv2) + */ +package net.imprex.orebfuscator.injector; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; + +import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.accessors.Accessors; +import com.comphenix.protocol.reflect.accessors.FieldAccessor; +import com.comphenix.protocol.reflect.accessors.MethodAccessor; +import com.comphenix.protocol.reflect.fuzzy.FuzzyFieldContract; +import com.comphenix.protocol.utility.MinecraftReflection; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.util.AttributeKey; + +@SuppressWarnings("unchecked") +final class ChannelProtocolUtil { + + public static final BiFunction PROTOCOL_RESOLVER; + + static { + Class networkManagerClass = MinecraftReflection.getNetworkManagerClass(); + List attributeKeys = FuzzyReflection.fromClass(networkManagerClass, true).getFieldList(FuzzyFieldContract.newBuilder() + .typeExact(AttributeKey.class) + .requireModifier(Modifier.STATIC) + .declaringClassExactType(networkManagerClass) + .build()); + + BiFunction baseResolver = null; + if (attributeKeys.isEmpty()) { + // since 1.20.5 the protocol is stored as final field in de-/encoder + baseResolver = new Post1_20_5WrappedResolver(); + } else if (attributeKeys.size() == 1) { + // if there is only one attribute key we can assume it's the correct one (1.8 - 1.20.1) + Object protocolKey = Accessors.getFieldAccessor(attributeKeys.get(0)).get(null); + baseResolver = new Pre1_20_2DirectResolver((AttributeKey) protocolKey); + } else if (attributeKeys.size() > 1) { + // most likely 1.20.2+: 1 protocol key per protocol direction + AttributeKey serverBoundKey = null; + AttributeKey clientBoundKey = null; + + for (Field keyField : attributeKeys) { + AttributeKey key = (AttributeKey) Accessors.getFieldAccessor(keyField).get(null); + if (key.name().equals("protocol")) { + // legacy (pre 1.20.2 name) - fall back to the old behaviour + baseResolver = new Pre1_20_2DirectResolver(key); + break; + } + + if (key.name().contains("protocol")) { + // one of the two protocol keys for 1.20.2 + if (key.name().contains("server")) { + serverBoundKey = key; + } else { + clientBoundKey = key; + } + } + } + + if (baseResolver == null) { + if ((serverBoundKey == null || clientBoundKey == null)) { + // neither pre 1.20.2 key nor 1.20.2+ keys are available + throw new ExceptionInInitializerError("Unable to resolve protocol state attribute keys"); + } else { + baseResolver = new Post1_20_2WrappedResolver(serverBoundKey, clientBoundKey); + } + } + } else { + throw new ExceptionInInitializerError("Unable to resolve protocol state attribute key(s)"); + } + + // decorate the base resolver by wrapping its return value into our packet type value + PROTOCOL_RESOLVER = baseResolver.andThen(protocol -> PacketType.Protocol.fromVanilla((Enum) protocol)); + } + + private static final class Pre1_20_2DirectResolver implements BiFunction { + + private final AttributeKey attributeKey; + + public Pre1_20_2DirectResolver(AttributeKey attributeKey) { + this.attributeKey = attributeKey; + } + + @Override + public Object apply(Channel channel, PacketType.Sender sender) { + return channel.attr(this.attributeKey).get(); + } + } + + private static final class Post1_20_2WrappedResolver implements BiFunction { + + private final AttributeKey serverBoundKey; + private final AttributeKey clientBoundKey; + + // lazy initialized when needed + private FieldAccessor protocolAccessor; + + public Post1_20_2WrappedResolver(AttributeKey serverBoundKey, AttributeKey clientBoundKey) { + this.serverBoundKey = serverBoundKey; + this.clientBoundKey = clientBoundKey; + } + + @Override + public Object apply(Channel channel, PacketType.Sender sender) { + AttributeKey key = this.getKeyForSender(sender); + Object codecData = channel.attr(key).get(); + if (codecData == null) { + return null; + } + + FieldAccessor protocolAccessor = this.getProtocolAccessor(codecData.getClass()); + return protocolAccessor.get(codecData); + } + + private AttributeKey getKeyForSender(PacketType.Sender sender) { + switch (sender) { + case SERVER: + return this.clientBoundKey; + case CLIENT: + return this.serverBoundKey; + default: + throw new IllegalArgumentException("Illegal packet sender " + sender.name()); + } + } + + private FieldAccessor getProtocolAccessor(Class codecClass) { + if (this.protocolAccessor == null) { + Class enumProtocolClass = MinecraftReflection.getEnumProtocolClass(); + this.protocolAccessor = Accessors.getFieldAccessor(codecClass, enumProtocolClass, true); + } + + return this.protocolAccessor; + } + } + + /** + * Since 1.20.5 the protocol is stored as final field in de-/encoder + */ + private static final class Post1_20_5WrappedResolver implements BiFunction { + + // lazy initialized when needed + private Function serverProtocolAccessor; + private Function clientProtocolAccessor; + + @Override + public Object apply(Channel channel, PacketType.Sender sender) { + ChannelHandlerContext context = channel.pipeline().context(this.getHandlerName(sender)); + if (context == null) { + return null; + } + + Object handler = context.handler(); + Function protocolAccessor = this.getProtocolAccessor(handler.getClass(), sender); + return protocolAccessor.apply(handler); + } + + private Function getProtocolAccessor(Class codecHandler, PacketType.Sender sender) { + switch (sender) { + case SERVER: + if (this.serverProtocolAccessor == null) { + this.serverProtocolAccessor = getProtocolAccessor(codecHandler); + } + return this.serverProtocolAccessor; + case CLIENT: + if (this.clientProtocolAccessor == null) { + this.clientProtocolAccessor = getProtocolAccessor(codecHandler); + } + return this.clientProtocolAccessor; + default: + throw new IllegalArgumentException("Illegal packet sender " + sender.name()); + } + } + + private String getHandlerName(PacketType.Sender sender) { + switch (sender) { + case SERVER: + return "encoder"; + case CLIENT: + return "decoder"; + default: + throw new IllegalArgumentException("Illegal packet sender " + sender.name()); + } + } + + private Function getProtocolAccessor(Class codecHandler) { + Class protocolInfoClass = MinecraftReflection.getProtocolInfoClass(); + + MethodAccessor protocolAccessor = Accessors.getMethodAccessor(FuzzyReflection + .fromClass(protocolInfoClass).getMethodByReturnTypeAndParameters("id", + MinecraftReflection.getEnumProtocolClass(), new Class[0])); + + FieldAccessor protocolInfoAccessor = Accessors.getFieldAccessor(codecHandler, protocolInfoClass, true); + + // get ProtocolInfo from handler and get EnumProtocol of ProtocolInfo + return (handler) -> { + Object protocolInfo = protocolInfoAccessor.get(handler); + return protocolAccessor.invoke(protocolInfo); + }; + } + } +} diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/injector/OrebfuscatorInjector.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/injector/OrebfuscatorInjector.java new file mode 100644 index 00000000..e3325f2e --- /dev/null +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/injector/OrebfuscatorInjector.java @@ -0,0 +1,172 @@ +package net.imprex.orebfuscator.injector; + +import java.lang.reflect.Modifier; +import java.util.LinkedList; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.bukkit.entity.Player; + +import com.comphenix.protocol.PacketType; +import com.comphenix.protocol.events.PacketContainer; +import com.comphenix.protocol.reflect.FuzzyReflection; +import com.comphenix.protocol.reflect.accessors.Accessors; +import com.comphenix.protocol.reflect.fuzzy.FuzzyFieldContract; +import com.comphenix.protocol.utility.MinecraftFields; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoop; +import io.netty.util.concurrent.Promise; +import net.imprex.orebfuscator.Orebfuscator; +import net.imprex.orebfuscator.chunk.ChunkStruct; +import net.imprex.orebfuscator.config.AdvancedConfig; +import net.imprex.orebfuscator.config.OrebfuscatorConfig; +import net.imprex.orebfuscator.obfuscation.ObfuscationResult; +import net.imprex.orebfuscator.obfuscation.ObfuscationSystem; +import net.imprex.orebfuscator.player.OrebfuscatorPlayer; +import net.imprex.orebfuscator.player.OrebfuscatorPlayerMap; +import net.imprex.orebfuscator.util.BlockPos; +import net.imprex.orebfuscator.util.OFCLogger; +import net.imprex.orebfuscator.util.PermissionUtil; + +public class OrebfuscatorInjector { + + private static final String OUTBOUND_HANDLER_NAME = "orebfuscator_outbound"; + + private final OrebfuscatorConfig config; + private final OrebfuscatorPlayerMap playerMap; + private final ObfuscationSystem obfuscationSystem; + + private final Player player; + private final Channel channel; + + private final Queue delayPackets = new LinkedList(); + + public OrebfuscatorInjector(Orebfuscator orebfuscator, Player player) { + this.config = orebfuscator.getOrebfuscatorConfig(); + this.playerMap = orebfuscator.getPlayerMap(); + this.obfuscationSystem = orebfuscator.getObfuscationSystem(); + + Object networkManager = MinecraftFields.getNetworkManager(player); + var channelAccessor = Accessors.getFieldAccessor(FuzzyReflection.fromObject(networkManager, true) + .getField(FuzzyFieldContract.newBuilder() + .typeExact(Channel.class) + .banModifier(Modifier.STATIC) + .build())); + + this.player = player; + this.channel = (Channel) channelAccessor.get(networkManager); + + ChannelPipeline pipeline = this.channel.pipeline(); + + String encoderName = pipeline.get("outbound_config") != null + ? "outbound_config" + : "encoder"; + + if (pipeline.context(OUTBOUND_HANDLER_NAME) == null) { + pipeline.addAfter(encoderName, OUTBOUND_HANDLER_NAME, new AsyncOutboundPacketHandler(this)); + } + } + + public void uninject() { + if (!this.channel.eventLoop().inEventLoop()) { + channel.eventLoop().execute(this::uninject); + return; + } + + ChannelPipeline pipeline = this.channel.pipeline(); + if (pipeline.context(OUTBOUND_HANDLER_NAME) != null) { + pipeline.remove(OUTBOUND_HANDLER_NAME); + } + } + + public void logPacketDelay(long nanos) { + while (this.delayPackets.size() > 10_000) + this.delayPackets.poll(); + + this.delayPackets.offer(nanos); + } + + public double averagePacketDelay() { + return this.delayPackets.stream() + .mapToLong(Long::longValue) + .average() + .orElse(0d); + } + + public PacketType.Protocol getOutboundProtocol() { + return ChannelProtocolUtil.PROTOCOL_RESOLVER.apply(this.channel, PacketType.Sender.SERVER); + } + + public boolean hasOutboundAsyncListener(PacketType packetType) { + return packetType == PacketType.Play.Server.MAP_CHUNK; + } + + public Promise processOutboundAsync(PacketType packetType, Object packet, EventLoop eventLoop) { + if (this.shouldNotObfuscate(this.player)) { + return null; + } + + PacketContainer packetContainer = new PacketContainer(packetType, packet); + ChunkStruct struct = new ChunkStruct(packetContainer, this.player.getWorld()); + if (struct.isEmpty()) { + return null; + } + + CompletableFuture future = this.obfuscationSystem.obfuscate(struct); + + AdvancedConfig advancedConfig = this.config.advanced(); + if (advancedConfig.hasObfuscationTimeout()) { + future = future.orTimeout(advancedConfig.obfuscationTimeout(), TimeUnit.MILLISECONDS); + } + + Promise promise = eventLoop.newPromise(); + future.whenComplete((chunk, throwable) -> { + if (throwable != null) { + this.completeExceptionally(struct, throwable); + promise.setSuccess(packet); + } else if (chunk != null) { + this.complete(struct, chunk); + promise.setSuccess(packet); + } else { + OFCLogger.warn(String.format("skipping chunk[world=%s, x=%d, z=%d] because obfuscation result is missing", + struct.world.getName(), struct.chunkX, struct.chunkZ)); + promise.setSuccess(packet); + } + }); + + return promise; + } + + private boolean shouldNotObfuscate(Player player) { + return PermissionUtil.canBypassObfuscate(player) || !config.world(player.getWorld()).needsObfuscation(); + } + + private void completeExceptionally(ChunkStruct struct, Throwable throwable) { + if (throwable instanceof TimeoutException) { + OFCLogger.warn(String.format("Obfuscation for chunk[world=%s, x=%d, z=%d] timed out", + struct.world.getName(), struct.chunkX, struct.chunkZ)); + } else { + OFCLogger.error(String.format("An error occurred while obfuscating chunk[world=%s, x=%d, z=%d]", + struct.world.getName(), struct.chunkX, struct.chunkZ), throwable); + } + } + + private void complete(ChunkStruct struct, ObfuscationResult chunk) { + struct.setDataBuffer(chunk.getData()); + + Set blockEntities = chunk.getBlockEntities(); + if (!blockEntities.isEmpty()) { + struct.removeBlockEntityIf(blockEntities::contains); + } + + final OrebfuscatorPlayer orebfuscatorPlayer = this.playerMap.get(this.player); + if (orebfuscatorPlayer != null) { + orebfuscatorPlayer.addChunk(struct.chunkX, struct.chunkZ, chunk.getProximityBlocks()); + } + } +} diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/injector/OrebfuscatorInjectorManager.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/injector/OrebfuscatorInjectorManager.java new file mode 100644 index 00000000..5a9f7ead --- /dev/null +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/injector/OrebfuscatorInjectorManager.java @@ -0,0 +1,56 @@ +package net.imprex.orebfuscator.injector; + +import java.util.HashMap; +import java.util.Map; + +import org.bukkit.Bukkit; +import org.bukkit.entity.Player; +import org.bukkit.event.EventHandler; +import org.bukkit.event.EventPriority; +import org.bukkit.event.Listener; +import org.bukkit.event.player.PlayerJoinEvent; +import org.bukkit.event.player.PlayerQuitEvent; + +import net.imprex.orebfuscator.Orebfuscator; + +public class OrebfuscatorInjectorManager implements Listener { + + private final Orebfuscator orebfuscator; + private final Map injectors = new HashMap<>(); + + public OrebfuscatorInjectorManager(Orebfuscator orebfuscator) { + this.orebfuscator = orebfuscator; + + Bukkit.getPluginManager().registerEvents(this, orebfuscator); + + for (Player player : Bukkit.getOnlinePlayers()) { + this.injectors.put(player, new OrebfuscatorInjector(this.orebfuscator, player)); + } + + this.orebfuscator.getStatistics().setAveragePacketDelay(() -> { + return this.injectors.values().stream() + .mapToDouble(OrebfuscatorInjector::averagePacketDelay) + .average() + .orElse(0d); + }); + } + + @EventHandler(priority = EventPriority.LOW) + public void handleJoin(PlayerJoinEvent event) { + Player player = event.getPlayer(); + this.injectors.put(player, new OrebfuscatorInjector(this.orebfuscator, player)); + } + + @EventHandler(priority = EventPriority.LOW) + public void handleQuit(PlayerQuitEvent event) { + this.injectors.remove(event.getPlayer()).uninject(); + } + + public void close() { + for (OrebfuscatorInjector injector : this.injectors.values()) { + injector.uninject(); + } + + this.injectors.clear(); + } +} diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationListener.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationListener.java index 10e4df55..3bd35691 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationListener.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationListener.java @@ -1,157 +1,33 @@ package net.imprex.orebfuscator.obfuscation; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.stream.Collectors; - -import org.bukkit.entity.Player; - -import com.comphenix.protocol.AsynchronousManager; import com.comphenix.protocol.PacketType; -import com.comphenix.protocol.PacketTypeEnum; import com.comphenix.protocol.ProtocolLibrary; -import com.comphenix.protocol.async.AsyncListenerHandler; +import com.comphenix.protocol.ProtocolManager; import com.comphenix.protocol.events.PacketAdapter; import com.comphenix.protocol.events.PacketEvent; import net.imprex.orebfuscator.Orebfuscator; -import net.imprex.orebfuscator.OrebfuscatorCompatibility; -import net.imprex.orebfuscator.chunk.ChunkStruct; -import net.imprex.orebfuscator.config.AdvancedConfig; -import net.imprex.orebfuscator.config.OrebfuscatorConfig; -import net.imprex.orebfuscator.player.OrebfuscatorPlayer; -import net.imprex.orebfuscator.player.OrebfuscatorPlayerMap; -import net.imprex.orebfuscator.util.BlockPos; -import net.imprex.orebfuscator.util.OFCLogger; -import net.imprex.orebfuscator.util.PermissionUtil; -import net.imprex.orebfuscator.util.ServerVersion; public class ObfuscationListener extends PacketAdapter { - private static final List PACKET_TYPES = Arrays.asList( - PacketType.Play.Server.MAP_CHUNK, - PacketType.Play.Server.UNLOAD_CHUNK, - PacketType.Play.Server.LIGHT_UPDATE, - PacketType.Play.Server.TILE_ENTITY_DATA, - tryGetPacketType(PacketType.Play.Client.getInstance(), "CHUNK_BATCH_RECEIVED") - ); - - private static PacketType tryGetPacketType(PacketTypeEnum packetTypeEnum, String name) { - return packetTypeEnum.values().stream() - .filter(packetType -> packetType.name().equals(name)) - .findAny() - .orElse(null); - } - - private final OrebfuscatorConfig config; - private final OrebfuscatorPlayerMap playerMap; - private final ObfuscationSystem obfuscationSystem; - - private final AsynchronousManager asynchronousManager; - private final AsyncListenerHandler asyncListenerHandler; + private final ProtocolManager protocolManager; public ObfuscationListener(Orebfuscator orebfuscator) { - super(orebfuscator, PACKET_TYPES.stream() - .filter(Objects::nonNull) - .filter(PacketType::isSupported) - .collect(Collectors.toList())); - - this.config = orebfuscator.getOrebfuscatorConfig(); - this.playerMap = orebfuscator.getPlayerMap(); - this.obfuscationSystem = orebfuscator.getObfuscationSystem(); + super(PacketAdapter.params() + .plugin(orebfuscator) + .types(PacketType.Play.Client.CHUNK_BATCH_RECEIVED) + .optionAsync()); - this.asynchronousManager = ProtocolLibrary.getProtocolManager().getAsynchronousManager(); - this.asyncListenerHandler = this.asynchronousManager.registerAsyncHandler(this); - - if (ServerVersion.isFolia()) { - OrebfuscatorCompatibility.runAsyncNow(this.asyncListenerHandler.getListenerLoop()); - } else { - this.asyncListenerHandler.start(); - } + this.protocolManager = ProtocolLibrary.getProtocolManager(); + this.protocolManager.addPacketListener(this); } public void unregister() { - this.asynchronousManager.unregisterAsyncHandler(this.asyncListenerHandler); + this.protocolManager.removePacketListener(this); } @Override public void onPacketReceiving(PacketEvent event) { event.getPacket().getFloat().write(0, 10f); } - - @Override - public void onPacketSending(PacketEvent event) { - if (event.getPacket().getType() != PacketType.Play.Server.MAP_CHUNK) { - return; - } - - Player player = event.getPlayer(); - if (this.shouldNotObfuscate(player)) { - return; - } - - ChunkStruct struct = new ChunkStruct(event.getPacket(), player.getWorld()); - if (struct.isEmpty()) { - return; - } - - // delay packet - event.getAsyncMarker().incrementProcessingDelay(); - - CompletableFuture future = this.obfuscationSystem.obfuscate(struct); - - AdvancedConfig advancedConfig = this.config.advanced(); - if (advancedConfig.hasObfuscationTimeout()) { - future = future.orTimeout(advancedConfig.obfuscationTimeout(), TimeUnit.MILLISECONDS); - } - - future.whenComplete((chunk, throwable) -> { - if (throwable != null) { - this.completeExceptionally(event, struct, throwable); - } else if (chunk != null) { - this.complete(event, struct, chunk); - } else { - OFCLogger.warn(String.format("skipping chunk[world=%s, x=%d, z=%d] because obfuscation result is missing", - struct.world.getName(), struct.chunkX, struct.chunkZ)); - this.asynchronousManager.signalPacketTransmission(event); - } - }); - } - - private boolean shouldNotObfuscate(Player player) { - return PermissionUtil.canBypassObfuscate(player) || !config.world(player.getWorld()).needsObfuscation(); - } - - private void completeExceptionally(PacketEvent event, ChunkStruct struct, Throwable throwable) { - if (throwable instanceof TimeoutException) { - OFCLogger.warn(String.format("Obfuscation for chunk[world=%s, x=%d, z=%d] timed out", - struct.world.getName(), struct.chunkX, struct.chunkZ)); - } else { - OFCLogger.error(String.format("An error occurred while obfuscating chunk[world=%s, x=%d, z=%d]", - struct.world.getName(), struct.chunkX, struct.chunkZ), throwable); - } - - this.asynchronousManager.signalPacketTransmission(event); - } - - private void complete(PacketEvent event, ChunkStruct struct, ObfuscationResult chunk) { - struct.setDataBuffer(chunk.getData()); - - Set blockEntities = chunk.getBlockEntities(); - if (!blockEntities.isEmpty()) { - struct.removeBlockEntityIf(blockEntities::contains); - } - - final OrebfuscatorPlayer player = this.playerMap.get(event.getPlayer()); - if (player != null) { - player.addChunk(struct.chunkX, struct.chunkZ, chunk.getProximityBlocks()); - } - - this.asynchronousManager.signalPacketTransmission(event); - } } diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationProcessor.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationProcessor.java index 2b761fd7..403110d1 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationProcessor.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/obfuscation/ObfuscationProcessor.java @@ -16,7 +16,6 @@ import net.imprex.orebfuscator.config.ObfuscationConfig; import net.imprex.orebfuscator.config.OrebfuscatorConfig; import net.imprex.orebfuscator.config.ProximityConfig; -import net.imprex.orebfuscator.config.ProximityHeightCondition; import net.imprex.orebfuscator.config.WorldConfigBundle; import net.imprex.orebfuscator.util.BlockPos; import net.imprex.orebfuscator.util.HeightAccessor; @@ -95,8 +94,7 @@ public void process(ObfuscationTask task) { if (!obfuscated && BlockFlags.isProximityBitSet(obfuscateBits) && proximityConfig.shouldObfuscate(y)) { proximityBlocks.add(new BlockPos(x, y, z)); if (BlockFlags.isUseBlockBelowBitSet(obfuscateBits)) { - boolean allowNonOcclude = !isObfuscateBitSet || !ProximityHeightCondition.isPresent(obfuscateBits); - blockState = getBlockStateBelow(bundle, chunk, x, y, z, allowNonOcclude); + blockState = getBlockStateBelow(bundle, chunk, x, y, z); } else { blockState = bundle.nextRandomProximityBlock(y); } @@ -121,12 +119,12 @@ public void process(ObfuscationTask task) { // returns first block below given position that wouldn't be obfuscated in any // way at given position - private int getBlockStateBelow(WorldConfigBundle bundle, Chunk chunk, int x, int y, int z, boolean allowNonOcclude) { + private int getBlockStateBelow(WorldConfigBundle bundle, Chunk chunk, int x, int y, int z) { BlockFlags blockFlags = bundle.blockFlags(); for (int targetY = y - 1; targetY > chunk.getHeightAccessor().getMinBuildHeight(); targetY--) { int blockData = chunk.getBlockState(x, targetY, z); - if (blockData != -1 && (allowNonOcclude || OrebfuscatorNms.isOccluding(blockData))) { + if (blockData != -1 && OrebfuscatorNms.isOccluding(blockData)) { int mask = blockFlags.flags(blockData, y); if (BlockFlags.isEmpty(mask) || BlockFlags.isAllowForUseBlockBelowBitSet(mask)) { return blockData; diff --git a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/proximity/ProximityPacketListener.java b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/proximity/ProximityPacketListener.java index d19e18b4..221c0caa 100644 --- a/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/proximity/ProximityPacketListener.java +++ b/orebfuscator-plugin/src/main/java/net/imprex/orebfuscator/proximity/ProximityPacketListener.java @@ -28,7 +28,10 @@ public class ProximityPacketListener extends PacketAdapter { private final OrebfuscatorPlayerMap playerMap; public ProximityPacketListener(Orebfuscator orebfuscator) { - super(orebfuscator, PacketType.Play.Server.UNLOAD_CHUNK); + super(PacketAdapter.params() + .plugin(orebfuscator) + .types(PacketType.Play.Server.UNLOAD_CHUNK) + .optionAsync()); this.protocolManager = ProtocolLibrary.getProtocolManager(); this.protocolManager.addPacketListener(this);