Creating a Minecraft Hacked Client: Flight
In the previous article we setup our development environment and in this video we’ll dive into our first hacks: Flight and NoFall! But to do this we first need a LOT of setup code. I want to make it easier for us to implement new hacks in the future, so I want to create a class which I can reuse for each new hack we create:
public abstract class Hack {
protected boolean enabled = false;
public void toggle() {
this.enabled = !this.enabled;
}
public boolean isEnabled() {
return this.enabled;
}
}
It is very simple and just keeps track of if this Hack is enabled, in the future we’ll expand this class but for now it’s fine. Now to keep track of all the hacks lets modify our ModInitializer
class:
public class ProfQuHack implements ModInitializer {
public static final Logger LOGGER = LoggerFactory.getLogger("profquhack");
private static final List<Hack> HACKS = new ArrayList<>();
@Override
public void onInitialize() {
// Add Hacks here e.g.
// HACKS.add(new Hack());
}
private static <T extends Hack> T getHack(Class<T> hackClass) {
for (var hack : HACKS) {
if (hack.getClass() == clazz) {
return clazz.cast(hack);
}
}
return null;
}
public static boolean isEnabled(Class<? extends Hack> hackClass) {
var hack = getHack(clazz);
return hack != null && hack.isEnabled();
}
public static void toggle(Class<? extends Hack> hackClass) {
var hack = getHack(clazz);
if (hack != null) {
hack.toggle();
}
}
public static List<Hack> getHacks() {
return HACKS;
}
}
The code is pretty self explanatory, it has some basic functionality to keep track of all our hacks, when we want to add some hack we can call HACKS.add
on it to add it to the list.
User Interface
Now we need some way to turn the hacks on and off. To do this we can add a button to the GameMenuScreen
class, which you can find in the Minecraft source code. It is just the main pause screen. To understand how we can add a button and our own custom screen for hacks let’s look at GameMenuScreen
. In this class there’s a lot going on, most of it doesn’t seem to do with creating the buttons so we’ll skip that and come back to it if the other options don’t work out. First let’s look at the init
method, which does some stuff that doesn’t seem too interesting and it calls initWidgets
, so let’s have a closer look:
@Override
protected void init() {
if (this.showMenu) {
this.initWidgets();
}
// Adding the title?
this.addDrawableChild(new TextWidget(0, this.showMenu ? 40 : 10, this.width, this.textRenderer.fontHeight, this.title, this.textRenderer));
}
private void initWidgets() {
GridWidget gridWidget = new GridWidget();
gridWidget.getMainPositioner().margin(4, 4, 4, 0);
// Creating an 'adder'...
GridWidget.Adder adder = gridWidget.createAdder(2);
adder.add(ButtonWidget.builder(RETURN_TO_GAME_TEXT, button -> {
this.client.setScreen(null);
this.client.mouse.lockCursor();
}).width(204).build(), 2, gridWidget.copyPositioner().marginTop(50));
// Calling 'this.createButton', apparently creating buttons for new menus
adder.add(this.createButton(ADVANCEMENTS_TEXT, () -> new AdvancementsScreen(this.client.player.networkHandler.getAdvancementHandler())));
adder.add(this.createButton(STATS_TEXT, () -> new StatsScreen(this, this.client.player.getStatHandler())));
adder.add(this.createUrlButton(SEND_FEEDBACK_TEXT, SharedConstants.getGameVersion().isStable() ? "https://aka.ms/javafeedback?ref=game" : "https://aka.ms/snapshotfeedback?ref=game"));
adder.add(this.createUrlButton((Text)GameMenuScreen.REPORT_BUGS_TEXT, (String)"https://aka.ms/snapshotbugs?ref=game")).active = !SharedConstants.getGameVersion().getSaveVersion().isNotMainSeries();
adder.add(this.createButton(OPTIONS_TEXT, () -> new OptionsScreen(this, this.client.options)));
if (this.client.isIntegratedServerRunning() && !this.client.getServer().isRemote()) {
adder.add(this.createButton(SHARE_TO_LAN_TEXT, () -> new OpenToLanScreen(this)));
} else {
adder.add(this.createButton(PLAYER_REPORTING_TEXT, SocialInteractionsScreen::new));
}
Text text = this.client.isInSingleplayer() ? RETURN_TO_MENU_TEXT : ScreenTexts.DISCONNECT;
// 'exit' button, so probably the 'DONE' button
this.exitButton = adder.add(ButtonWidget.builder(text, button -> {
button.active = false;
this.client.getAbuseReportContext().tryShowDraftScreen(this.client, this, this::disconnect, true);
}).width(204).build(), 2);
// Does some stuff with 'gridWidget'
gridWidget.refreshPositions();
SimplePositioningWidget.setPos(gridWidget, 0, 0, this.width, this.height, 0.5f, 0.25f);
// Then adds all buttons as drawable children to be able to draw them most likely
gridWidget.forEachChild(this::addDrawableChild);
}
I think the createButton
function is worth looking more into because it could make a button to take us to our hacking screen so let’s take a closer look:
private ButtonWidget createButton(Text text, Supplier<Screen> screenSupplier) {
return ButtonWidget.builder(
text,
button -> this.client.setScreen((Screen)screenSupplier.get())
).width(98).build();
}
It seems like it creates a button and uses a setter to set the client’s screen. If we can create our own screen we might be able to set the screen of the client to it. But first let’s see if we can’t add a button to the pause screen. For that I’ve written the GameMenuScreenMixin
class, here’s the code:
@Mixin(GameMenuScreen.class)
public abstract class GameMenuScreenMixin extends Screen {
protected GameMenuScreenMixin(Text title) {
super(title);
}
@Inject(method="initWidgets", at=@At("HEAD"))
void initWidgets(CallbackInfo ci) {
var buttonWidget = ButtonWidget.builder(
Text.of("Hacks"),
button -> {
if (MinecraftClient.getInstance() != null) {
MinecraftClient.getInstance().setScreen(new HackScreen(this));
}
}
).position(10, 10).build();
this.addDrawableChild(buttonWidget);
}
}
I just inject my own initWidgets
code at the head of the GameMenuScreen.initWidgets
function. It creates a buttonWidget
from the ButtonWidget.builder
and adds it as a drawable child. Running a client and joining our Paper server when we press ESC we can see our button in-game:
I’ve already given a sneak peek at the HackScreen
class so let’s look at it in closer detail. Let’s find the Minecraft OptionsScreen
class and basically copy the code with while removing most of the code, only leaving the bare-bones.
public class HackScreen extends Screen {
private final Screen parent;
private final Map<ButtonWidget, Class<? extends Hack>> hackButtons = new HashMap<>();
public HackScreen(Screen parent) {
super(Text.translatable("options.title"));
this.parent = parent;
}
@Override
protected void init() {
GridWidget gridWidget = new GridWidget();
gridWidget.getMainPositioner().marginX(5).marginBottom(4).alignHorizontalCenter();
GridWidget.Adder adder = gridWidget.createAdder(2);
adder.add(ButtonWidget.builder(ScreenTexts.DONE, button -> this.client.setScreen(this.parent)).width(200).build(), 2, adder.copyPositioner().marginTop(6));
gridWidget.refreshPositions();
SimplePositioningWidget.setPos(gridWidget, 0, this.height / 6 - 12, this.width, this.height, 0.5f, 0.0f);
gridWidget.forEachChild(this::addDrawableChild);
}
}
This is basically the bare bones of the OptionsScreen
class and if we want to add some buttons we can do so with the adder
but first see if it works in-game:
We have a single Done button that takes us back to the main menu, how exciting! Now let’s create some more buttons! We want to keep track of whether the hacks are enabled or not. To do so we want to keep track of the buttons to use them later. We can do this with a Map
of a ButtonWidget
and the Class<? extends Hack
which the button gets its updates from. Then we can add this simple piece of code to add all Hacks automatically to it:
for (var hack : ProfQuHack.getHacks()) {
this.hackButtons.put(adder.add(ButtonWidget.builder(
Text.empty(),
button -> ProfQuHack.toggle(hack.getClass())
).build()),
hack.getClass()
);
}
Then finally we have to update the text, we can do this is a tick
method but it is probably better to overwrite the render
method to change the text right before rendering it.
@Override
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
// Change the text on the buttons depending on if the hacks are enabled
for (var entry : this.hackButtons.entrySet()) {
var button = entry.getKey();
var clazz = entry.getValue();
button.setMessage(Text.of(enabledText(clazz)));
}
super.render(context, mouseX, mouseY, delta);
}
/**
* A simple utility function to generate the enabled text of a hack
* @param clazz the clazz to get the enabled text for
* @return the generated text
*/
private Text enabledText(Class<? extends Hack> clazz) {
var text = clazz.getSimpleName() + " is ";
text += ProfQuHack.isEnabled(clazz) ? "enabled" : "disabled";
return Text.of(text);
}
During this I also learned about JavaDoc comments to describe your code I found this guide from Oracle on how to write them and I think they are interesting. There isn’t a lot to learn about them and everyone has a different way to write them but it’s still helpful to know about them. And IntelliJ IDEA provides a ton of functionality to help creating them.
Flight
public class Flight extends Hack {
@Override
public void toggle() {
super.toggle();
var player = MinecraftClient.getInstance().player;
if (player == null)
return;
player.getAbilities().allowFlying = this.enabled;
}
}
Using this we can then join our server again and see that the hack has been added:
When we enable it we can fly around! But, when we fly around for too long we get:
When we search this with CTRL+SHIFT+F and selecting Scope we see that it is defined by multiplayer.disconnect.flying
when we then search this we find two references:
if (this.floating && !this.player.isSleeping() && !this.player.hasVehicle() && !this.player.isDead()) {
if (++this.floatingTicks > 80) {
LOGGER.warn("{} was kicked for floating too long!", (Object)this.player.getName().getString());
this.disconnect(Text.translatable("multiplayer.disconnect.flying"));
return;
}
}
if (this.vehicleFloating && this.player.getRootVehicle().getControllingPassenger() == this.player) {
if (++this.vehicleFloatingTicks > 80) {
LOGGER.warn("{} was kicked for floating a vehicle too long!", (Object)this.player.getName().getString());
this.disconnect(Text.translatable("multiplayer.disconnect.flying"));
return;
}
}
In ServerPlayNetworkHandler
, in this case we want to take a look at the top one because that probably references the player because the other one references a vehicles. So let’s think about how we can get around this.
We have a few checks which we could get around:
this.floating &&
!this.player.isSleeping() &&
!this.player.hasVehicle() &&
!this.player.isDead() &&
++this.floatingTicks > 80
We can easily cross out a few, like isSleeping
(can’t during the day), hasVehicle
(trying to fly without vehicle) and isDead
(being dead is bad) because we don’t want to be any of those things when we are flying. Looking at floatingTicks
reveals that it is only incremented and set to 0 when we aren’t doing one of the first 4 conditions. This means that the condition we should be looking at is floating
searching for it shows this monstrosity of an expression:
this.floating = s >= -0.03125 &&
!bl22 &&
this.player.interactionManager.getGameMode() != GameMode.SPECTATOR &&
!this.server.isFlightEnabled() &&
!this.player.getAbilities().allowFlying &&
!this.player.hasStatusEffect(StatusEffects.LEVITATION) &&
!this.player.isFallFlying() &&
!this.player.isUsingRiptide() &&
this.isEntityOnAir(this.player);
Looking through you might think ‘Why does this trigger at all? Didn’t we set allowFlying
to true?’ and we did but this code is run on the server, not on the client. That’s why that specific check fails. The other checks also aren’t that exploitable. Our GameMode isn’t Spectator, I’ll assume flying isn’t enabled on the server, we can’t easily get Levitation, FallFlying
refers to using an Elytra, which we would like to fly without, the same for Riptide on a Trident and we are in the air. That leaves two conditions, the one with s
and the one with bl22
. Apparently bl22
is basically ‘Is the player on the ground?’ but I think s
is more exploitable. Turns out s
is basically m
which is basically the vertical movement. Therefore if we can fall enough every so often we won’t be kicked!
Let’s implement it, but to do that we need some way to trigger our Flight hack every tick. For that we can use ClientTickEvents
:
ClientTickEvents.START_CLIENT_TICK.register(client ->
HACKS.forEach(hack -> {
if (hack.isEnabled())
hack.tick();
}
)
);
With this code inside our onInitialize
function we’ll be able to tick
our hacks every, well, tick. Now we’ll have to implement our tick
method in Flight:
private static final double FALL_DIST = 0.04;
private static final int MAX_FLOATING_TICKS = 70;
private double previousY;
private int floatingTicks;
@Override
public void tick() {
var player = MinecraftClient.getInstance().player;
if (player == null)
return;
if (!player.getAbilities().flying)
return;
if (player.getY() >= this.previousY - MAX_FALL_DIST)
this.floatingTicks++;
if (floatingTicks >= MAX_FLOATING_TICKS) {
double newY = player.getY() - MAX_FALL_DIST;
player.setPos(player.getX(), newY, player.getZ());
this.floatingTicks = 0;
}
this.previousY = player.getY();
}
This code works when not flying up, but I want to be able to fly up indefinitely so I’ll add a yDifference
variable to make up for that as well:
double yDifference = Math.max(player.getY() - this.previousY, 0);
double newY = player.getY() - FALL_DIST - yDifference;
This makes it so that the player falls by at least FALL_DIST, which is just a bit higher than the value in ServerPlayNetworkHandler
to make sure it works. There is just one problem now: that I keep going down, which is the point of course, but I’d rather not. Therefore I updated the code to instead of setting the position locally I’ll send a packet. When I search for ‘sendpacket’ one of the results is this line:
this.player.networkHandler.sendPacket(new BlockUpdateS2CPacket(world, pos));
Perhaps there is some packet that is sent to update the position, I eventually found this line after some searching around:
this.networkHandler.sendPacket(new PlayerMoveC2SPacket.PositionAndOnGround(this.getX(), this.getY(), this.getZ(), this.isOnGround()));
With that in mind, I tried this instead of player.setPos
:
player.networkHandler.sendPacket(
new PlayerMoveC2SPacket.PositionAndOnGround(player.getX(), newY, player.getZ(), player.isOnGround())
);
When we do this, it still kicks us however, so I tried to send it for multiple ticks after each other. I played with using the sendPacket
method and other methods that are similar a lot, but I couldn’t get them to work, so I searched how other clients implemented it. I looked at the meteorclient repository and searched for ‘Flight’ and found a class that extends Module. I looked around and finally determined that this function is probably what changes the packet:
private void antiKickPacket(PlayerMoveC2SPacket packet, double currentY) {
// maximum time we can be "floating" is 80 ticks, so 4 seconds max
if (this.delayLeft <= 0 && this.lastPacketY != Double.MAX_VALUE &&
shouldFlyDown(currentY, this.lastPacketY) && isEntityOnAir(mc.player)) {
// actual check is for >= -0.03125D, but we have to do a bit more than that
// due to the fact that it's a bigger or *equal* to, and not just a bigger than
((PlayerMoveC2SPacketAccessor) packet).setY(lastPacketY - 0.03130D);
} else {
lastPacketY = currentY;
}
}
It is pretty complicated but the gist seems to be that once they’ve passed our MAX_FLOATING_TICKS
they change the next movement packet. I implemented a way for us to easily modify packets as well, first I added a Mixin to the ClientConnection
class:
@Mixin(ClientConnection.class)
public abstract class ClientConnectionMixin {
@Inject(method="send(Lnet/minecraft/network/packet/Packet;Lnet/minecraft/network/PacketCallbacks;Z)V", at=@At("HEAD"))
private void send(Packet<?> packet, @Nullable PacketCallbacks callbacks, boolean flush, CallbackInfo ci) {
ProfQuHack.getHacks().forEach(hack -> {
if (hack.isEnabled())
hack.modifyPacket(packet);
});
}
}
And then just add a modifyPacket(Packet<?> packet)
function to handle it. To do that I need to modify the y
variable in the PlayerMoveC2SPacket
so I googled ‘fabric mixin change private variables’. I found this link from the Fabric Wiki which gave me some more information. I tried it by doing this:
@Mixin(PlayerMoveC2SPacket.class)
public interface PlayerMoveC2SPacketAccessor {
@Accessor("y")
void setY(double y);
}
However that crashed me with this error: Update to non-static final field y attempted from a different method (setY) than the initializer method <init>
. I searched then fabric change final field and came along this link from the Fabric Wiki and there I found this:
Mutable
Mutable should be used when you want to mutate a final field.
Which is exactly what I wanted! So I added the @Mutable
decoration and now we can create the modifyPacket
function:
@Override
public void modifyPacket(Packet<?> packet) {
if (!(packet instanceof PlayerMoveC2SPacket))
return;
if (floatingTicks >= MAX_FLOATING_TICKS) {
((PlayerMoveC2SPacketAccessor) packet).setY(this.previousY - FALL_DIST);
floatingTicks = 0;
}
}
We can now fly without jittering up and down! There is however one more thing that annoys me. When you die or go to another server the Flight hack will stay on, but you won’t be able to fly anymore. For this reason I’ve decided to add the line player.getAbilities().allowFlying = this.enabled;
to the tick method to always set it, if need be.
NoFall
When I fly, if I just stop flying while high above the ground I fall to the ground and die, that’s not optimal. However, I have no clue as to how to implement something like this, so… to GitHub! I searched the meteor-client repository again to find anything with NoFall. I found a module about it and it seems to work with packets:
@EventHandler
private void onSendPacket(PacketEvent.Send event) {
if (mc.player.getAbilities().creativeMode
|| !(event.packet instanceof PlayerMoveC2SPacket)
|| mode.get() != Mode.Packet
|| ((IPlayerMoveC2SPacket) event.packet).getTag() == 1337) return;
if (!Modules.get().isActive(Flight.class)) {
if (mc.player.isFallFlying()) return;
if (mc.player.getVelocity().y > -0.5) return;
((PlayerMoveC2SPacketAccessor) event.packet).setOnGround(true);
} else {
((PlayerMoveC2SPacketAccessor) event.packet).setOnGround(true);
}
}
It seems to set onGround
in the PlayerMoveC2SPacket
to true if certain conditions are met. It seems like it first checks if the player is flying with the hack and if it is, always set it to true. If it isn’t it first checks for an elytra, which after some testing makes sense as you can’t fly if that is set. And I assume the last is because the damage would otherwise be too great. However to set the onGround
variable he uses an Accessor, but we just learned about it so it’s a piece of cake: add @Mutable
and @Accessor
and it’s done! When testing we can now easily fly everyone and not have to worry about fall damage.
That was the first real implementation of some hacks! If you followed all of that you can give yourself a pat on the back, if you didn’t follow all of that you are free to ask me any questions and I’ll see what answers I can come up with.