Major update

Added configuration for delay and attempts
Added commands to reload and change config
Added support for 1.16
Added support for ModMenu and AuthMe
Improved reconnecting logic to be more efficient and cleaner looking
Improved countdown overlay
This commit is contained in:
Bstn1802
2021-01-05 03:06:41 +01:00
parent 0daf476c92
commit e3187ee615
19 changed files with 420 additions and 119 deletions

View File

@ -1,20 +1,56 @@
# AutoReconnect
# AutoReconnect [1.16+][Fabric]
### Description
This mod will automatically try to reconnect you back to a server if you got disconnected.
By default, it will make 4 attempts after 3, 10, 30 and 60 seconds.
This mod allows you to afk without fear of getting disconnected. It will make 4 attempts to reconnect you to the server you were disconnected from, with a delay of 3, 10, 60 and 300 seconds.
After 4 failed attempts it will no longer try to reconnect.
### Features
<sub>(Btw you can exit the disconnect screen with <kbd>Esc</kbd>)</sub>
* Multiple individually delayed reconnect attempts
* Displays a countdown on the disconnect screen
* Allows you to exit the disconnect screen quickly by pressing the escape key
* Customizable
* Amount of attempts
* Delay between each attempt
* Client side commands
* `/autoreconnect reload` Reloads the config and displays the settings in chat
* `/autoreconnect config [<delayList>]` Sets the delay between each attempt<br>
`[<delayList>]` must be a Nbt List Tag containing Integers, e.g. `[3, 10, 30, 60]` or `[I;3, 10, 30, 60]`
* Support for several mods
### Installation
1. [Download](https://fabricmc.net/use/) and install Fabric
2. Download [Fabric API]() and put the jar file into the mods folder
3. Do the same for this mod
### Compatibility
* [ModMenu](https://www.curseforge.com/minecraft/mc-mods/modmenu) <br>
Properly shows the mod icon, name and author
and provides a link for the curseforge project page and the github issues page
* [AuthMe](https://www.curseforge.com/minecraft/mc-mods/auth-me) <br>
Pauses the countdown if you click on the Re-authenticate button to revalidate the session of the game
### Common questions
* _Can I change the delay?_<br>
Yes, you finally can with the latest version of this mod.
* _Forge version?_<br>
Simply no. I am not interested in developing mods using Forge.
* _Version for 1.13.x or lower?_<br>
Fabric does not exist for those versions.
* _Version for 1.14.x or 1.15.x?_<br>
If there is enough demand for it I might make a version for those versions of the game.
### Screenshots
![countdown](src/main/resources/assets/screenshot_countdown.png)
![countdown](src/main/resources/assets/countdown.png)
![failed](src/main/resources/assets/screenshot_failed.png)
<sub>(Mod doesn't change the background or the font, I was using a resource pack when I took the screenshots)</sub>
![failed](src/main/resources/assets/failed.png)
### License
This mod is available under the CC0 license. Feel free to learn from it and incorporate it in your own projects.
This mod is available under the CC0 license.
Feel free to learn from it and incorporate it in your own projects.
If you actually just copy code or use this mod in a mod pack I would appreciate it if you mention me
by linking the github page or the curseforge project page.

View File

@ -1,5 +1,5 @@
plugins {
id 'fabric-loom' version '0.4-SNAPSHOT'
id 'fabric-loom' version '0.5-SNAPSHOT'
id 'maven-publish'
}
@ -11,12 +11,12 @@ version = project.mod_version
group = project.maven_group
dependencies {
minecraft "com.mojang:minecraft:1.16.1"
mappings "net.fabricmc:yarn:1.16.1+build.21:v2"
modImplementation "net.fabricmc:fabric-loader:0.9.3+build.207"
minecraft "com.mojang:minecraft:1.16"
mappings "net.fabricmc:yarn:1.16+build.4:v2"
modImplementation "net.fabricmc:fabric-loader:0.10.8"
//Fabric api
modImplementation "net.fabricmc.fabric-api:fabric-api:0.18.0+build.387-1.16.1"
modImplementation "net.fabricmc.fabric-api:fabric-api:0.29.2+1.16"
}
processResources {

View File

@ -3,15 +3,15 @@ org.gradle.jvmargs=-Xmx1G
# Fabric Properties
# check these on https://fabricmc.net/use
minecraft_version=1.16.1
yarn_mappings=1.16.1+build.21
loader_version=0.9.3+build.207
minecraft_version=1.16
yarn_mappings=1.16+build.4
loader_version=0.10.8
# Mod Properties
mod_version = 1.0.1
mod_version = 1.1.0
maven_group = net.autoreconnect
archives_base_name = auto-reconnect
archives_base_name = autoreconnect
# Dependencies
# currently not on the main fabric site, check on the maven: https://maven.fabricmc.net/net/fabricmc/fabric-api/fabric-api
fabric_version=0.18.0+build.387-1.16.1
fabric_version=0.29.2+1.16

View File

@ -1,57 +1,126 @@
package net.autoreconnect;
import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import com.mojang.brigadier.context.CommandContext;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.client.network.ClientCommandSource;
import net.minecraft.client.network.ServerInfo;
import net.minecraft.nbt.*;
import net.minecraft.util.Formatting;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.atomic.AtomicInteger;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.IntStream;
import static com.mojang.brigadier.Command.SINGLE_SUCCESS;
import static net.autoreconnect.ClientCommands.*;
import static net.autoreconnect.Util.*;
import static net.minecraft.command.arguments.NbtTagArgumentType.nbtTag;
import static net.minecraft.util.Formatting.*;
public class AutoReconnect implements ModInitializer
{
public static int attempt = 0;
public static boolean connect = false;
private static final AtomicInteger countdown = new AtomicInteger();
private static Timer timer = null;
public static final String MOD_ID = "autoreconnect";
public static int[] delayList = { 3, 10, 30, 60 };
public static int ticks = -1;
public static int attempt = -1;
public static ServerInfo lastServerEntry = null;
public static boolean pause = false;
@Override
public void onInitialize() { }
public static void startCountdown(int seconds)
public void onInitialize()
{
countdown.set(seconds);
timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask()
ClientCommands.register(literal("reload").executes(AutoReconnect::cmdReload));
ClientCommands.register(literal("config").then(argument("delayList", nbtTag()).executes(AutoReconnect::cmdConfig)));
loadConfig();
}
private static int cmdReload(CommandContext<ClientCommandSource> ctx)
{
loadConfig();
return SINGLE_SUCCESS;
}
private static int cmdConfig(CommandContext<ClientCommandSource> ctx)
{
Tag tag = ctx.getArgument("delayList", Tag.class);
try
{
@Override
public void run()
// if tag is not a list or a list not containing integers it will pass null or an empty list
setDelayList(tag instanceof AbstractListTag ? ((AbstractListTag<? extends Tag>) tag).stream().filter(IntTag.class::isInstance).map(IntTag.class::cast).mapToInt(IntTag::getInt).toArray() : null);
saveConfig();
send(colored("Current configuration: " + Arrays.toString(delayList), GREEN));
}
catch (IOException | IllegalArgumentException ex)
{
send(err(ex));
}
return SINGLE_SUCCESS;
}
private static void loadConfig()
{
Path configPath = FabricLoader.getInstance().getConfigDir().resolve(MOD_ID + ".json");
try
{
setDelayList(new Gson().fromJson(Files.newBufferedReader(configPath), int[].class));
send(colored("Current configuration: " + Arrays.toString(delayList), GREEN));
}
catch (IOException | IllegalArgumentException | JsonParseException ex)
{
send(err(ex));
try
{
if (countdown.decrementAndGet() <= 0)
{
connect = true;
cancel();
}
send(colored("Creating default config...", GREEN));
saveConfig();
}
}, 1000, 1000);
catch (IOException ex2)
{
send(err(ex2));
}
}
}
private static void cancel()
private static void saveConfig() throws IOException
{
if (timer == null) return;
timer.cancel();
timer = null;
Path configPath = FabricLoader.getInstance().getConfigDir().resolve(MOD_ID + ".json");
File configFile = configPath.toFile();
// if file already exists or could successfully be created
if (configFile.exists() || configFile.createNewFile())
{
Files.write(configPath, new Gson().toJson(delayList).getBytes());
send(colored("Saved config", GREEN));
}
}
public static void reset()
private static void setDelayList(int[] delayList) throws IllegalArgumentException
{
cancel();
attempt = 0;
connect = false;
System.out.println("reset");
// if null or empty or contains negatives or zeros
if (delayList == null || delayList.length == 0 || IntStream.of(delayList).anyMatch(i -> i <= 0))
throw new IllegalArgumentException("delayList must be a non-empty list of strictly positive integers");
AutoReconnect.delayList = delayList;
}
public static int getCountdown()
public static void resetAttempts()
{
return countdown.get();
ticks = -1;
attempt = -1;
log("reset");
}
}
public static String getMessage()
{
return attempt < 0 ? "Could not reconnect" : String.format("Reconnect in %d...", ticks / 20 + 1);
}
public static int getColor()
{
return Optional.of(attempt < 0 ? RED : GREEN).filter(Formatting::isColor).map(Formatting::getColorValue).orElse(0xFFFFFF);
}
}

View File

@ -0,0 +1,47 @@
package net.autoreconnect;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.ArgumentType;
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
import net.minecraft.client.network.ClientCommandSource;
import java.util.*;
import static net.autoreconnect.AutoReconnect.MOD_ID;
public final class ClientCommands
{
private ClientCommands() { }
private static final List<LiteralArgumentBuilder<ClientCommandSource>> commands = new ArrayList<>();
public static boolean contains(String command)
{
// checks if command starts with '<MOD_ID> ' and looks for a command literal to match the second word in the command
return command.startsWith(MOD_ID + " ") && commands.stream().map(LiteralArgumentBuilder::getLiteral).anyMatch(command.substring(MOD_ID.length() + 1).split(" ", 2)[0]::equals);
}
// register commands more or less the usual way
public static void register(LiteralArgumentBuilder<ClientCommandSource> command)
{
commands.add(command);
}
// variants of static methods literal and argument from net.minecraft.server.command.CommandManager replacing ServerCommandSource with ClientCommandSource
public static LiteralArgumentBuilder<ClientCommandSource> literal(String literal)
{
return LiteralArgumentBuilder.literal(literal);
}
public static <T> RequiredArgumentBuilder<ClientCommandSource, T> argument(String name, ArgumentType<T> type)
{
return RequiredArgumentBuilder.argument(name, type);
}
// actually register the commands and add the prefix node
public static void register(CommandDispatcher<ClientCommandSource> dispatcher)
{
commands.stream().map(literal(MOD_ID)::then).forEach(dispatcher::register);
}
}

View File

@ -0,0 +1,55 @@
package net.autoreconnect;
import net.minecraft.SharedConstants;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.text.LiteralText;
import net.minecraft.text.Text;
import net.minecraft.text.TextColor;
import net.minecraft.util.Formatting;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.util.Objects;
import static net.minecraft.util.Formatting.RED;
public class Util
{
// sends message to player if in game or logs to console with prefix '[<MOD_NAME>]'
public static void send(Text text)
{
ClientPlayerEntity player = MinecraftClient.getInstance().player;
if (player == null)
{
// if player is not in game then print to console in error or default stream depending on weather the text is red or not
Logger logger = LogManager.getLogger("AutoReconnect");
if (Objects.equals(text.getStyle().getColor(), TextColor.fromFormatting(RED)))
{
logger.error("[AutoReconnect] " + text.getString());
}
else
{
logger.info("[AutoReconnect] " + text.getString());
}
}
else player.sendMessage(text, false);
}
// logs in console with prefix '[<MOD_NAME>]'
public static void log(String format, Object... args)
{
LogManager.getLogger("AutoReconnect").info("[AutoReconnect] " + String.format(format, args));
}
// easy text creation methods
public static Text colored(String text, Formatting formatting)
{
return new LiteralText(text).formatted(formatting);
}
public static Text err(Exception ex)
{
return colored(ex.getClass().getSimpleName() + ": " + ex.getLocalizedMessage(), RED);
}
}

View File

@ -0,0 +1,40 @@
package net.autoreconnect.mixin;
import com.mojang.authlib.GameProfile;
import com.mojang.brigadier.CommandDispatcher;
import net.autoreconnect.ClientCommands;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.Screen;
import net.minecraft.client.network.ClientCommandSource;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.network.ClientConnection;
import net.minecraft.network.packet.s2c.play.CommandTreeS2CPacket;
import net.minecraft.server.command.CommandSource;
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(ClientPlayNetworkHandler.class)
public class MixinClientPlayNetworkHandler
{
@Shadow
private CommandDispatcher<CommandSource> commandDispatcher;
@SuppressWarnings("unchecked")
@Inject(method = "<init>", at = @At("RETURN"))
private void init(MinecraftClient client, Screen screen, ClientConnection connection, GameProfile profile, CallbackInfo info)
{
// register commands to the initial dispatcher
ClientCommands.register((CommandDispatcher<ClientCommandSource>) (Object) commandDispatcher);
}
@SuppressWarnings("unchecked")
@Inject(method = "onCommandTree", at = @At("TAIL"))
private void onCommandTree(CommandTreeS2CPacket packet, CallbackInfo info)
{
// register commands to the new dispatcher
ClientCommands.register((CommandDispatcher<ClientCommandSource>) (Object) commandDispatcher);
}
}

View File

@ -0,0 +1,49 @@
package net.autoreconnect.mixin;
import net.autoreconnect.ClientCommands;
import net.minecraft.client.MinecraftClient;
import net.minecraft.client.network.ClientCommandSource;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.client.network.ClientPlayerEntity;
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 static net.autoreconnect.Util.err;
import static net.autoreconnect.Util.send;
@Mixin(ClientPlayerEntity.class)
public abstract class MixinClientPlayerEntity
{
@Shadow @Final
protected MinecraftClient client;
@Shadow @Final
public ClientPlayNetworkHandler networkHandler;
@Inject(at = @At("HEAD"), method = "sendChatMessage", cancellable = true)
private void sendChatMessage(String message, CallbackInfo info)
{
if (message.charAt(0) == '/')
{
String command = message.substring(1);
if(ClientCommands.contains(command))
{
// prevent message from being sent
info.cancel();
try
{
networkHandler.getCommandDispatcher().execute(command, new ClientCommandSource(networkHandler, client));
}
catch (Exception ex)
{
send(err(ex));
}
}
}
}
}

View File

@ -6,32 +6,37 @@ import net.minecraft.client.gui.screen.DisconnectedScreen;
import net.minecraft.client.util.Window;
import net.minecraft.client.util.math.MatrixStack;
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.CallbackInfoReturnable;
import static net.autoreconnect.AutoReconnect.attempt;
import static net.autoreconnect.AutoReconnect.getCountdown;
import static net.autoreconnect.AutoReconnect.*;
@Mixin(DisconnectedScreen.class)
public class MixinDisconnectedScreen
{
@Shadow
private int reasonHeight;
// make this screen closable by pressing escape
@Inject(at = @At("RETURN"), method = "shouldCloseOnEsc", cancellable = true)
private void shouldCloseOnEsc(CallbackInfoReturnable<Boolean> info)
{
info.setReturnValue(true);
}
// render the text overlay
@Inject(at = @At("RETURN"), method = "render")
private void render(MatrixStack matrices, int mouseX, int mouseY, float delta, CallbackInfo info)
{
Window window = MinecraftClient.getInstance().getWindow();
TextRenderer renderer = MinecraftClient.getInstance().textRenderer;
String text = attempt == -1 ? "Can not reconnect!" : "Reconnecting in " + getCountdown() + "...";
String text = getMessage();
renderer.draw(matrices, text,
(window.getScaledWidth() - renderer.getWidth(text)) / 2F,
(window.getScaledHeight() - renderer.fontHeight) / 3F,
0xFF4422);
(window.getScaledWidth() - renderer.getWidth(text)) / 2F, // centered
(window.getScaledHeight() - reasonHeight) / 2F - 9 * 4, // 9 * 2 higher than the title which is 9 * 2 higher than the disconnect reason
getColor());
}
}

View File

@ -5,76 +5,76 @@ import net.minecraft.client.gui.screen.*;
import net.minecraft.client.gui.screen.multiplayer.MultiplayerScreen;
import net.minecraft.client.network.ServerInfo;
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 static net.autoreconnect.AutoReconnect.*;
import static net.autoreconnect.Util.log;
@Mixin(MinecraftClient.class)
public class MixinMinecraftClient
{
private ServerInfo lastServerEntry;
@Shadow
public Screen currentScreen;
@Inject(at = @At("HEAD"), method = "setCurrentServerEntry")
private void setCurrentServerEntry(ServerInfo info, CallbackInfo ci)
private void setCurrentServerEntry(ServerInfo serverInfo, CallbackInfo info)
{
if (info != null)
//save last known non-null server entry
if (serverInfo != null)
{
lastServerEntry = info;
lastServerEntry = serverInfo;
}
}
@Inject(at = @At("RETURN"), method = "openScreen")
private void openScreen(Screen screen, CallbackInfo info)
{
System.out.println(screen == null ? null : screen.getClass().getSimpleName());
//TODO interpret disconnect reason
//TODO revalidate session if needed
if (screen instanceof DisconnectedScreen)
{
if (attempt < 0) return;
switch (attempt++)
{
case 0:
startCountdown(3);
break;
case 1:
startCountdown(10);
break;
case 2:
startCountdown(60);
break;
case 3:
startCountdown(300);
break;
default:
attempt = -1;
}
}
else if (screen instanceof MultiplayerScreen || MinecraftClient.getInstance().player != null)
{
System.out.println(screen == null ? null : screen.getClass().getSimpleName());
//TODO find better conditions to reset
reset();
}
}
@Inject(at = @At("RETURN"), method = "tick")
@Inject(at = @At("HEAD"), method = "tick")
private void tick(CallbackInfo info)
{
//TODO find better way to call connect on main thread from after timer countdown finished
if (connect)
// if not paused, decrements countdown until its negative, succeeds if its 0
if (ticks >= 0 && --ticks == 0)
{
connect = false;
MinecraftClient mc = MinecraftClient.getInstance();
if (lastServerEntry == null)
{
attempt = -1;
return;
resetAttempts();
}
else
{
MinecraftClient mc = MinecraftClient.getInstance();
mc.openScreen(new ConnectScreen(new MultiplayerScreen(new TitleScreen()), mc, lastServerEntry));
}
}
}
@Inject(at = @At("INVOKE"), method = "openScreen")
private void openScreen(Screen newScreen, CallbackInfo info)
{
// old and new screen must not be the same type, actually happens very often for some reason
if ((currentScreen == null ? null : currentScreen.getClass()) != (newScreen == null ? null : newScreen.getClass()))
{
if (currentScreen instanceof DisconnectedScreen && ( // exited disconnect screen using...
newScreen instanceof MultiplayerScreen || // ...cancel button on disconnect screen
newScreen instanceof TitleScreen || // ...escape key
newScreen != null && newScreen.getClass().getSimpleName().equals("AuthScreen")) || // ...AuthMe re-authenticate button
(currentScreen instanceof ConnectScreen && !(newScreen instanceof DisconnectedScreen))) // connection successful or cancelled using cancel button on connect screen
{
resetAttempts();
}
// player got disconnected
else if (newScreen instanceof DisconnectedScreen)
{
// if last known server is not null and next attempt is configured
if (lastServerEntry != null && ++attempt < delayList.length)
{
ticks = delayList[attempt] * 20;
}
else
{
resetAttempts();
}
log("lastServerEntry: %s, attempt: %d", lastServerEntry == null ? "null" : lastServerEntry.name, attempt);
}
mc.disconnect();
mc.openScreen(new ConnectScreen(new MultiplayerScreen(new TitleScreen()), mc, lastServerEntry));
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 388 KiB

View File

@ -6,7 +6,9 @@
"mixins": [
],
"client": [
"MixinClientPlayNetworkHandler",
"MixinDisconnectedScreen",
"MixinClientPlayerEntity",
"MixinMinecraftClient"
],
"injectors": {

View File

@ -1,22 +1,23 @@
{
"schemaVersion": 1,
"id": "autoreconnect",
"version": "1.0.1",
"version": "1.1.0",
"name": "AutoReconnect",
"description": "This mod allows you to afk without fear of getting disconnected. It will make 4 attempts to reconnect you to the server you were disconnected from, with a delay of 3, 10, 60 and 300 seconds.",
"description": "This mod will automatically try to reconnect you back to a server if you got disconnected.\nBy default, it will make 4 attempts after 3, 10, 30 and 60 seconds.",
"authors": [
"Bstn1802"
],
"contact": {
"homepage": "https://github.com/Bstn1802",
"sources": "https://github.com/Bstn1802/AutoReconnect"
"homepage": "https://www.curseforge.com/minecraft/mc-mods/autoreconnect",
"sources": "https://github.com/Bstn1802/AutoReconnect",
"issues": "https://github.com/Bstn1802/AutoReconnect/issues"
},
"license": "CC0-1.0",
"icon": "assets/icon.png",
"icon": "assets/icon16.png",
"environment": "*",
"environment": "client",
"entrypoints": {
"main": [
"net.autoreconnect.AutoReconnect"
@ -29,9 +30,6 @@
"depends": {
"fabricloader": ">=0.7.4",
"fabric": "*",
"minecraft": "1.16.x"
},
"suggests": {
"flamingo": "*"
"minecraft": ">=1.16"
}
}