Skip to content
Closed
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
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ dependencies {

modCompileOnly "com.viaversion:viafabricplus-api:${project.viafabricplus_version}"

include implementation("com.github.Vatuu:discord-rpc:${project.discordrpc_version}")
include implementation("com.github.JoyelTheDev:discord-rpc:${project.discordrpc_version}")

}

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,6 @@ adventure_version=4.17.0
viafabricplus_version=4.4.8
minecraftauth_version=4.1.1

discordrpc_version=1.6.2
discordrpc_version=v1

examination_version=1.3.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package me.madeq.client.command.suggestion;

public record CommandSuggestion(String completion, String label, String description) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
package me.madeq.client.command.suggestion;

import me.madeq.client.LiteClient;
import me.madeq.client.command.Command;
import me.madeq.client.command.CommandArgument;
import me.madeq.client.command.CommandManager;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.EditBox;
import org.lwjgl.glfw.GLFW;

import java.util.ArrayList;
import java.util.List;

public class CommandSuggestionManager {

private static final int ROW_HEIGHT = 12;
private static final int PADDING_X = 4;
private static final int PADDING_Y = 3;
private static final int MAX_VISIBLE = 8;

private static final int COLOR_BG = 0xCC101010;
private static final int COLOR_HIGHLIGHT = 0xCC1A4A7A;
private static final int COLOR_BORDER = 0xFF2A6AB0;
private static final int COLOR_LABEL = 0xFFFFFFFF;
private static final int COLOR_DESCRIPTION = 0xFFAAAAAA;
private static final int COLOR_ARG_HINT = 0xFF55AAFF;
private static final int COLOR_SEPARATOR = 0xFF333333;

private final List<CommandSuggestion> suggestions = new ArrayList<>();
private int selectedIndex = 0;
private String lastInput = "";

public void updateIfChanged(String currentInput) {
if (!currentInput.equals(lastInput)) {
lastInput = currentInput;
recompute(currentInput);
}
}

public void render(GuiGraphics graphics, EditBox input, int screenWidth, int screenHeight) {
if (suggestions.isEmpty()) return;

Minecraft mc = Minecraft.getInstance();
int fontHeight = mc.font.lineHeight;

int visibleCount = Math.min(suggestions.size(), MAX_VISIBLE);
int scrollOffset = computeScrollOffset(visibleCount);
int popupWidth = computePopupWidth(mc, visibleCount, scrollOffset);
int popupHeight = visibleCount * ROW_HEIGHT + PADDING_Y * 2;
int inputY = input.getY();
int popupX = input.getX();
int popupY = inputY - popupHeight - 2;

if (popupY < 2) popupY = inputY + input.getHeight() + 2;

graphics.fill(popupX - 1, popupY - 1, popupX + popupWidth + 1, popupY + popupHeight + 1, COLOR_BORDER);
graphics.fill(popupX, popupY, popupX + popupWidth, popupY + popupHeight, COLOR_BG);

for (int i = 0; i < visibleCount; i++) {
int globalIdx = scrollOffset + i;
CommandSuggestion suggestion = suggestions.get(globalIdx);

int rowY = popupY + PADDING_Y + i * ROW_HEIGHT;

if (globalIdx == selectedIndex) {
graphics.fill(popupX, rowY - 1, popupX + popupWidth, rowY + ROW_HEIGHT - 1, COLOR_HIGHLIGHT);
}

if (i < visibleCount - 1) {
graphics.fill(popupX + 2, rowY + ROW_HEIGHT - 2, popupX + popupWidth - 2, rowY + ROW_HEIGHT - 1, COLOR_SEPARATOR);
}

int textY = rowY + (ROW_HEIGHT - fontHeight) / 2;
int cursorX = popupX + PADDING_X;

graphics.drawString(mc.font, suggestion.label(), cursorX, textY, COLOR_LABEL, false);
cursorX += mc.font.width(suggestion.label()) + 4;

if (!suggestion.description().isEmpty()) {
String desc = "- " + suggestion.description();
graphics.drawString(mc.font, desc, cursorX, textY, COLOR_DESCRIPTION, false);
cursorX += mc.font.width(desc) + 6;
}

if (globalIdx == selectedIndex) {
String argHint = buildArgHint(suggestion.completion());
if (!argHint.isEmpty()) {
graphics.drawString(mc.font, argHint, cursorX, textY, COLOR_ARG_HINT, false);
}
}
}

if (suggestions.size() > MAX_VISIBLE) {
String indicator = (scrollOffset + 1) + "-" + (scrollOffset + visibleCount) + "/" + suggestions.size();
int indX = popupX + popupWidth - mc.font.width(indicator) - PADDING_X;
int indY = popupY + popupHeight - ROW_HEIGHT / 2 - mc.font.lineHeight / 2;
graphics.drawString(mc.font, indicator, indX, indY, COLOR_DESCRIPTION, false);
}
}

public boolean handleKeyPress(int keyCode, EditBox input) {
if (suggestions.isEmpty()) return false;

return switch (keyCode) {
case GLFW.GLFW_KEY_UP -> {
selectedIndex = (selectedIndex - 1 + suggestions.size()) % suggestions.size();
yield true;
}
case GLFW.GLFW_KEY_DOWN -> {
selectedIndex = (selectedIndex + 1) % suggestions.size();
yield true;
}
case GLFW.GLFW_KEY_TAB -> {
applySuggestion(input, suggestions.get(selectedIndex));
yield true;
}
case GLFW.GLFW_KEY_ESCAPE -> {
suggestions.clear();
yield true;
}
default -> false;
};
}

public boolean handleMouseClick(double mouseX, double mouseY, EditBox input, int screenWidth, int screenHeight) {
if (suggestions.isEmpty()) return false;

Minecraft mc = Minecraft.getInstance();
int visibleCount = Math.min(suggestions.size(), MAX_VISIBLE);
int scrollOffset = computeScrollOffset(visibleCount);
int popupWidth = computePopupWidth(mc, visibleCount, scrollOffset);
int popupHeight = visibleCount * ROW_HEIGHT + PADDING_Y * 2;
int popupX = input.getX();
int inputY = input.getY();
int popupY = inputY - popupHeight - 2;

if (popupY < 2) popupY = inputY + input.getHeight() + 2;

if (mouseX < popupX || mouseX > popupX + popupWidth) return false;
if (mouseY < popupY || mouseY > popupY + popupHeight) return false;

int relY = (int) mouseY - popupY - PADDING_Y;
int clickedRow = relY / ROW_HEIGHT;
int globalIdx = scrollOffset + clickedRow;

if (globalIdx >= 0 && globalIdx < suggestions.size()) {
selectedIndex = globalIdx;
applySuggestion(input, suggestions.get(globalIdx));
return true;
}
return false;
}

private void recompute(String input) {
suggestions.clear();
selectedIndex = 0;

CommandManager cm = LiteClient.getCommandManager();
String prefix = cm.getPrefix();

if (!input.startsWith(prefix)) return;

String afterPrefix = input.substring(prefix.length());
String[] parts = afterPrefix.split(" ", -1);

if (parts.length == 0) return;

String typedCommandName = parts[0].toLowerCase();

if (parts.length == 1) {
for (Command cmd : cm.getCommands()) {
if (!cmd.isVisibleInHelp()) continue;
if (cmd.getName().toLowerCase().startsWith(typedCommandName)) {
suggestions.add(new CommandSuggestion(
prefix + cmd.getName(),
prefix + cmd.getName(),
cmd.getDescription()
));
}
}
} else {
Command cmd = findCommand(cm, typedCommandName);
if (cmd == null) return;

int argIndex = parts.length - 2;
if (argIndex < 0 || argIndex >= cmd.getArguments().size()) return;

CommandArgument arg = cmd.getArguments().get(argIndex);
if (!arg.hasOptions()) return;

String typedArg = parts[parts.length - 1].toLowerCase();
String baseCompletion = prefix + typedCommandName + " "
+ String.join(" ", java.util.Arrays.copyOfRange(parts, 1, parts.length - 1));
if (parts.length > 2) baseCompletion += " ";

for (String option : arg.options()) {
if (option.toLowerCase().startsWith(typedArg)) {
suggestions.add(new CommandSuggestion(
baseCompletion + option,
option,
arg.name() + " option"
));
}
}
}
}

private void applySuggestion(EditBox input, CommandSuggestion suggestion) {
input.setValue(suggestion.completion() + " ");
input.moveCursorToEnd(false);
recompute(input.getValue());
lastInput = input.getValue();
}

private Command findCommand(CommandManager cm, String name) {
return cm.getCommands().stream()
.filter(c -> c.getName().equalsIgnoreCase(name))
.findFirst()
.orElse(null);
}

private String buildArgHint(String completion) {
CommandManager cm = LiteClient.getCommandManager();
String prefix = cm.getPrefix();

String afterPrefix = completion.startsWith(prefix)
? completion.substring(prefix.length()).trim()
: completion.trim();
String commandName = afterPrefix.split(" ")[0];

Command cmd = findCommand(cm, commandName);
if (cmd == null || cmd.getArguments().isEmpty()) return "";

StringBuilder hint = new StringBuilder(" ");
for (CommandArgument arg : cmd.getArguments()) {
hint.append("<").append(arg.name()).append(":").append(arg.type().getDisplayName()).append("> ");
}
return hint.toString().stripTrailing();
}

private int computeScrollOffset(int visibleCount) {
int offset = 0;
if (selectedIndex >= visibleCount) {
offset = selectedIndex - visibleCount + 1;
}
return offset;
}

private int computePopupWidth(Minecraft mc, int visibleCount, int scrollOffset) {
int maxWidth = 120;
for (int i = 0; i < visibleCount; i++) {
CommandSuggestion s = suggestions.get(scrollOffset + i);
int w = PADDING_X * 2
+ mc.font.width(s.label())
+ (s.description().isEmpty() ? 0 : 4 + mc.font.width("- " + s.description()));
if (w > maxWidth) maxWidth = w;
}
return maxWidth;
}
}
20 changes: 18 additions & 2 deletions src/client/java/me/madeq/client/hud/HudManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.client.multiplayer.ServerData;
import net.minecraft.world.entity.player.Player;

public class HudManager {
private static final int X = 8;
Expand All @@ -37,7 +38,9 @@ public void render(GuiGraphics graphics) {
List<HudLine> lines = List.of(
new HudLine("IP", getServerIp(minecraft)),
new HudLine("Engine", getEngine(minecraft)),
new HudLine("Last Packet", getLastPacketText())
new HudLine("Last Packet", getLastPacketText()),
new HudLine("XYZ", getCoordinates(minecraft)),
new HudLine("FPS", getFps(minecraft))
);
int width = getPanelWidth(font, lines);
int height = HEADER_HEIGHT + PADDING + lines.size() * ROW_HEIGHT + PADDING;
Expand Down Expand Up @@ -123,6 +126,19 @@ private double getPacketAgeSeconds() {
return (System.currentTimeMillis() - lastPacketTimeMillis) / 1000.0D;
}

private String getCoordinates(Minecraft minecraft) {
Player player = minecraft.player;
if (player == null) {
return "N/A";
}
return String.format(Locale.ROOT, "%.1f / %.1f / %.1f",
player.getX(), player.getY(), player.getZ());
}

private String getFps(Minecraft minecraft) {
return minecraft.getFps() + " fps";
}

private record HudLine(String label, String value) {
}
}
}
49 changes: 49 additions & 0 deletions src/client/java/me/madeq/client/mixin/ChatScreenMixin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package me.madeq.client.mixin;

import me.madeq.client.command.suggestion.CommandSuggestionManager;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.screens.ChatScreen;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
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;

@Mixin(ChatScreen.class)
public class ChatScreenMixin {

@Shadow
protected EditBox input;

@Unique
private final CommandSuggestionManager liteclient$suggestionManager = new CommandSuggestionManager();

@Inject(method = "render", at = @At("HEAD"))
private void liteclient$updateSuggestions(GuiGraphics graphics, int mouseX, int mouseY, float delta, CallbackInfo ci) {
liteclient$suggestionManager.updateIfChanged(input.getValue());
}

@Inject(method = "render", at = @At("TAIL"))
private void liteclient$renderSuggestions(GuiGraphics graphics, int mouseX, int mouseY, float delta, CallbackInfo ci) {
ChatScreen self = (ChatScreen)(Object)this;
liteclient$suggestionManager.render(graphics, input, self.width, self.height);
}

@Inject(method = "keyPressed", at = @At("HEAD"), cancellable = true)
private void liteclient$onKeyPressed(int keyCode, int scanCode, int modifiers, CallbackInfoReturnable<Boolean> cir) {
if (liteclient$suggestionManager.handleKeyPress(keyCode, input)) {
cir.setReturnValue(true);
}
}

@Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true)
private void liteclient$onMouseClicked(double mouseX, double mouseY, int button, CallbackInfoReturnable<Boolean> cir) {
ChatScreen self = (ChatScreen)(Object)this;
if (liteclient$suggestionManager.handleMouseClick(mouseX, mouseY, input, self.width, self.height)) {
cir.setReturnValue(true);
}
}
}
1 change: 1 addition & 0 deletions src/client/resources/liteclient.client.mixins.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"client": [
"AbstractWidgetMixin",
"ChatComponentMixin",
"ChatScreenMixin",
"ClientHandshakePacketListenerImplMixin",
"ClientPacketListenerMixin",
"ConnectScreenMixin",
Expand Down
Loading