Creating a Minecraft Hacked Client: Flight

ProfessorQu
11 min readApr 1, 2024

--

Flying in Survival

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:

Our Hacks button

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:

HackScreen

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:

Hack screen with Flight hack

When we enable it we can fly around! But, when we fly around for too long we get:

Kicked for flying

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.

--

--

ProfessorQu

I'm a student who is getting a degree in Computer Science avid about learning and wanting to take others along on the journey!