Skip to content
Open
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
122 changes: 97 additions & 25 deletions src/main/java/net/earthcomputer/clientcommands/features/Relogger.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

import net.earthcomputer.clientcommands.event.MoreScreenEvents;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphicsExtractor;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.events.GuiEventListener;
import net.minecraft.client.gui.screens.ConnectScreen;
import net.minecraft.client.gui.screens.DisconnectedScreen;
import net.minecraft.client.gui.screens.GenericMessageScreen;
import net.minecraft.client.gui.screens.LevelLoadingScreen;
import net.minecraft.client.gui.screens.PauseScreen;
Expand All @@ -14,6 +18,8 @@
import net.minecraft.client.multiplayer.ServerData;
import net.minecraft.client.multiplayer.resolver.ServerAddress;
import net.minecraft.client.server.IntegratedServer;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.contents.TranslatableContents;
import net.minecraft.world.level.storage.LevelResource;
import org.jspecify.annotations.Nullable;

Expand All @@ -22,27 +28,27 @@

public class Relogger {
public static boolean isRelogging;
@Nullable
public static ServerData cachedServerData;
public static int remainingTicks;
public static final List<Runnable> relogSuccessTasks = new ArrayList<>();

public static final String RATE_LIMIT_MESSAGE = "RateLimiter disallowed request";
public static final int RETRY_DELAY_TICKS = 200;

static {
MoreScreenEvents.BEFORE_ADD.register(Relogger::onAddScreen);
}

public static boolean disconnect() {
return disconnect(false);
}

private static boolean disconnect(boolean relogging) {
private static boolean disconnect() {
Minecraft mc = Minecraft.getInstance();
if (mc.level == null) {
return false;
}

boolean singleplayer = mc.isLocalServer();
mc.level.disconnect(ClientLevel.DEFAULT_QUIT_MESSAGE);
if (relogging) {
isRelogging = true;
}
isRelogging = true;
if (singleplayer) {
mc.disconnectWithSavingScreen();
} else {
Expand All @@ -62,29 +68,88 @@ private static boolean disconnect(boolean relogging) {
public static boolean relog() {
Minecraft mc = Minecraft.getInstance();
if (mc.isLocalServer()) {
IntegratedServer server = Minecraft.getInstance().getSingleplayerServer();
if (server == null) {
return false;
}
String levelName = server.getWorldPath(LevelResource.ROOT).normalize().getFileName().toString();
if (!disconnect(true)) {
IntegratedServer server = mc.getSingleplayerServer();
if (!disconnect()) {
return false;
}
if (!mc.getLevelSource().levelExists(levelName)) {
return false;
}
mc.createWorldOpenFlows().openWorld(levelName, () -> mc.setScreen(new TitleScreen()));
return true;
return loginToIntegratedServer(server);
} else {
ServerData serverData = mc.getCurrentServer();
if (serverData == null) {
ServerData server = mc.getCurrentServer();
if (!disconnect()) {
return false;
}
if (!disconnect(true)) {
return false;
return loginToDedicatedServer(server);
}
}

private static boolean loginToIntegratedServer(@Nullable IntegratedServer server) {
Minecraft mc = Minecraft.getInstance();

if (server == null) {
return false;
}
String levelName = server.getWorldPath(LevelResource.ROOT).normalize().getFileName().toString();
if (!mc.getLevelSource().levelExists(levelName)) {
return false;
}
mc.createWorldOpenFlows().openWorld(levelName, () -> mc.setScreen(new TitleScreen()));
return true;
}

private static boolean loginToDedicatedServer(@Nullable ServerData serverData) {
Minecraft mc = Minecraft.getInstance();

if (serverData == null) {
return false;
}
isRelogging = true;
cachedServerData = serverData;
ConnectScreen.startConnecting(mc.screen, mc, ServerAddress.parseString(serverData.ip), serverData, false, null);
return true;
}

public static void onFailedRelog() {
Minecraft mc = Minecraft.getInstance();
// only possible if the user clicks off and "cancels"
if (!(mc.screen instanceof DisconnectedScreen screen) || cachedServerData == null) {
isRelogging = false;
cachedServerData = null;
return;
}

mc.setScreen(screen.parent);

ServerData serverData = cachedServerData;
cachedServerData = null;
isRelogging = false;
loginToDedicatedServer(serverData);
}

public static void onDisconnectScreenRender(Screen screen, GuiGraphicsExtractor graphics, int mouseX, int mouseY, float tickProgress) {
Button button = null;
for (GuiEventListener child : screen.children()) {
if (child instanceof Button buttonChild) {
button = buttonChild;
break;
}
}
if (button == null) {
throw new IllegalStateException("Expected a back button in the DisconnectScreen");
}

double remainingSeconds = (double) remainingTicks * 0.050;
Component text = Component.translatable("commands.crelog.retry", String.format("%.1f", remainingSeconds));
int textWidth = screen.getFont().width(text);

graphics.text(screen.getFont(), text, (screen.width - textWidth) / 2, button.getY() + button.getHeight() + 10, 0xff_ffffff);
}

public static void onDisconnectScreenTick(Screen screen) {
if (remainingTicks > 0) {
remainingTicks--;
if (remainingTicks == 0) {
onFailedRelog();
}
ConnectScreen.startConnecting(mc.screen, mc, ServerAddress.parseString(serverData.ip), serverData, false, null);
return true;
}
}

Expand All @@ -94,6 +159,7 @@ private static boolean onAddScreen(@Nullable Screen screen) {
&& !(screen instanceof LevelLoadingScreen)
&& !(screen instanceof ProgressScreen)
&& !(screen instanceof ConnectScreen)
&& !(screen instanceof DisconnectedScreen)
&& !(screen instanceof PauseScreen)
&& !(screen instanceof TitleScreen)
&& !(screen instanceof JoinMultiplayerScreen)
Expand All @@ -110,6 +176,12 @@ public static boolean onRelogSuccess() {
task.run();
}
relogSuccessTasks.clear();
isRelogging = false;
cachedServerData = null;
return result;
}

public static boolean isRateLimitMessage(Component error) {
return error.getContents() instanceof TranslatableContents translate && translate.getKey().equals("disconnect.loginFailedInfo") && translate.getArgs().length == 1 && translate.getArgs()[0].toString().equals(RATE_LIMIT_MESSAGE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package net.earthcomputer.clientcommands.mixin.commands.relog;

import com.llamalad7.mixinextras.sugar.Local;
import net.earthcomputer.clientcommands.features.Relogger;
import net.fabricmc.fabric.impl.client.screen.ScreenExtensions;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.DisconnectedScreen;
import net.minecraft.client.multiplayer.ClientHandshakePacketListenerImpl;
import net.minecraft.network.Connection;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.login.ServerboundKeyPacket;
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;

@Mixin(ClientHandshakePacketListenerImpl.class)
public class ClientHandshakePacketListenerImplMixin {
@Shadow
@Final
private Connection connection;

@Inject(method = "lambda$handleHello$0", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/Connection;disconnect(Lnet/minecraft/network/chat/Component;)V", shift = At.Shift.AFTER))
private void onRelogFail(String digest, ServerboundKeyPacket setKeyPacket, Cipher decryptCipher, Cipher encryptCipher, CallbackInfo ci, @Local(name = "error") Component error) {
if (!Relogger.isRelogging) {
return;
}

if (!Relogger.isRateLimitMessage(error)) {
Relogger.isRelogging = false;
return;
}

Minecraft mc = Minecraft.getInstance();

mc.execute(() -> {
connection.handleDisconnection();
if (mc.screen instanceof DisconnectedScreen screen && Relogger.isRateLimitMessage(error)) {
Relogger.remainingTicks = Relogger.RETRY_DELAY_TICKS;
ScreenExtensions.getExtensions(screen).fabric_getAfterRenderEvent().register(Relogger::onDisconnectScreenRender);
ScreenExtensions.getExtensions(screen).fabric_getAfterTickEvent().register(Relogger::onDisconnectScreenTick);
}
});
}
}
1 change: 1 addition & 0 deletions src/main/resources/assets/clientcommands/lang/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@
"commands.cpredictbrushables.stewEffect": "%s (%s seconds)",

"commands.crelog.failed": "Failed to relog",
"commands.crelog.retry": "Retrying in %s seconds...",

"commands.crender.entities.success": "Entity rendering rules have been updated",

Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/clientcommands.aw
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ accessible method net/minecraft/client/renderer/GameRenderer setPostEffect (Lnet
accessible field net/minecraft/client/renderer/ShaderManager compilationCache Lnet/minecraft/client/renderer/ShaderManager$CompilationCache;
accessible field net/minecraft/client/renderer/ShaderManager$CompilationCache configs Lnet/minecraft/client/renderer/ShaderManager$Configs;

# crelog
accessible field net/minecraft/client/gui/screens/DisconnectedScreen parent Lnet/minecraft/client/gui/screens/Screen;

Comment thread
RealRTTV marked this conversation as resolved.
# Game Options
accessible field net/minecraft/client/OptionInstance value Ljava/lang/Object;

Expand Down
1 change: 1 addition & 0 deletions src/main/resources/mixins.clientcommands.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"commands.fps.MinecraftMixin",
"commands.generic.CommandSuggestionsMixin",
"commands.glow.LivingEntityRenderStateMixin",
"commands.relog.ClientHandshakePacketListenerImplMixin",
"commands.render.LevelRendererMixin",
"commands.reply.ClientPacketListenerMixin",
"commands.snap.MinecraftMixin",
Expand Down
Loading