Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified prebuilt/legacylink-0.1.1.jar
Binary file not shown.
57 changes: 38 additions & 19 deletions src/main/java/dev/ohno/legacylink/handler/LegacyPacketHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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<String, SimpleParticleType> LEGACY_PARTICLE_FALLBACKS = Map.of(
"minecraft:noxious_gas", ParticleTypes.SMOKE,
"minecraft:noxious_gas_cloud", ParticleTypes.CLOUD,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
21 changes: 6 additions & 15 deletions src/main/java/dev/ohno/legacylink/mixin/StatusPacketMixin.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 {

Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Loading