diff --git a/build.gradle.kts b/build.gradle.kts index 15be5ea..80ce6ac 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,7 +25,7 @@ loom { log4jConfigs.from(file("log4j2.xml")) launchConfigs { "client" { - // If you don't want mixins, remove these lines + // If you don't want ms, remove these lines property("mixin.debug", "true") arg("--tweakClass", "org.spongepowered.asm.launch.MixinTweaker") } @@ -107,6 +107,9 @@ dependencies { // Spotify API shadowImpl("com.github.LabyStudio:java-spotify-api:+:all") + // Discord RPC + shadowImpl("com.github.MinnDevelopment:java-discord-rpc:v2.0.2") + } // Tasks: diff --git a/gradle.properties b/gradle.properties index 77c0486..11473a9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,4 +3,4 @@ org.gradle.jvmargs=-Xmx2g baseGroup = com.github.jtxofficial.flustclient mcVersion = 1.8.9 modid = flustclient -version = beta-v0.1 +version = v0.1.1-beta diff --git a/src/main/java/com/github/jtxofficial/flustclient/FlustClient.java b/src/main/java/com/github/jtxofficial/flustclient/FlustClient.java index bd578d2..d25f008 100644 --- a/src/main/java/com/github/jtxofficial/flustclient/FlustClient.java +++ b/src/main/java/com/github/jtxofficial/flustclient/FlustClient.java @@ -10,6 +10,7 @@ import com.github.jtxofficial.flustclient.manager.ModuleManager; import com.github.jtxofficial.flustclient.manager.HudManager; import com.github.jtxofficial.flustclient.manager.PlayerManager; +import com.github.jtxofficial.flustclient.manager.DiscordRPCManager; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.event.FMLInitializationEvent; @@ -19,7 +20,7 @@ public class FlustClient { public static final String MODID = "flustclient"; - public static final String VERSION = "beta-v0.1"; + public static final String VERSION = "v0.1.1-beta"; public static final String NAME = "FlustClient"; @Mod.Instance(MODID) @@ -30,6 +31,7 @@ public class FlustClient { private HudManager hudManager; private PlayerManager playerManager; private SocketEventHandler socketEventHandler; + private DiscordRPCManager discordRPCManager; @Mod.EventHandler public void preInit(FMLPreInitializationEvent event) { @@ -50,11 +52,13 @@ public void init(FMLInitializationEvent event) { moduleManager = new ModuleManager(); hudManager = new HudManager(); playerManager = new PlayerManager(); + discordRPCManager = new DiscordRPCManager(); // Initialize managers moduleManager.init(); hudManager.init(); playerManager.init(); + discordRPCManager.init(); // Register keybindings (must be done before event handler registration) KeyHandler.registerKeybindings(); diff --git a/src/main/java/com/github/jtxofficial/flustclient/feature/hud/Scoreboard.java b/src/main/java/com/github/jtxofficial/flustclient/feature/hud/Scoreboard.java new file mode 100644 index 0000000..8bde297 --- /dev/null +++ b/src/main/java/com/github/jtxofficial/flustclient/feature/hud/Scoreboard.java @@ -0,0 +1,241 @@ +package com.github.jtxofficial.flustclient.feature.hud; + +import com.github.jtxofficial.flustclient.api.modules.FlustModule; +import com.github.jtxofficial.flustclient.api.modules.HudModule; +import com.github.jtxofficial.flustclient.api.modules.annotations.HudInfo; +import com.github.jtxofficial.flustclient.api.modules.annotations.ModInfo; +import com.github.jtxofficial.flustclient.api.modules.category.EnumModuleCategory; +import com.github.jtxofficial.flustclient.config.ConfigProperty; +import com.github.jtxofficial.flustclient.manager.hud.HudElement; +import net.minecraft.scoreboard.Score; +import net.minecraft.scoreboard.ScoreObjective; +import net.minecraft.scoreboard.ScorePlayerTeam; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.network.FMLNetworkEvent; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@ModInfo(fileName = "Scoreboard") +public class Scoreboard extends FlustModule implements HudModule { + + private static final int MAX_SCOREBOARD_LINES = 15; + private static final int LINE_SPACING = 2; + private static final int HORIZONTAL_PADDING = 10; + private static final int TEXT_PADDING = 2; + private static final int TITLE_OFFSET = 1; + private static final int TITLE_BOTTOM_SPACING = 4; + private static final int TEXT_COLOR = 0xFFFFFF; + + @ConfigProperty(settingName = "Hide Scoreboard") + public boolean hideScoreboard = false; + + private transient ScoreObjective scoreObjective; + private transient HudElement hudElement; + private HudInfo hudInfo; + + public Scoreboard() { + super(); + this.hudInfo = new HudInfo(0.8, 0.02, 120, 100, 2.0f); + } + + @SubscribeEvent + public void onDisconnect(FMLNetworkEvent.ClientDisconnectionFromServerEvent event) { + this.scoreObjective = null; + } + + @Override + public void renderHudElement() { + if (shouldSkipRendering()) { + return; + } + + net.minecraft.scoreboard.Scoreboard scoreboard = scoreObjective.getScoreboard(); + Collection scores = scoreboard.getSortedScores(scoreObjective); + + List processedScores = filterAndLimitScores(scores); + List formattedLines = formatScoreLines(scoreboard, processedScores); + + renderScoreboard(scoreObjective.getDisplayName(), formattedLines, processedScores); + } + + private boolean shouldSkipRendering() { + return hideScoreboard || scoreObjective == null || mc.theWorld == null; + } + + private List filterAndLimitScores(Collection scores) { + List filteredScores = scores.stream() + .filter(this::isValidScore) + .collect(Collectors.toList()); + + if (filteredScores.size() > MAX_SCOREBOARD_LINES) { + filteredScores = filteredScores.subList(0, MAX_SCOREBOARD_LINES); + } + + Collections.reverse(filteredScores); + return filteredScores; + } + + private boolean isValidScore(Score score) { + return score != null + && score.getPlayerName() != null + && !score.getPlayerName().startsWith("#"); + } + + private List formatScoreLines(net.minecraft.scoreboard.Scoreboard scoreboard, List scores) { + List lines = new ArrayList<>(); + for (Score score : scores) { + String formattedName = ScorePlayerTeam.formatPlayerName( + scoreboard.getPlayersTeam(score.getPlayerName()), + score.getPlayerName() + ); + lines.add(formattedName); + } + return lines; + } + + private void renderScoreboard(String title, List lines, List scores) { + int width = calculateScoreboardWidth(title, lines); + int height = calculateScoreboardHeight(lines.size()); + + updateHudDimensions(width, height); + + int absoluteX = getHudAbsoluteX(); + int absoluteY = getHudAbsoluteY(); + + renderTitle(title, absoluteX, absoluteY, width); + renderLines(lines, absoluteX, absoluteY); + } + + private int calculateScoreboardWidth(String title, List lines) { + int maxWidth = mc.fontRendererObj.getStringWidth(title); + + for (String line : lines) { + int lineWidth = mc.fontRendererObj.getStringWidth(line); + if (lineWidth > maxWidth) { + maxWidth = lineWidth; + } + } + + return maxWidth + HORIZONTAL_PADDING; + } + + private int calculateScoreboardHeight(int lineCount) { + int lineHeight = mc.fontRendererObj.FONT_HEIGHT; + return lineHeight * (lineCount + 1) + (lineCount + 1) * LINE_SPACING; + } + + private void updateHudDimensions(int width, int height) { + if (hudElement != null) { + hudElement.setWidth(width); + hudElement.setHeight(height); + } + + hudInfo.setWidth(width); + hudInfo.setHeight(height); + } + + private int getHudAbsoluteX() { + return hudElement != null ? hudElement.getX() : 0; + } + + private int getHudAbsoluteY() { + return hudElement != null ? hudElement.getY() : 0; + } + + private void renderTitle(String title, int x, int y, int width) { + int titleWidth = mc.fontRendererObj.getStringWidth(title); + int centeredX = x + (width / 2 - titleWidth / 2); + mc.fontRendererObj.drawStringWithShadow(title, centeredX, y + TITLE_OFFSET, TEXT_COLOR); + } + + private void renderLines(List lines, int x, int y) { + int lineHeight = mc.fontRendererObj.FONT_HEIGHT; + int currentY = y + lineHeight + TITLE_BOTTOM_SPACING; + + for (String line : lines) { + mc.fontRendererObj.drawStringWithShadow(line, x + TEXT_PADDING, currentY, TEXT_COLOR); + currentY += lineHeight + LINE_SPACING; + } + } + + @Override + public boolean isHudElementEnabled() { + return isEnabled() && !hideScoreboard; + } + + @Override + public void setHudElement(HudElement hudElement) { + this.hudElement = hudElement; + } + + @Override + public HudInfo getHudInfo() { + return hudInfo; + } + + @Override + public HudElement getHudElement() { + if (hudElement == null) { + hudElement = createHudElement(); + } + return hudElement; + } + + @Override + public void save() { + // Sync position from HudElement before saving + if (hudElement != null && hudElement.getHudInfo() != null) { + HudInfo elementInfo = hudElement.getHudInfo(); + this.hudInfo.setPercentX(elementInfo.getPercentX()); + this.hudInfo.setPercentY(elementInfo.getPercentY()); + this.hudInfo.setWidth(elementInfo.getWidth()); + this.hudInfo.setHeight(elementInfo.getHeight()); + this.hudInfo.setScale(elementInfo.getScale()); + } + super.save(); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + save(); + } + + @Override + protected String[] setAliases() { + return new String[]{}; + } + + @Override + public String getName() { + return "Scoreboard"; + } + + @Override + public String getDescription() { + return "Renders the scoreboard with customizable settings"; + } + + @Override + public Object getDescriptor() { + return HUD; + } + + @Override + public EnumModuleCategory getCategory() { + return EnumModuleCategory.HUD; + } + + + public void setScoreObjective(ScoreObjective scoreObjective) { + this.scoreObjective = scoreObjective; + } + + public ScoreObjective getScoreObjective() { + return scoreObjective; + } +} \ No newline at end of file diff --git a/src/main/java/com/github/jtxofficial/flustclient/manager/DiscordRPCManager.java b/src/main/java/com/github/jtxofficial/flustclient/manager/DiscordRPCManager.java new file mode 100644 index 0000000..ba7234a --- /dev/null +++ b/src/main/java/com/github/jtxofficial/flustclient/manager/DiscordRPCManager.java @@ -0,0 +1,130 @@ +package com.github.jtxofficial.flustclient.manager; + +import club.minnced.discord.rpc.DiscordEventHandlers; +import club.minnced.discord.rpc.DiscordRPC; +import club.minnced.discord.rpc.DiscordRichPresence; +import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ServerData; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.TickEvent; +import net.minecraftforge.fml.common.network.FMLNetworkEvent; + +public class DiscordRPCManager extends Manager { + + private static final String APPLICATION_ID = "1442966231723278510"; + private static final DiscordRPC lib = DiscordRPC.INSTANCE; + + private final DiscordRichPresence presence = new DiscordRichPresence(); + private long startTimestamp; + private int updateCounter = 0; + private static final int UPDATE_INTERVAL = 100; // Update every 5 seconds (100 ticks) + + private boolean initialized = false; + + @Override + public void init() { + try { + DiscordEventHandlers handlers = new DiscordEventHandlers(); + handlers.errored = (errorCode, message) -> System.err.println("Discord RPC error: " + message); + + lib.Discord_Initialize(APPLICATION_ID, handlers, true, ""); + startTimestamp = System.currentTimeMillis() / 1000; + + // Set initial presence + updatePresence(); + + // Register this manager to the event bus + register(); + + initialized = true; + } catch (Exception e) { + System.err.println("Failed to initialize Discord RPC: " + e.getMessage()); + e.printStackTrace(); + } + } + + @SubscribeEvent + public void onClientTick(TickEvent.ClientTickEvent event) { + if (!initialized || event.phase != TickEvent.Phase.END) { + return; + } + + // Process Discord callbacks + lib.Discord_RunCallbacks(); + + // Update presence periodically + updateCounter++; + if (updateCounter >= UPDATE_INTERVAL) { + updateCounter = 0; + updatePresence(); + } + } + + @SubscribeEvent + public void onClientConnectedToServer(FMLNetworkEvent.ClientConnectedToServerEvent event) { + if (initialized) { + updatePresence(); + } + } + + @SubscribeEvent + public void onClientDisconnectedFromServer(FMLNetworkEvent.ClientDisconnectionFromServerEvent event) { + if (initialized) { + updatePresence(); + } + } + + private void updatePresence() { + try { + Minecraft mc = Minecraft.getMinecraft(); + + if (mc.theWorld == null) { + // Not in a world - show main menu + presence.details = "In Main Menu"; + presence.state = ""; + presence.largeImageKey = "flust_logo"; + presence.largeImageText = "FlustClient"; + } else { + ServerData serverData = mc.getCurrentServerData(); + + if (serverData != null) { + // On a multiplayer server + presence.details = "Playing on Server"; + presence.state = serverData.serverIP; + + // Show player count if available + if (mc.getNetHandler() != null && mc.getNetHandler().getPlayerInfoMap() != null) { + int playerCount = mc.getNetHandler().getPlayerInfoMap().size(); + presence.partySize = playerCount; + presence.partyMax = 0; // Max players not easily available in 1.8.9 + } + } else { + // Singleplayer or LAN + presence.details = "Playing Singleplayer"; + presence.state = ""; + } + + presence.largeImageKey = "flust_logo"; + presence.largeImageText = "FlustClient Beta"; + } + + presence.startTimestamp = startTimestamp; + + lib.Discord_UpdatePresence(presence); + } catch (Exception e) { + System.err.println("Failed to update Discord presence: " + e.getMessage()); + } + } + + public void shutdown() { + if (initialized) { + try { + lib.Discord_Shutdown(); + unregister(); + initialized = false; + } catch (Exception e) { + System.err.println("Failed to shutdown Discord RPC: " + e.getMessage()); + } + } + } +} diff --git a/src/main/java/com/github/jtxofficial/flustclient/mixin/AutoDiscoveryMixinPlugin.java b/src/main/java/com/github/jtxofficial/flustclient/mixin/AutoDiscoveryMixinPlugin.java index 372ddb0..c9afb12 100644 --- a/src/main/java/com/github/jtxofficial/flustclient/mixin/AutoDiscoveryMixinPlugin.java +++ b/src/main/java/com/github/jtxofficial/flustclient/mixin/AutoDiscoveryMixinPlugin.java @@ -1,4 +1,4 @@ -package com.github.jtxofficial.flustclient.init; +package com.github.jtxofficial.flustclient.mixin; import org.spongepowered.asm.lib.tree.ClassNode; import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; @@ -99,7 +99,11 @@ public void tryAddMixinClass(String className) { .replace("\\", "/") .replace("/", "."); if (norm.startsWith(getMixinPackage() + ".") && !norm.endsWith(".")) { - mixins.add(norm.substring(getMixinPackage().length() + 1)); + String mixinClassName = norm.substring(getMixinPackage().length() + 1); + // Skip the plugin itself - it's not a mixin + if (!mixinClassName.equals("AutoDiscoveryMixinPlugin")) { + mixins.add(mixinClassName); + } } } diff --git a/src/main/java/com/github/jtxofficial/flustclient/mixin/MixinGuiIngame.java b/src/main/java/com/github/jtxofficial/flustclient/mixin/MixinGuiIngame.java new file mode 100644 index 0000000..7fd574e --- /dev/null +++ b/src/main/java/com/github/jtxofficial/flustclient/mixin/MixinGuiIngame.java @@ -0,0 +1,32 @@ +package com.github.jtxofficial.flustclient.mixin; + +import com.github.jtxofficial.flustclient.feature.hud.Scoreboard; +import com.github.jtxofficial.flustclient.manager.ModuleManager; +import net.minecraft.client.gui.GuiIngame; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.scoreboard.ScoreObjective; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(GuiIngame.class) +public class MixinGuiIngame { + + /** + * Inject into renderScoreboard to capture the ScoreObjective and pass it to our custom scoreboard module. + * Also cancels the vanilla scoreboard rendering if our module is enabled. + */ + @Inject(method = "renderScoreboard", at = @At("HEAD"), cancellable = true) + private void onRenderScoreboard(ScoreObjective objective, ScaledResolution scaledRes, CallbackInfo ci) { + Scoreboard scoreboard = ModuleManager.getInstance().getModule(Scoreboard.class); + + if (scoreboard != null) { + scoreboard.setScoreObjective(objective); + + if (scoreboard.isEnabled() && !scoreboard.hideScoreboard) { + ci.cancel(); + } + } + } +}