diff --git a/CURSEFORGE_DESCRIPTION.md b/CURSEFORGE_DESCRIPTION.md new file mode 100644 index 0000000..377f7cc --- /dev/null +++ b/CURSEFORGE_DESCRIPTION.md @@ -0,0 +1,60 @@ +# Vault Party UI + +Vault Party UI is a client-side Forge mod for Vault Hunters 1.18.2 that adds a fast, focused party management screen. It reduces chat command spam and keeps the most common party actions in one place. + +## Features + +* Open a party UI with a keybind instead of typing `/party` commands by hand. +* Create, leave, and disband parties. +* Invite nearby players, invite everyone nearby, or invite favorites. +* Handle incoming party invites with accept and decline buttons. +* Keep track of current party members and online players in one screen. +* Invite or remove players directly from the player list. +* Mark favorite players with a star and send favorite-only invites. +* Use an auto-accept invites toggle for faster party joins. + +## Why use it? + +If you play Vault Hunters with friends, this mod makes party management much less annoying. Instead of constantly typing commands, you get a dedicated UI for the most common party actions, plus favorites and auto-accept to make repeated party setup faster. + +## Requirements + +* Minecraft 1.18.2 +* Forge 40.x +* Vault Hunters modpack + +## Notes + +* This mod is client-side only. +* The server still controls party permissions and final command behavior. +* For best results, test it inside the Vault Hunters pack client. + +## Changelog + +### 1.2.2 + +* Removed the `Runtime.getRuntime()` fallback from the external link handler. + +### 1.2.1 + +* Fixed the accept and decline invite buttons so they sit correctly around the Create Party button. + +### 1.2.0 + +* Added favorite players with a clickable star in the online player list. +* Added Invite Favorites to invite every available favorite player at once. +* Reworked the party screen layout to better fit the top controls and both list panels. +* Improved the offline player label shown in the party list. + +### 1.1.0 + +* Added auto-accept invites toggle that works even when the screen is closed. +* Added player heads beside party and online player names. +* Improved row states, invite cooldown handling, and inline UI feedback. +* Refined layout spacing and removed the extra filter button/context text. +* Split party screen logic into helper classes to keep the screen file smaller. + +### 1.0.0 + +* Initial release of Vault Party UI. +* Added the party management screen, keybind, and core invite/remove actions. diff --git a/GITHUB_RELEASE_1.2.2.md b/GITHUB_RELEASE_1.2.2.md new file mode 100644 index 0000000..8ffc287 --- /dev/null +++ b/GITHUB_RELEASE_1.2.2.md @@ -0,0 +1,29 @@ +# Vault Party UI 1.2.2 + +## Highlights + +- Removed the `Runtime.getRuntime()` fallback from the external link handler. +- Kept the project branding as Vault Party UI for CurseForge-friendly naming. +- Preserved the current party UI improvements, including favorites, invite handling, and the layout updates around the Create Party button. + +## Notes + +- This is a client-side Forge mod for Vault Hunters 1.18.2. +- Built for Forge 40.x. + +## Changelog + +### 1.2.2 + +- Removed the `Runtime.getRuntime()` fallback from the external link handler. + +### 1.2.1 + +- Fixed the accept and decline invite buttons so they sit correctly around the Create Party button. + +### 1.2.0 + +- Added favorite players with a clickable star in the online player list. +- Added Invite Favorites to invite every available favorite player at once. +- Reworked the party screen layout to better fit the top controls and both list panels. +- Improved the offline player label shown in the party list. diff --git a/README.md b/README.md index 749e0ec..27f2c16 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,21 @@ Client-side Forge mod for Vault Hunters (Minecraft 1.18.2) that provides a party ## Changelog +### 1.2.2 + +- Removed Runtime.getRuntime() fallback. + +### 1.2.1 + +- Fixed the accept and decline invite buttons so they sit correctly around the Create Party button. + +### 1.2.0 + +- Added favorite players with a clickable star in the online player list. +- Added Invite Favorites to invite every available favorite player at once. +- Reworked the party screen layout to better fit the top controls and both list panels. +- Improved the offline player label shown in the party list. + ### 1.1.0 - Added auto-accept invites toggle that works even when the screen is closed. diff --git a/gradle.properties b/gradle.properties index 9464d20..45d6357 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,5 +8,5 @@ mapping_version=1.18.2 mod_id=vaultpartyui mod_name=Vault Party UI -mod_version=1.1.0 +mod_version=1.2.2 mod_group_id=dev.massuus.vaultpartyui diff --git a/src/main/java/dev/massuus/vaultpartyui/client/ClientFavoritePlayers.java b/src/main/java/dev/massuus/vaultpartyui/client/ClientFavoritePlayers.java new file mode 100644 index 0000000..04f8d2b --- /dev/null +++ b/src/main/java/dev/massuus/vaultpartyui/client/ClientFavoritePlayers.java @@ -0,0 +1,94 @@ +package dev.massuus.vaultpartyui.client; + +import net.minecraftforge.fml.loading.FMLPaths; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +public final class ClientFavoritePlayers { + private static final String FILE_NAME = "vaultpartyui-favorites.txt"; + private static final Set FAVORITES = new LinkedHashSet<>(); + + private static boolean loaded; + + private ClientFavoritePlayers() { + } + + public static boolean isFavorite(UUID playerId) { + ensureLoaded(); + return playerId != null && FAVORITES.contains(playerId); + } + + public static void toggleFavorite(UUID playerId) { + if (playerId == null) { + return; + } + setFavorite(playerId, !isFavorite(playerId)); + } + + public static void setFavorite(UUID playerId, boolean favorite) { + if (playerId == null) { + return; + } + + ensureLoaded(); + if (favorite) { + FAVORITES.add(playerId); + } else { + FAVORITES.remove(playerId); + } + save(); + } + + private static void ensureLoaded() { + if (loaded) { + return; + } + loaded = true; + + Path file = favoritesFile(); + if (file == null || !Files.exists(file)) { + return; + } + + try { + List lines = Files.readAllLines(file); + for (String line : lines) { + String trimmed = line.trim(); + if (trimmed.isEmpty()) { + continue; + } + try { + FAVORITES.add(UUID.fromString(trimmed)); + } catch (IllegalArgumentException ignored) { + } + } + } catch (IOException ignored) { + } + } + + private static void save() { + Path file = favoritesFile(); + if (file == null) { + return; + } + + try { + Path parent = file.getParent(); + if (parent != null) { + Files.createDirectories(parent); + } + Files.write(file, FAVORITES.stream().map(UUID::toString).toList()); + } catch (IOException ignored) { + } + } + + private static Path favoritesFile() { + return FMLPaths.CONFIGDIR.get().resolve(FILE_NAME); + } +} \ No newline at end of file diff --git a/src/main/java/dev/massuus/vaultpartyui/client/screen/OnlineRow.java b/src/main/java/dev/massuus/vaultpartyui/client/screen/OnlineRow.java index 13bfe51..b99bb10 100644 --- a/src/main/java/dev/massuus/vaultpartyui/client/screen/OnlineRow.java +++ b/src/main/java/dev/massuus/vaultpartyui/client/screen/OnlineRow.java @@ -3,9 +3,11 @@ package dev.massuus.vaultpartyui.client.screen; final class OnlineRow { final OnlinePlayer player; final RowState state; + final boolean favorite; - OnlineRow(OnlinePlayer player, RowState state) { + OnlineRow(OnlinePlayer player, RowState state, boolean favorite) { this.player = player; this.state = state; + this.favorite = favorite; } } diff --git a/src/main/java/dev/massuus/vaultpartyui/client/screen/PartyRosterService.java b/src/main/java/dev/massuus/vaultpartyui/client/screen/PartyRosterService.java index 5f7beab..10b2d61 100644 --- a/src/main/java/dev/massuus/vaultpartyui/client/screen/PartyRosterService.java +++ b/src/main/java/dev/massuus/vaultpartyui/client/screen/PartyRosterService.java @@ -1,5 +1,6 @@ package dev.massuus.vaultpartyui.client.screen; +import dev.massuus.vaultpartyui.client.ClientFavoritePlayers; import iskallia.vault.client.data.ClientPartyData; import iskallia.vault.world.data.VaultPartyData.Party; @@ -32,7 +33,7 @@ final class PartyRosterService { if (filterMode == FilterMode.OTHER_PARTY && state != RowState.OTHER_PARTY) { continue; } - rows.add(new OnlineRow(player, state)); + rows.add(new OnlineRow(player, state, ClientFavoritePlayers.isFavorite(player.id))); } rows.sort((a, b) -> { diff --git a/src/main/java/dev/massuus/vaultpartyui/client/screen/PartyScreen.java b/src/main/java/dev/massuus/vaultpartyui/client/screen/PartyScreen.java index 9ed9ea4..4f37f31 100644 --- a/src/main/java/dev/massuus/vaultpartyui/client/screen/PartyScreen.java +++ b/src/main/java/dev/massuus/vaultpartyui/client/screen/PartyScreen.java @@ -3,6 +3,7 @@ package dev.massuus.vaultpartyui.client.screen; import com.mojang.authlib.GameProfile; import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.vertex.PoseStack; +import dev.massuus.vaultpartyui.client.ClientFavoritePlayers; import dev.massuus.vaultpartyui.client.ClientPartySettings; import iskallia.vault.client.data.ClientPartyData; import iskallia.vault.client.data.ClientPartyInviteState; @@ -36,14 +37,15 @@ 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 = 124; - private static final int PANEL_HEIGHT = 155; + private static final int PANEL_TOP = 58; + private static final int PANEL_HEIGHT = 246; private static final int PANEL_PADDING = 10; private static final int ONLINE_ROW_HEIGHT = 14; - private static final int VISIBLE_ONLINE_ROWS = 8; + private static final int VISIBLE_ONLINE_ROWS = 15; private static final int HEAD_SIZE = 8; private static final long INVITE_COOLDOWN_MS = 8000L; private static final int STATE_REFRESH_INTERVAL_TICKS = 4; + private static final int STAR_SIZE = 8; private final Screen parentScreen; @@ -55,6 +57,7 @@ public class PartyScreen extends Screen { private Button disbandPartyButton; private Button inviteNearbyButton; private Button inviteAllButton; + private Button inviteFavoritesButton; private Button acceptInviteButton; private Button declineInviteButton; private Button autoAcceptToggleButton; @@ -74,31 +77,34 @@ public class PartyScreen extends Screen { super.init(); rebuildState(); - int centerX = this.width / 2; - int rowWidth = BUTTON_WIDTH * 3 + BUTTON_GAP * 2; - int rowX = centerX - rowWidth / 2; + int panelWidth = (this.width - 40 - PANEL_PADDING) / 2; + int leftPanelX = 20; + int rightPanelX = leftPanelX + panelWidth + PANEL_PADDING; + int leftRowWidth = BUTTON_WIDTH * 2 + BUTTON_GAP; + int rightRowWidth = BUTTON_WIDTH * 3 + BUTTON_GAP * 2; + int leftRowX = leftPanelX + panelWidth / 2 - leftRowWidth / 2; + int rightRowX = rightPanelX + panelWidth / 2 - rightRowWidth / 2; - this.createPartyButton = addRenderableWidget(new Button(rowX, 34, BUTTON_WIDTH, BUTTON_HEIGHT, new TranslatableComponent("screen.vaultpartyui.create"), button -> sendPartyCommand("party create"))); - this.leavePartyButton = addRenderableWidget(new Button(rowX + BUTTON_WIDTH + BUTTON_GAP, 34, BUTTON_WIDTH, BUTTON_HEIGHT, new TranslatableComponent("screen.vaultpartyui.leave"), button -> sendPartyCommand("party leave"))); - this.disbandPartyButton = addRenderableWidget(new Button(rowX + (BUTTON_WIDTH + BUTTON_GAP) * 2, 34, BUTTON_WIDTH, BUTTON_HEIGHT, new TranslatableComponent("screen.vaultpartyui.disband"), button -> sendPartyCommand("party disband"))); + int createX = this.width / 2 - BUTTON_WIDTH / 2; + this.createPartyButton = addRenderableWidget(new Button(createX, 34, BUTTON_WIDTH, BUTTON_HEIGHT, new TranslatableComponent("screen.vaultpartyui.create"), button -> sendPartyCommand("party create"))); + this.leavePartyButton = addRenderableWidget(new Button(leftRowX, 34, BUTTON_WIDTH, BUTTON_HEIGHT, new TranslatableComponent("screen.vaultpartyui.leave"), button -> sendPartyCommand("party leave"))); + this.disbandPartyButton = addRenderableWidget(new Button(leftRowX + BUTTON_WIDTH + BUTTON_GAP, 34, BUTTON_WIDTH, BUTTON_HEIGHT, new TranslatableComponent("screen.vaultpartyui.disband"), button -> sendPartyCommand("party disband"))); - this.inviteNearbyButton = addRenderableWidget(new Button(centerX - BUTTON_WIDTH - (BUTTON_GAP / 2), 62, BUTTON_WIDTH, BUTTON_HEIGHT, new TranslatableComponent("screen.vaultpartyui.invite_nearby"), button -> sendPartyCommand("party invite nearby"))); - this.inviteAllButton = addRenderableWidget(new Button(centerX + (BUTTON_GAP / 2), 62, BUTTON_WIDTH, BUTTON_HEIGHT, new TranslatableComponent("screen.vaultpartyui.invite_all"), button -> sendPartyCommand("party invite all"))); + this.inviteNearbyButton = addRenderableWidget(new Button(rightRowX, 34, BUTTON_WIDTH, BUTTON_HEIGHT, new TranslatableComponent("screen.vaultpartyui.invite_nearby"), button -> sendPartyCommand("party invite nearby"))); + this.inviteAllButton = addRenderableWidget(new Button(rightRowX + BUTTON_WIDTH + BUTTON_GAP, 34, BUTTON_WIDTH, BUTTON_HEIGHT, new TranslatableComponent("screen.vaultpartyui.invite_all"), button -> sendPartyCommand("party invite all"))); + this.inviteFavoritesButton = addRenderableWidget(new Button(rightRowX + (BUTTON_WIDTH + BUTTON_GAP) * 2, 34, BUTTON_WIDTH, BUTTON_HEIGHT, new TranslatableComponent("screen.vaultpartyui.invite_favorites"), button -> inviteFavoritePlayers())); int inviteButtonWidth = 140; - int inviteButtonX = centerX - inviteButtonWidth - 4; - int declineButtonX = centerX + 4; - this.acceptInviteButton = addRenderableWidget(new Button(inviteButtonX, 90, inviteButtonWidth, BUTTON_HEIGHT, new TranslatableComponent("screen.vaultpartyui.accept_invite"), button -> acceptPendingInvite())); - this.declineInviteButton = addRenderableWidget(new Button(declineButtonX, 90, inviteButtonWidth, BUTTON_HEIGHT, new TranslatableComponent("screen.vaultpartyui.decline_invite"), button -> declinePendingInvite())); - this.autoAcceptToggleButton = addRenderableWidget(new Button(centerX - 95, 90, 190, BUTTON_HEIGHT, autoAcceptToggleLabel(), button -> { + // Position invite accept/decline to the left/right of the centered Create Party button, same vertical level + this.acceptInviteButton = addRenderableWidget(new Button(createX - inviteButtonWidth - BUTTON_GAP, 34, inviteButtonWidth, BUTTON_HEIGHT, new TranslatableComponent("screen.vaultpartyui.accept_invite"), button -> acceptPendingInvite())); + this.declineInviteButton = addRenderableWidget(new Button(createX + BUTTON_WIDTH + BUTTON_GAP, 34, inviteButtonWidth, BUTTON_HEIGHT, new TranslatableComponent("screen.vaultpartyui.decline_invite"), button -> declinePendingInvite())); + this.autoAcceptToggleButton = addRenderableWidget(new Button(20, this.height - BUTTON_HEIGHT - 8, 190, BUTTON_HEIGHT, autoAcceptToggleLabel(), button -> { ClientPartySettings.toggleAutoAcceptInvites(); updateAutoAcceptToggleLabel(); pushToast(new TranslatableComponent(ClientPartySettings.isAutoAcceptInvitesEnabled() ? "screen.vaultpartyui.toast_auto_accept_on" : "screen.vaultpartyui.toast_auto_accept_off"), 0xE3C38C); })); - 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); @@ -192,7 +198,7 @@ public class PartyScreen extends Screen { int creditH = 10; if (mouseX >= creditX && mouseX <= creditX + creditW && mouseY >= creditY && mouseY <= creditY + creditH) { try { - openUrl("https://github.com/massuus/vault-hunters-party-ui"); + openUrl("https://github.com/massuus/vault-party-ui"); } catch (Exception ignored) { } return true; @@ -391,11 +397,23 @@ public class PartyScreen extends Screen { private void updateActionVisibility() { boolean inParty = isLocalPlayerInParty(); - int centerX = this.width / 2; + int panelWidth = (this.width - 40 - PANEL_PADDING) / 2; + int leftPanelX = 20; + int rightPanelX = leftPanelX + panelWidth + PANEL_PADDING; if (this.createPartyButton != null) { this.createPartyButton.visible = !inParty; - this.createPartyButton.x = centerX - (BUTTON_WIDTH / 2); + int createX = this.width / 2 - (BUTTON_WIDTH / 2); + this.createPartyButton.x = createX; + // keep accept/decline positioned relative to create button + if (this.acceptInviteButton != null) { + this.acceptInviteButton.x = createX - (this.acceptInviteButton.getWidth() + BUTTON_GAP); + this.acceptInviteButton.y = this.createPartyButton.y; + } + if (this.declineInviteButton != null) { + this.declineInviteButton.x = createX + BUTTON_WIDTH + BUTTON_GAP; + this.declineInviteButton.y = this.createPartyButton.y; + } } if (this.leavePartyButton != null) { this.leavePartyButton.visible = inParty; @@ -404,10 +422,9 @@ public class PartyScreen extends Screen { 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; + int rowX = leftPanelX + panelWidth / 2 - rowWidth / 2; this.leavePartyButton.x = rowX; this.disbandPartyButton.x = rowX + BUTTON_WIDTH + BUTTON_GAP; } @@ -418,16 +435,22 @@ public class PartyScreen extends Screen { if (this.inviteAllButton != null) { this.inviteAllButton.visible = inParty; } + if (this.inviteFavoritesButton != null) { + this.inviteFavoritesButton.visible = inParty; + this.inviteFavoritesButton.active = inParty && hasInviteableFavorites(); + } if (this.autoAcceptToggleButton != null) { this.autoAcceptToggleButton.visible = true; this.autoAcceptToggleButton.active = true; + this.autoAcceptToggleButton.x = 20; + this.autoAcceptToggleButton.y = this.height - BUTTON_HEIGHT - 8; } - // 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; + if (this.inviteNearbyButton != null && this.inviteAllButton != null && this.inviteFavoritesButton != null) { + int rowWidth = BUTTON_WIDTH * 3 + BUTTON_GAP * 2; + int rowX = rightPanelX + panelWidth / 2 - rowWidth / 2; this.inviteNearbyButton.x = rowX; this.inviteAllButton.x = rowX + BUTTON_WIDTH + BUTTON_GAP; + this.inviteFavoritesButton.x = rowX + (BUTTON_WIDTH + BUTTON_GAP) * 2; } updateInviteButtons(); @@ -487,7 +510,8 @@ public class PartyScreen extends Screen { private void renderOnlinePanel(PoseStack poseStack, int panelX, int panelWidth, int mouseX, int mouseY) { int textX = panelX + 10; int listTop = PANEL_TOP + 48; - int listHeight = VISIBLE_ONLINE_ROWS * ONLINE_ROW_HEIGHT + 6; + int panelBottom = PANEL_TOP + PANEL_HEIGHT; + int listHeight = Math.min(VISIBLE_ONLINE_ROWS * ONLINE_ROW_HEIGHT + 6, Math.max(0, panelBottom - listTop - 8)); 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); @@ -522,16 +546,30 @@ public class PartyScreen extends Screen { // 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); - drawPlayerHead(poseStack, player.id, panelX + 12, rowY); - this.font.draw(poseStack, player.name, panelX + 12 + HEAD_SIZE + 4, rowY, RowPresentation.nameColor(row.state)); + int starX = panelX + 12; + int starColor = row.favorite ? 0xFFD76A : 0xB0B0B0; + boolean starHovered = isFavoriteToggleHovered(mouseX, mouseY, starX, rowY); + if (starHovered) { + starColor = 0xFFFFFF; + } + this.font.draw(poseStack, row.favorite ? "\u2605" : "\u2606", starX, rowY, starColor); + + drawPlayerHead(poseStack, player.id, starX + STAR_SIZE + 4, rowY); int actionX = panelX + panelWidth - 110; + int nameX = starX + STAR_SIZE + 4 + HEAD_SIZE + 4; + int nameWidth = Math.max(0, actionX - nameX - 8); + String displayName = this.font.plainSubstrByWidth(player.name, nameWidth); + this.font.draw(poseStack, displayName, nameX, rowY, RowPresentation.nameColor(row.state)); + Component action = RowPresentation.actionLabel(row, isPartyLeader()); if (action != null) { this.font.draw(poseStack, action.getString(), actionX, rowY, RowPresentation.actionColor(row.state)); } - if (hovered) { + if (starHovered) { + renderTooltip(poseStack, RowPresentation.favoriteTooltip(row.favorite), mouseX, mouseY); + } else if (hovered) { Component hint = RowPresentation.tooltip(row, isPartyLeader()); if (hint != null) { renderTooltip(poseStack, hint, mouseX, mouseY); @@ -547,7 +585,8 @@ public class PartyScreen extends Screen { 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; + int panelBottom = PANEL_TOP + PANEL_HEIGHT; + int listHeight = Math.min(VISIBLE_ONLINE_ROWS * ONLINE_ROW_HEIGHT + 6, Math.max(0, panelBottom - listTop - 8)); return mouseX >= rightPanelX + 8 && mouseX <= rightPanelX + panelWidth - 8 && mouseY >= listTop && mouseY <= listTop + listHeight; } @@ -572,6 +611,12 @@ public class PartyScreen extends Screen { int actionY = listTop + (index - this.onlineScrollOffset) * ONLINE_ROW_HEIGHT + 4; int panelWidth = (this.width - 40 - PANEL_PADDING) / 2; int actionX = panelX + panelWidth - 110; + int starX = panelX + 12; + if (isFavoriteToggleHovered(mouseX, mouseY, starX, actionY)) { + ClientFavoritePlayers.toggleFavorite(player.id); + pushToast(new TranslatableComponent(row.favorite ? "screen.vaultpartyui.toast_favorite_removed" : "screen.vaultpartyui.toast_favorite_added", player.name), 0xE3C38C); + return true; + } if (mouseX >= actionX && mouseX <= actionX + 104 && mouseY >= actionY && mouseY <= actionY + ONLINE_ROW_HEIGHT - 2) { performPrimaryRowAction(row); return true; @@ -585,6 +630,49 @@ public class PartyScreen extends Screen { return PartyRosterService.isPartyLeader(this.currentParty, getLocalPlayerId()); } + private boolean hasInviteableFavorites() { + List rows = PartyRosterService.buildRows( + this.onlinePlayers, + FilterMode.ALL, + this.currentParty, + getLocalPlayerId(), + this.inviteCooldownUntilMs + ); + + for (OnlineRow row : rows) { + if (row.favorite && row.state == RowState.INVITEABLE) { + return true; + } + } + return false; + } + + private void inviteFavoritePlayers() { + List rows = PartyRosterService.buildRows( + this.onlinePlayers, + FilterMode.ALL, + this.currentParty, + getLocalPlayerId(), + this.inviteCooldownUntilMs + ); + + int invited = 0; + for (OnlineRow row : rows) { + if (!row.favorite || row.state != RowState.INVITEABLE) { + continue; + } + sendPartyCommand("party invite " + row.player.name); + this.inviteCooldownUntilMs.put(row.player.id, System.currentTimeMillis() + INVITE_COOLDOWN_MS); + invited++; + } + + if (invited > 0) { + pushToast(new TranslatableComponent("screen.vaultpartyui.toast_invited_favorites"), 0xA0E0A0); + } else { + pushToast(new TranslatableComponent("screen.vaultpartyui.toast_no_favorite_invites"), 0xB0B0B0); + } + } + private boolean isLocalPlayerInParty() { return PartyRosterService.isLocalPlayerInParty(this.currentParty, getLocalPlayerId()); } @@ -678,16 +766,16 @@ public class PartyScreen extends Screen { private String resolvePlayerName(UUID playerId) { Minecraft minecraft = Minecraft.getInstance(); - if (minecraft == null || playerId == null) return "?"; + if (minecraft == null || playerId == null) return "offline"; ClientPacketListener connection = minecraft.getConnection(); - if (connection == null) return "?"; + if (connection == null) return "offline"; for (PlayerInfo info : connection.getOnlinePlayers()) { GameProfile profile = info.getProfile(); if (profile != null && playerId.equals(profile.getId())) { return profile.getName(); } } - return "?"; + return "offline"; } private String formatHealth(float hp) { @@ -717,6 +805,10 @@ public class PartyScreen extends Screen { return DefaultPlayerSkin.getDefaultSkin(safeId); } + private boolean isFavoriteToggleHovered(double mouseX, double mouseY, int starX, int rowY) { + return mouseX >= starX && mouseX <= starX + STAR_SIZE && mouseY >= rowY - 1 && mouseY <= rowY + STAR_SIZE; + } + private int statusColor(PartyMember.Status status) { if (status == null) return 0xFFFFFF; String s = status.name(); @@ -744,18 +836,9 @@ public class PartyScreen extends Screen { } 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) { - } + // Do not attempt Runtime.exec fallbacks — these use Runtime.getRuntime() and + // are rejected by some moderation systems (e.g. CurseForge). If the AWT + // Desktop API is not available, silently fail to avoid using Runtime. } } diff --git a/src/main/java/dev/massuus/vaultpartyui/client/screen/RowPresentation.java b/src/main/java/dev/massuus/vaultpartyui/client/screen/RowPresentation.java index 7e65a9a..1dd3fea 100644 --- a/src/main/java/dev/massuus/vaultpartyui/client/screen/RowPresentation.java +++ b/src/main/java/dev/massuus/vaultpartyui/client/screen/RowPresentation.java @@ -69,4 +69,8 @@ final class RowPresentation { return null; } } + + static Component favoriteTooltip(boolean favorite) { + return new TranslatableComponent(favorite ? "screen.vaultpartyui.tip_unfavorite" : "screen.vaultpartyui.tip_favorite"); + } } diff --git a/src/main/resources/assets/vaultpartyui/lang/en_us.json b/src/main/resources/assets/vaultpartyui/lang/en_us.json index f1235b4..9e7c358 100644 --- a/src/main/resources/assets/vaultpartyui/lang/en_us.json +++ b/src/main/resources/assets/vaultpartyui/lang/en_us.json @@ -14,6 +14,7 @@ "screen.vaultpartyui.invite_selected": "Invite Selected", "screen.vaultpartyui.invite_nearby": "Invite Nearby", "screen.vaultpartyui.invite_all": "Invite All", + "screen.vaultpartyui.invite_favorites": "Invite Favorites", "screen.vaultpartyui.remove_selected": "Remove Selected", "screen.vaultpartyui.accept_invite": "Accept Invite", "screen.vaultpartyui.decline_invite": "Decline Invite", @@ -22,6 +23,10 @@ "screen.vaultpartyui.auto_accepted": "Auto-accepted invite from %s", "screen.vaultpartyui.toast_auto_accept_on": "Auto-accept enabled", "screen.vaultpartyui.toast_auto_accept_off": "Auto-accept disabled", + "screen.vaultpartyui.toast_invited_favorites": "Invited favorite players", + "screen.vaultpartyui.toast_no_favorite_invites": "No favorite players are available to invite", + "screen.vaultpartyui.toast_favorite_added": "Added %s to favorites", + "screen.vaultpartyui.toast_favorite_removed": "Removed %s from favorites", "screen.vaultpartyui.no_party": "You are not in a party.", "screen.vaultpartyui.no_target": "Type or select a player name first.", "screen.vaultpartyui.pending_invite": "Incoming invite from %s", @@ -37,6 +42,8 @@ "screen.vaultpartyui.tip_invite": "Invite this player", "screen.vaultpartyui.tip_remove": "Remove this member", "screen.vaultpartyui.tip_member": "Only leader can remove members", + "screen.vaultpartyui.tip_favorite": "Add this player to favorites", + "screen.vaultpartyui.tip_unfavorite": "Remove this player from favorites", "screen.vaultpartyui.tip_other_party": "Player is already in another party", "screen.vaultpartyui.tip_cooldown": "Invite recently sent", "screen.vaultpartyui.tip_no_action": "Create or join a party to invite",