Initial commit: Vault Party UI (massuus)

This commit is contained in:
Sam van Remortel
2026-05-24 15:52:40 +02:00
commit 5a6bb2c2fb
23 changed files with 999 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
package dev.massuus.vaultpartyui;
import net.minecraftforge.fml.common.Mod;
@Mod(VaultPartyUiMod.MODID)
public class VaultPartyUiMod {
public static final String MODID = "vaultpartyui";
public VaultPartyUiMod() {
}
}

View File

@@ -0,0 +1,28 @@
package dev.massuus.vaultpartyui.client;
import com.mojang.blaze3d.platform.InputConstants;
import dev.massuus.vaultpartyui.VaultPartyUiMod;
import net.minecraft.client.KeyMapping;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.ClientRegistry;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
import org.lwjgl.glfw.GLFW;
public final class ClientKeyMappings {
public static final KeyMapping OPEN_PARTY_UI = new KeyMapping(
"key.vaultpartyui.open_party_ui",
InputConstants.Type.KEYSYM,
GLFW.GLFW_KEY_I,
"key.categories.vaultpartyui"
);
private ClientKeyMappings() {
}
@SubscribeEvent
public static void onClientSetup(FMLClientSetupEvent event) {
ClientRegistry.registerKeyBinding(OPEN_PARTY_UI);
}
}

View File

@@ -0,0 +1,18 @@
package dev.massuus.vaultpartyui.client;
import dev.massuus.vaultpartyui.VaultPartyUiMod;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent;
@Mod.EventBusSubscriber(modid = VaultPartyUiMod.MODID, value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.MOD)
public final class ClientSetupEvents {
private ClientSetupEvents() {
}
@SubscribeEvent
public static void onClientSetup(FMLClientSetupEvent event) {
ClientKeyMappings.onClientSetup(event);
}
}

View File

@@ -0,0 +1,30 @@
package dev.massuus.vaultpartyui.client;
import dev.massuus.vaultpartyui.VaultPartyUiMod;
import dev.massuus.vaultpartyui.client.screen.PartyScreen;
import net.minecraft.client.Minecraft;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.InputEvent;
import net.minecraftforge.event.TickEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
import net.minecraftforge.fml.common.Mod;
@Mod.EventBusSubscriber(modid = VaultPartyUiMod.MODID, value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.FORGE)
public final class ClientTickEvents {
private ClientTickEvents() {
}
@SubscribeEvent
public static void onClientTick(TickEvent.ClientTickEvent event) {
if (event.phase != TickEvent.Phase.END) {
return;
}
Minecraft minecraft = Minecraft.getInstance();
while (ClientKeyMappings.OPEN_PARTY_UI.consumeClick()) {
if (minecraft.player != null) {
minecraft.setScreen(new PartyScreen(minecraft.screen));
}
}
}
}

View File

@@ -0,0 +1,544 @@
package dev.massuus.vaultpartyui.client.screen;
import com.mojang.authlib.GameProfile;
import com.mojang.blaze3d.vertex.PoseStack;
import iskallia.vault.client.data.ClientPartyData;
import iskallia.vault.client.data.ClientPartyInviteState;
import iskallia.vault.network.message.ServerboundPartyInviteResponseMessage;
import iskallia.vault.world.data.VaultPartyData.Party;
import iskallia.vault.client.data.ClientPartyData.PartyMember;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.client.multiplayer.PlayerInfo;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.TranslatableComponent;
import net.minecraft.util.Mth;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
public class PartyScreen extends Screen {
private static final int BUTTON_WIDTH = 90;
private static final int BUTTON_HEIGHT = 20;
private static final int BUTTON_GAP = 4;
private static final int PANEL_TOP = 100;
private static final int PANEL_HEIGHT = 155;
private static final int PANEL_PADDING = 10;
private static final int ONLINE_ROW_HEIGHT = 14;
private static final int VISIBLE_ONLINE_ROWS = 8;
private final Screen parentScreen;
private Party currentParty;
private List<OnlinePlayer> onlinePlayers = Collections.emptyList();
private EditBox targetBox;
private Button createPartyButton;
private Button leavePartyButton;
private Button disbandPartyButton;
private Button inviteNearbyButton;
private Button inviteAllButton;
private Button acceptInviteButton;
private Button declineInviteButton;
private int onlineScrollOffset;
public PartyScreen(Screen parentScreen) {
super(new TranslatableComponent("screen.vaultpartyui.title"));
this.parentScreen = parentScreen;
}
@Override
protected void init() {
super.init();
rebuildState();
int centerX = this.width / 2;
int rowWidth = BUTTON_WIDTH * 3 + BUTTON_GAP * 2;
int rowX = centerX - rowWidth / 2;
this.createPartyButton = addRenderableWidget(new Button(rowX, 24, BUTTON_WIDTH, BUTTON_HEIGHT, new TranslatableComponent("screen.vaultpartyui.create"), button -> sendPartyCommand("party create")));
this.leavePartyButton = addRenderableWidget(new Button(rowX + BUTTON_WIDTH + BUTTON_GAP, 24, BUTTON_WIDTH, BUTTON_HEIGHT, new TranslatableComponent("screen.vaultpartyui.leave"), button -> sendPartyCommand("party leave")));
this.disbandPartyButton = addRenderableWidget(new Button(rowX + (BUTTON_WIDTH + BUTTON_GAP) * 2, 24, BUTTON_WIDTH, BUTTON_HEIGHT, new TranslatableComponent("screen.vaultpartyui.disband"), button -> sendPartyCommand("party disband")));
this.inviteNearbyButton = addRenderableWidget(new Button(centerX - BUTTON_WIDTH - (BUTTON_GAP / 2), 48, BUTTON_WIDTH, BUTTON_HEIGHT, new TranslatableComponent("screen.vaultpartyui.invite_nearby"), button -> sendPartyCommand("party invite nearby")));
this.inviteAllButton = addRenderableWidget(new Button(centerX + (BUTTON_GAP / 2), 48, BUTTON_WIDTH, BUTTON_HEIGHT, new TranslatableComponent("screen.vaultpartyui.invite_all"), button -> sendPartyCommand("party invite all")));
int inviteButtonWidth = 140;
int inviteButtonX = centerX - inviteButtonWidth - 4;
int declineButtonX = centerX + 4;
this.acceptInviteButton = addRenderableWidget(new Button(inviteButtonX, 72, inviteButtonWidth, BUTTON_HEIGHT, new TranslatableComponent("screen.vaultpartyui.accept_invite"), button -> acceptPendingInvite()));
this.declineInviteButton = addRenderableWidget(new Button(declineButtonX, 72, inviteButtonWidth, BUTTON_HEIGHT, new TranslatableComponent("screen.vaultpartyui.decline_invite"), button -> declinePendingInvite()));
int panelWidth = (this.width - 40 - PANEL_PADDING) / 2;
int targetBoxWidth = panelWidth - PANEL_PADDING * 2;
int rightPanelX = 20 + panelWidth + PANEL_PADDING;
this.targetBox = new EditBox(this.font, rightPanelX + PANEL_PADDING, PANEL_TOP + 18, targetBoxWidth, 20, new TranslatableComponent("screen.vaultpartyui.target"));
this.targetBox.setMaxLength(64);
addRenderableWidget(this.targetBox);
updateActionVisibility();
}
@Override
public void tick() {
super.tick();
rebuildState();
if (this.targetBox != null) {
this.targetBox.tick();
}
updateActionVisibility();
}
@Override
public void render(PoseStack poseStack, int mouseX, int mouseY, float partialTick) {
this.renderBackground(poseStack);
int leftPanelX = 20;
int panelWidth = (this.width - 40 - PANEL_PADDING) / 2;
int rightPanelX = leftPanelX + panelWidth + PANEL_PADDING;
int panelBottom = PANEL_TOP + PANEL_HEIGHT;
drawPanel(poseStack, leftPanelX, PANEL_TOP, panelWidth, PANEL_HEIGHT);
drawPanel(poseStack, rightPanelX, PANEL_TOP, panelWidth, PANEL_HEIGHT);
this.drawCenteredString(poseStack, this.font, this.title, this.width / 2, 8, 0xFFFFFF);
this.drawCenteredString(poseStack, this.font, new TranslatableComponent("screen.vaultpartyui.party"), leftPanelX + panelWidth / 2, PANEL_TOP + 6, 0xE3C38C);
this.drawCenteredString(poseStack, this.font, new TranslatableComponent("screen.vaultpartyui.players"), rightPanelX + panelWidth / 2, PANEL_TOP + 6, 0xE3C38C);
if (ClientPartyInviteState.hasPendingInvite()) {
String inviterName = ClientPartyInviteState.getInviterName();
String inviteText = new TranslatableComponent("screen.vaultpartyui.pending_invite", inviterName == null ? "?" : inviterName).getString();
int noticeWidth = this.font.width(inviteText) + 14;
int noticeX = this.width / 2 - noticeWidth / 2;
fill(poseStack, noticeX, panelBottom + 8, noticeX + noticeWidth, panelBottom + 26, 0xCC1D1D1D);
fill(poseStack, noticeX, panelBottom + 8, noticeX + noticeWidth, panelBottom + 9, 0xFFE3C38C);
this.font.draw(poseStack, inviteText, noticeX + 7, panelBottom + 13, 0xFFFFFF);
}
renderPartyPanel(poseStack, leftPanelX, panelWidth, mouseX, mouseY);
renderOnlinePanel(poseStack, rightPanelX, panelWidth, mouseX, mouseY);
super.render(poseStack, mouseX, mouseY, partialTick);
if (this.targetBox != null) {
this.font.draw(poseStack, new TranslatableComponent("screen.vaultpartyui.target").getString(), this.targetBox.x, this.targetBox.y - 10, 0xA0A0A0);
}
// Credit
String credit = "Made by Massuus";
int creditX = this.width - this.font.width(credit) - 8;
int creditY = this.height - 18;
this.font.draw(poseStack, credit, creditX, creditY, 0xAAAAAA);
}
@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
// Check credit link click (bottom-right)
String credit = "Made by Massuus";
int creditX = this.width - this.font.width(credit) - 8;
int creditY = this.height - 18;
int creditW = this.font.width(credit);
int creditH = 10;
if (mouseX >= creditX && mouseX <= creditX + creditW && mouseY >= creditY && mouseY <= creditY + creditH) {
try {
openUrl("https://github.com/massuus/vault-hunters-party-ui");
} catch (Exception ignored) {
}
return true;
}
if (this.targetBox != null && this.targetBox.mouseClicked(mouseX, mouseY, button)) {
this.setFocused(this.targetBox);
return true;
}
if (super.mouseClicked(mouseX, mouseY, button)) {
return true;
}
if (handleOnlinePlayerClick(mouseX, mouseY)) {
return true;
}
return false;
}
@Override
public boolean mouseScrolled(double mouseX, double mouseY, double scrollDelta) {
if (!isInsideOnlinePanel(mouseX, mouseY)) {
return super.mouseScrolled(mouseX, mouseY, scrollDelta);
}
List<OnlinePlayer> visiblePlayers = filteredOnlinePlayers();
int maxOffset = Math.max(0, visiblePlayers.size() - VISIBLE_ONLINE_ROWS);
if (maxOffset == 0) {
return true;
}
int direction = scrollDelta > 0 ? -1 : 1;
this.onlineScrollOffset = Mth.clamp(this.onlineScrollOffset + direction, 0, maxOffset);
return true;
}
@Override
public void onClose() {
this.minecraft.setScreen(this.parentScreen);
}
private void rebuildState() {
Minecraft minecraft = Minecraft.getInstance();
if (minecraft.player == null) {
this.currentParty = null;
this.onlinePlayers = Collections.emptyList();
this.onlineScrollOffset = 0;
return;
}
this.currentParty = ClientPartyData.getParty(minecraft.player.getUUID());
this.onlinePlayers = gatherOnlinePlayers(minecraft.getConnection());
List<OnlinePlayer> visiblePlayers = filteredOnlinePlayers();
int maxOffset = Math.max(0, visiblePlayers.size() - VISIBLE_ONLINE_ROWS);
this.onlineScrollOffset = Mth.clamp(this.onlineScrollOffset, 0, maxOffset);
}
private List<OnlinePlayer> gatherOnlinePlayers(ClientPacketListener connection) {
if (connection == null) {
return Collections.emptyList();
}
List<OnlinePlayer> players = new ArrayList<>();
for (PlayerInfo playerInfo : connection.getOnlinePlayers()) {
GameProfile profile = playerInfo.getProfile();
if (profile != null && profile.getId() != null && profile.getName() != null) {
players.add(new OnlinePlayer(profile.getId(), profile.getName()));
}
}
players.sort(Comparator.comparing(player -> player.name.toLowerCase(Locale.ROOT)));
return players;
}
private List<OnlinePlayer> filteredOnlinePlayers() {
if (this.onlinePlayers.isEmpty()) {
return Collections.emptyList();
}
String filter = this.targetBox == null ? "" : this.targetBox.getValue().trim().toLowerCase(Locale.ROOT);
if (filter.isEmpty()) {
return this.onlinePlayers;
}
List<OnlinePlayer> filtered = new ArrayList<>();
for (OnlinePlayer player : this.onlinePlayers) {
if (player.name.toLowerCase(Locale.ROOT).contains(filter)) {
filtered.add(player);
}
}
return filtered;
}
private void sendPartyCommand(String command) {
Minecraft minecraft = Minecraft.getInstance();
ClientPacketListener connection = minecraft.getConnection();
if (connection != null) {
if (minecraft.player != null) {
minecraft.player.chat("/" + command);
}
}
}
private void acceptPendingInvite() {
if (!ClientPartyInviteState.hasPendingInvite()) {
return;
}
UUID inviteId = ClientPartyInviteState.getInviteId();
if (inviteId != null) {
ServerboundPartyInviteResponseMessage.send(inviteId, true);
ClientPartyInviteState.clearInvite();
}
}
private void declinePendingInvite() {
if (!ClientPartyInviteState.hasPendingInvite()) {
return;
}
UUID inviteId = ClientPartyInviteState.getInviteId();
if (inviteId != null) {
ServerboundPartyInviteResponseMessage.send(inviteId, false);
ClientPartyInviteState.clearInvite();
}
}
private void updateInviteButtons() {
boolean hasInvite = ClientPartyInviteState.hasPendingInvite() && this.currentParty == null;
if (this.acceptInviteButton != null) {
this.acceptInviteButton.visible = hasInvite;
}
if (this.declineInviteButton != null) {
this.declineInviteButton.visible = hasInvite;
}
}
private void updateActionVisibility() {
boolean inParty = this.currentParty != null;
int centerX = this.width / 2;
if (this.createPartyButton != null) {
this.createPartyButton.visible = !inParty;
this.createPartyButton.x = centerX - (BUTTON_WIDTH / 2);
}
if (this.leavePartyButton != null) {
this.leavePartyButton.visible = inParty;
}
if (this.disbandPartyButton != null) {
this.disbandPartyButton.visible = inParty;
}
// Keep the first action row centered for the currently visible controls.
if (inParty && this.leavePartyButton != null && this.disbandPartyButton != null) {
int rowWidth = BUTTON_WIDTH * 2 + BUTTON_GAP;
int rowX = centerX - rowWidth / 2;
this.leavePartyButton.x = rowX;
this.disbandPartyButton.x = rowX + BUTTON_WIDTH + BUTTON_GAP;
}
if (this.inviteNearbyButton != null) {
this.inviteNearbyButton.visible = inParty;
}
if (this.inviteAllButton != null) {
this.inviteAllButton.visible = inParty;
}
// Keep the second action row centered as a pair.
if (this.inviteNearbyButton != null && this.inviteAllButton != null) {
int rowWidth = BUTTON_WIDTH * 2 + BUTTON_GAP;
int rowX = centerX - rowWidth / 2;
this.inviteNearbyButton.x = rowX;
this.inviteAllButton.x = rowX + BUTTON_WIDTH + BUTTON_GAP;
}
updateInviteButtons();
}
private void renderPartyPanel(PoseStack poseStack, int panelX, int panelWidth, int mouseX, int mouseY) {
int textX = panelX + 10;
int textY = PANEL_TOP + 24;
if (this.currentParty == null) {
this.font.draw(poseStack, new TranslatableComponent("screen.vaultpartyui.no_party").getString(), textX, textY, 0xE0E0E0);
return;
}
UUID leaderId = this.currentParty.getLeader();
List<UUID> members = this.currentParty.getMembers();
this.font.draw(poseStack, new TranslatableComponent("screen.vaultpartyui.members").getString() + ": " + members.size(), textX, textY, 0xE0E0E0);
textY += 16;
for (UUID memberId : members) {
String memberName = resolvePlayerName(memberId);
PartyMember cachedMember = ClientPartyData.getCachedMember(memberId);
StringBuilder line = new StringBuilder(memberName);
if (memberId.equals(leaderId)) {
line.append(" [").append(new TranslatableComponent("screen.vaultpartyui.leader").getString()).append("]");
}
if (memberId.equals(getLocalPlayerId())) {
line.append(" [").append(new TranslatableComponent("screen.vaultpartyui.self").getString()).append("]");
}
int color = 0xFFFFFF;
if (cachedMember != null) {
if (cachedMember.status != PartyMember.Status.NORMAL) {
line.append(" - ").append(cachedMember.status.name());
}
line.append(" - ").append(formatHealth(cachedMember.healthPts)).append(" HP");
color = statusColor(cachedMember.status);
}
this.font.draw(poseStack, line.toString(), textX, textY, color);
textY += 14;
}
}
private void renderOnlinePanel(PoseStack poseStack, int panelX, int panelWidth, int mouseX, int mouseY) {
int textX = panelX + 10;
int boxWidth = panelWidth - 20;
int listTop = PANEL_TOP + 48;
int listHeight = VISIBLE_ONLINE_ROWS * ONLINE_ROW_HEIGHT + 6;
fill(poseStack, panelX + 8, PANEL_TOP + 20, panelX + panelWidth - 8, PANEL_TOP + 42, 0xAA1A1A1A);
this.font.draw(poseStack, new TranslatableComponent("screen.vaultpartyui.target").getString(), textX, PANEL_TOP + 24, 0xA0A0A0);
List<OnlinePlayer> visiblePlayers = filteredOnlinePlayers();
int maxOffset = Math.max(0, visiblePlayers.size() - VISIBLE_ONLINE_ROWS);
this.onlineScrollOffset = Mth.clamp(this.onlineScrollOffset, 0, maxOffset);
int startIndex = this.onlineScrollOffset;
int endIndex = Math.min(visiblePlayers.size(), startIndex + VISIBLE_ONLINE_ROWS);
fill(poseStack, panelX + 8, listTop, panelX + panelWidth - 8, listTop + listHeight, 0x66111111);
fill(poseStack, panelX + 8, listTop, panelX + panelWidth - 8, listTop + 1, 0xFFE3C38C);
if (visiblePlayers.isEmpty()) {
this.font.draw(poseStack, "No matching players.", textX, listTop + 6, 0xA0A0A0);
return;
}
int rowY = listTop + 4;
for (int index = startIndex; index < endIndex; index++) {
OnlinePlayer player = visiblePlayers.get(index);
boolean hovered = mouseX >= panelX + 10 && mouseX <= panelX + panelWidth - 10 && mouseY >= rowY - 2 && mouseY < rowY + ONLINE_ROW_HEIGHT - 2;
int background = hovered ? 0x663C3122 : 0x00000000;
// draw player name and per-row action (invite/remove)
this.fill(poseStack, panelX + 10, rowY - 2, panelX + panelWidth - 10, rowY + ONLINE_ROW_HEIGHT - 2, background);
this.font.draw(poseStack, player.name, panelX + 12, rowY, 0xFFFFFF);
// skip drawing actions for self
if (!player.id.equals(getLocalPlayerId())) {
int actionX = panelX + panelWidth - 84;
if (this.currentParty == null) {
// Invite button
this.font.draw(poseStack, new TranslatableComponent("screen.vaultpartyui.invite").getString(), actionX, rowY, 0xA0E0A0);
} else if (isPartyLeader()) {
// Remove button
this.font.draw(poseStack, new TranslatableComponent("screen.vaultpartyui.remove").getString(), actionX, rowY, 0xE0A0A0);
}
}
rowY += ONLINE_ROW_HEIGHT;
}
}
private boolean isInsideOnlinePanel(double mouseX, double mouseY) {
int leftPanelX = 20;
int panelWidth = (this.width - 40 - PANEL_PADDING) / 2;
int rightPanelX = leftPanelX + panelWidth + PANEL_PADDING;
int listTop = PANEL_TOP + 48;
int listHeight = VISIBLE_ONLINE_ROWS * ONLINE_ROW_HEIGHT + 6;
return mouseX >= rightPanelX + 8 && mouseX <= rightPanelX + panelWidth - 8 && mouseY >= listTop && mouseY <= listTop + listHeight;
}
private boolean handleOnlinePlayerClick(double mouseX, double mouseY) {
if (!isInsideOnlinePanel(mouseX, mouseY)) {
return false;
}
int panelX = 20 + (this.width - 40 - PANEL_PADDING) / 2 + PANEL_PADDING;
int listTop = PANEL_TOP + 48;
int relativeY = (int)mouseY - listTop - 4;
int index = this.onlineScrollOffset + (relativeY / ONLINE_ROW_HEIGHT);
List<OnlinePlayer> visiblePlayers = filteredOnlinePlayers();
if (index < 0 || index >= visiblePlayers.size()) {
return false;
}
OnlinePlayer player = visiblePlayers.get(index);
if (player.id.equals(getLocalPlayerId())) {
return false;
}
int actionX = panelX + ((this.width - 40 - PANEL_PADDING) / 2) - 84;
int actionY = listTop + (index - this.onlineScrollOffset) * ONLINE_ROW_HEIGHT + 4;
if (mouseX >= actionX && mouseX <= actionX + 70 && mouseY >= actionY && mouseY <= actionY + ONLINE_ROW_HEIGHT - 2) {
if (this.currentParty == null) {
sendPartyCommand("party invite " + player.name);
} else if (isPartyLeader()) {
sendPartyCommand("party remove " + player.name);
}
return true;
}
return false;
}
private boolean isPartyLeader() {
if (this.currentParty == null) return false;
UUID leader = this.currentParty.getLeader();
return leader != null && leader.equals(getLocalPlayerId());
}
private UUID getLocalPlayerId() {
Minecraft minecraft = Minecraft.getInstance();
if (minecraft.player == null) return null;
return minecraft.player.getUUID();
}
private String resolvePlayerName(UUID playerId) {
Minecraft minecraft = Minecraft.getInstance();
if (minecraft == null || playerId == null) return "?";
ClientPacketListener connection = minecraft.getConnection();
if (connection == null) return "?";
for (PlayerInfo info : connection.getOnlinePlayers()) {
GameProfile profile = info.getProfile();
if (profile != null && playerId.equals(profile.getId())) {
return profile.getName();
}
}
return "?";
}
private String formatHealth(float hp) {
return String.format(Locale.ROOT, "%.1f", hp);
}
private int statusColor(PartyMember.Status status) {
if (status == null) return 0xFFFFFF;
switch (status) {
case DEAD:
return 0xFF5555;
case DOWNED:
return 0xFFAA00;
default:
return 0xFFFFFF;
}
}
private void drawPanel(PoseStack poseStack, int x, int y, int width, int height) {
fill(poseStack, x, y, x + width, y + height, 0xAA111111);
fill(poseStack, x, y, x + width, y + 1, 0xFFE3C38C);
}
private void openUrl(String url) {
try {
java.awt.Desktop desktop = java.awt.Desktop.isDesktopSupported() ? java.awt.Desktop.getDesktop() : null;
if (desktop != null && desktop.isSupported(java.awt.Desktop.Action.BROWSE)) {
desktop.browse(new java.net.URI(url));
return;
}
} catch (Exception ignored) {
}
// Fallbacks
try {
String os = System.getProperty("os.name").toLowerCase(java.util.Locale.ROOT);
if (os.contains("win")) {
Runtime.getRuntime().exec(new String[]{"rundll32", "url.dll,FileProtocolHandler", url});
} else if (os.contains("mac")) {
Runtime.getRuntime().exec(new String[]{"open", url});
} else {
Runtime.getRuntime().exec(new String[]{"xdg-open", url});
}
} catch (Exception ignored) {
}
}
private static final class OnlinePlayer {
final UUID id;
final String name;
OnlinePlayer(UUID id, String name) {
this.id = id;
this.name = name;
}
}
}