diff --git a/prebuilt/legacylink-0.1.1.jar b/prebuilt/legacylink-0.1.1.jar index c82e5ed..12f0458 100644 Binary files a/prebuilt/legacylink-0.1.1.jar and b/prebuilt/legacylink-0.1.1.jar differ diff --git a/src/main/java/dev/ohno/legacylink/handler/LegacyPacketHandler.java b/src/main/java/dev/ohno/legacylink/handler/LegacyPacketHandler.java index 82b9726..55acd43 100644 --- a/src/main/java/dev/ohno/legacylink/handler/LegacyPacketHandler.java +++ b/src/main/java/dev/ohno/legacylink/handler/LegacyPacketHandler.java @@ -23,6 +23,7 @@ import dev.ohno.legacylink.mapping.LegacyAttributeWireTable; import dev.ohno.legacylink.mapping.RegistryRemapper; import dev.ohno.legacylink.runtime.LegacyRuntimeContext; +import dev.ohno.legacylink.status.LegacyStatusCacheManager; import dev.ohno.legacylink.telemetry.TranslationStats; import io.netty.channel.ChannelDuplexHandler; import io.netty.channel.ChannelHandlerContext; @@ -65,7 +66,6 @@ import net.minecraft.network.protocol.game.ClientboundUpdateAdvancementsPacket; import net.minecraft.network.protocol.game.ClientboundUpdateRecipesPacket; import net.minecraft.network.protocol.status.ClientboundStatusResponsePacket; -import net.minecraft.network.protocol.status.ServerStatus; import net.minecraft.resources.Identifier; import net.minecraft.resources.ResourceKey; import net.minecraft.server.MinecraftServer; @@ -91,7 +91,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import java.lang.reflect.Constructor; import java.util.Set; @@ -104,6 +103,11 @@ public class LegacyPacketHandler extends ChannelDuplexHandler { private static final String HANDLER_NAME = "legacylink"; private static final EntityType LEGACY_SLIME_TYPE = resolveEntityType("minecraft:slime"); + + private static final long STATUS_LOG_INTERVAL_NS = 10_000_000_000L; // 10 seconds + private static long statusInstallCount; + private static long statusInstallWindowStart; + private static final Map LEGACY_PARTICLE_FALLBACKS = Map.of( "minecraft:noxious_gas", ParticleTypes.SMOKE, "minecraft:noxious_gas_cloud", ParticleTypes.CLOUD, @@ -204,12 +208,36 @@ public static void install(Connection connection, String phase) { + "; refusing addLast fallback (legacy translation would not run correctly)."); } pipeline.addAfter(HandlerNames.PACKET_HANDLER, HANDLER_NAME, new LegacyPacketHandler()); - LegacyLinkMod.LOGGER.info( - "[LegacyLink] Outbound translator placed after '{}' (phase={}) for {}", - HandlerNames.PACKET_HANDLER, - phase, - anonymizeAddress(connection.getRemoteAddress()) - ); + + if ("status".equals(phase)) { + logStatusInstall(); + } else { + LegacyLinkMod.LOGGER.info( + "[LegacyLink] Outbound translator placed after '{}' (phase={}) for {}", + HandlerNames.PACKET_HANDLER, + phase, + anonymizeAddress(connection.getRemoteAddress()) + ); + } + } + + private static synchronized void logStatusInstall() { + long now = System.nanoTime(); + if (statusInstallCount == 0L) { + statusInstallWindowStart = now; + } + statusInstallCount++; + long elapsed = now - statusInstallWindowStart; + if (elapsed >= STATUS_LOG_INTERVAL_NS) { + long elapsedSeconds = elapsed / 1_000_000_000L; + LegacyLinkMod.LOGGER.info( + "[LegacyLink] STATUS translator installed {} time(s) in the last {}s", + statusInstallCount, + elapsedSeconds + ); + statusInstallCount = 0; + statusInstallWindowStart = now; + } } private static String anonymizeAddress(@Nullable Object remoteAddress) { @@ -524,17 +552,8 @@ public ClientboundBundlePacket remapBundlePacket(ClientboundBundlePacket bundleP return new ClientboundBundlePacket(rewritten); } - private ClientboundStatusResponsePacket remapStatusResponse(ClientboundStatusResponsePacket packet) { - ServerStatus status = packet.status(); - ServerStatus.Version forcedLegacyVersion = new ServerStatus.Version("26.1.2", LegacyLinkConstants.PROTOCOL_26_1_2); - ServerStatus remapped = new ServerStatus( - status.description(), - status.players(), - Optional.of(forcedLegacyVersion), - status.favicon(), - status.enforcesSecureChat() - ); - return new ClientboundStatusResponsePacket(remapped); + private static ClientboundStatusResponsePacket remapStatusResponse(ClientboundStatusResponsePacket packet) { + return LegacyStatusCacheManager.getOrBuildForOutboundHandler(packet.status()); } private ClientboundRegistryDataPacket filterRegistryData(ClientboundRegistryDataPacket packet) { diff --git a/src/main/java/dev/ohno/legacylink/mixin/StatusPacketMixin.java b/src/main/java/dev/ohno/legacylink/mixin/StatusPacketMixin.java index 671c937..0c82755 100644 --- a/src/main/java/dev/ohno/legacylink/mixin/StatusPacketMixin.java +++ b/src/main/java/dev/ohno/legacylink/mixin/StatusPacketMixin.java @@ -1,7 +1,7 @@ package dev.ohno.legacylink.mixin; -import dev.ohno.legacylink.LegacyLinkConstants; import dev.ohno.legacylink.connection.LegacyTracker; +import dev.ohno.legacylink.status.LegacyStatusCacheManager; import net.minecraft.network.Connection; import net.minecraft.network.protocol.status.ClientboundStatusResponsePacket; import net.minecraft.network.protocol.status.ServerStatus; @@ -14,8 +14,6 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import java.util.Optional; - @Mixin(ServerStatusPacketListenerImpl.class) public abstract class StatusPacketMixin { @@ -33,18 +31,11 @@ public abstract class StatusPacketMixin { } this.hasRequestedStatus = true; - ServerStatus.Version forcedLegacyVersion = new ServerStatus.Version( - "26.1.2", - LegacyLinkConstants.PROTOCOL_26_1_2 - ); - ServerStatus remapped = new ServerStatus( - this.status.description(), - this.status.players(), - Optional.of(forcedLegacyVersion), - this.status.favicon(), - this.status.enforcesSecureChat() - ); - this.connection.send(new ClientboundStatusResponsePacket(remapped)); + this.connection.send(getOrBuildCachedResponse(this.status)); ci.cancel(); } + + private static ClientboundStatusResponsePacket getOrBuildCachedResponse(ServerStatus current) { + return LegacyStatusCacheManager.getOrBuildForStatusListener(current); + } } diff --git a/src/main/java/dev/ohno/legacylink/status/LegacyStatusCacheManager.java b/src/main/java/dev/ohno/legacylink/status/LegacyStatusCacheManager.java new file mode 100644 index 0000000..5c5cb8b --- /dev/null +++ b/src/main/java/dev/ohno/legacylink/status/LegacyStatusCacheManager.java @@ -0,0 +1,83 @@ +package dev.ohno.legacylink.status; + +import dev.ohno.legacylink.LegacyLinkConstants; +import net.minecraft.network.protocol.status.ClientboundStatusResponsePacket; +import net.minecraft.network.protocol.status.ServerStatus; + +import java.util.Optional; + +/** + * Centralized status remap cache so all status pathways share identical TTL and keying rules. + */ +public final class LegacyStatusCacheManager { + private static final long STATUS_CACHE_TTL_NS = 1_000_000_000L; // 1 second + + private static volatile CacheEntry listenerCache; + private static volatile CacheEntry outboundHandlerCache; + + private LegacyStatusCacheManager() {} + + public static ClientboundStatusResponsePacket getOrBuildForStatusListener(ServerStatus current) { + return getOrBuild(current, CacheScope.LISTENER); + } + + public static ClientboundStatusResponsePacket getOrBuildForOutboundHandler(ServerStatus current) { + return getOrBuild(current, CacheScope.OUTBOUND_HANDLER); + } + + private static ClientboundStatusResponsePacket getOrBuild(ServerStatus current, CacheScope scope) { + long now = System.nanoTime(); + CacheEntry cached = read(scope); + if (cached != null && cached.sourceStatus == current && (now - cached.builtAtNanos) < STATUS_CACHE_TTL_NS) { + return cached.response; + } + + ClientboundStatusResponsePacket built = buildLegacyStatusResponse(current); + write(scope, new CacheEntry(built, current, now)); + return built; + } + + private static CacheEntry read(CacheScope scope) { + return scope == CacheScope.LISTENER ? listenerCache : outboundHandlerCache; + } + + private static void write(CacheScope scope, CacheEntry entry) { + if (scope == CacheScope.LISTENER) { + listenerCache = entry; + } else { + outboundHandlerCache = entry; + } + } + + private static ClientboundStatusResponsePacket buildLegacyStatusResponse(ServerStatus current) { + ServerStatus.Version forcedLegacyVersion = new ServerStatus.Version( + "26.1.2", + LegacyLinkConstants.PROTOCOL_26_1_2 + ); + ServerStatus remapped = new ServerStatus( + current.description(), + current.players(), + Optional.of(forcedLegacyVersion), + current.favicon(), + current.enforcesSecureChat() + ); + return new ClientboundStatusResponsePacket(remapped); + } + + private enum CacheScope { + LISTENER, + OUTBOUND_HANDLER + } + + private static final class CacheEntry { + private final ClientboundStatusResponsePacket response; + private final ServerStatus sourceStatus; + private final long builtAtNanos; + + private CacheEntry(ClientboundStatusResponsePacket response, ServerStatus sourceStatus, long builtAtNanos) { + this.response = response; + this.sourceStatus = sourceStatus; + this.builtAtNanos = builtAtNanos; + } + } +}