Added auto-accept, player heads beside party.
This commit is contained in:
15
README.md
15
README.md
@@ -11,6 +11,21 @@ Client-side Forge mod for Vault Hunters (Minecraft 1.18.2) that provides a party
|
||||
- Party member list panel.
|
||||
- Online player list with per-player `Invite` / `Remove` actions.
|
||||
|
||||
## Changelog
|
||||
|
||||
### 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 the Vault Party UI.
|
||||
- Added the party management screen, keybind, and core invite/remove actions.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Minecraft `1.18.2`
|
||||
|
||||
@@ -8,5 +8,5 @@ mapping_version=1.18.2
|
||||
|
||||
mod_id=vaultpartyui
|
||||
mod_name=Vault Party UI
|
||||
mod_version=1.0.0
|
||||
mod_version=1.1.0
|
||||
mod_group_id=dev.massuus.vaultpartyui
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package dev.massuus.vaultpartyui.client;
|
||||
|
||||
public final class ClientPartySettings {
|
||||
private static boolean autoAcceptInvitesEnabled;
|
||||
|
||||
private ClientPartySettings() {
|
||||
}
|
||||
|
||||
public static boolean isAutoAcceptInvitesEnabled() {
|
||||
return autoAcceptInvitesEnabled;
|
||||
}
|
||||
|
||||
public static void setAutoAcceptInvitesEnabled(boolean enabled) {
|
||||
autoAcceptInvitesEnabled = enabled;
|
||||
}
|
||||
|
||||
public static void toggleAutoAcceptInvites() {
|
||||
autoAcceptInvitesEnabled = !autoAcceptInvitesEnabled;
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,21 @@
|
||||
package dev.massuus.vaultpartyui.client;
|
||||
|
||||
import dev.massuus.vaultpartyui.VaultPartyUiMod;
|
||||
import iskallia.vault.client.data.ClientPartyData;
|
||||
import iskallia.vault.client.data.ClientPartyInviteState;
|
||||
import dev.massuus.vaultpartyui.client.screen.PartyScreen;
|
||||
import iskallia.vault.network.message.ServerboundPartyInviteResponseMessage;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.TranslatableComponent;
|
||||
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;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Mod.EventBusSubscriber(modid = VaultPartyUiMod.MODID, value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.FORGE)
|
||||
public final class ClientTickEvents {
|
||||
private ClientTickEvents() {
|
||||
@@ -26,5 +33,24 @@ public final class ClientTickEvents {
|
||||
minecraft.setScreen(new PartyScreen(minecraft.screen));
|
||||
}
|
||||
}
|
||||
|
||||
if (minecraft.player == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (ClientPartySettings.isAutoAcceptInvitesEnabled()
|
||||
&& ClientPartyData.getParty(minecraft.player.getUUID()) == null
|
||||
&& ClientPartyInviteState.hasPendingInvite()) {
|
||||
String inviterName = ClientPartyInviteState.getInviterName();
|
||||
UUID inviteId = ClientPartyInviteState.getInviteId();
|
||||
if (inviteId != null) {
|
||||
ServerboundPartyInviteResponseMessage.send(inviteId, true);
|
||||
ClientPartyInviteState.clearInvite();
|
||||
if (inviterName != null && !inviterName.isEmpty()) {
|
||||
Component msg = new TranslatableComponent("screen.vaultpartyui.auto_accepted", inviterName);
|
||||
minecraft.player.displayClientMessage(msg, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package dev.massuus.vaultpartyui.client.screen;
|
||||
|
||||
enum FilterMode {
|
||||
ALL,
|
||||
ACTIONABLE,
|
||||
OTHER_PARTY
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package dev.massuus.vaultpartyui.client.screen;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
final class OnlinePlayer {
|
||||
final UUID id;
|
||||
final String name;
|
||||
|
||||
OnlinePlayer(UUID id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package dev.massuus.vaultpartyui.client.screen;
|
||||
|
||||
final class OnlineRow {
|
||||
final OnlinePlayer player;
|
||||
final RowState state;
|
||||
|
||||
OnlineRow(OnlinePlayer player, RowState state) {
|
||||
this.player = player;
|
||||
this.state = state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
package dev.massuus.vaultpartyui.client.screen;
|
||||
|
||||
import iskallia.vault.client.data.ClientPartyData;
|
||||
import iskallia.vault.world.data.VaultPartyData.Party;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
final class PartyRosterService {
|
||||
private PartyRosterService() {
|
||||
}
|
||||
|
||||
static List<OnlineRow> buildRows(
|
||||
List<OnlinePlayer> players,
|
||||
FilterMode filterMode,
|
||||
Party currentParty,
|
||||
UUID localPlayerId,
|
||||
Map<UUID, Long> inviteCooldownUntilMs
|
||||
) {
|
||||
if (players == null || players.isEmpty()) {
|
||||
return java.util.Collections.emptyList();
|
||||
}
|
||||
|
||||
List<OnlineRow> rows = new ArrayList<>();
|
||||
for (OnlinePlayer player : players) {
|
||||
RowState state = resolveRowState(player, currentParty, localPlayerId, inviteCooldownUntilMs);
|
||||
if (filterMode == FilterMode.ACTIONABLE && state != RowState.INVITEABLE && state != RowState.PARTY_MEMBER) {
|
||||
continue;
|
||||
}
|
||||
if (filterMode == FilterMode.OTHER_PARTY && state != RowState.OTHER_PARTY) {
|
||||
continue;
|
||||
}
|
||||
rows.add(new OnlineRow(player, state));
|
||||
}
|
||||
|
||||
rows.sort((a, b) -> {
|
||||
int p = Integer.compare(rowStatePriority(a.state), rowStatePriority(b.state));
|
||||
if (p != 0) return p;
|
||||
return a.player.name.compareToIgnoreCase(b.player.name);
|
||||
});
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
static boolean isPartyLeader(Party currentParty, UUID localPlayerId) {
|
||||
if (currentParty == null || localPlayerId == null) return false;
|
||||
UUID leader = currentParty.getLeader();
|
||||
return leader != null && leader.equals(localPlayerId);
|
||||
}
|
||||
|
||||
static boolean isLocalPlayerInParty(Party currentParty, UUID localPlayerId) {
|
||||
if (currentParty == null || localPlayerId == null) return false;
|
||||
UUID leader = currentParty.getLeader();
|
||||
if (leader != null && leader.equals(localPlayerId)) return true;
|
||||
List<UUID> members = currentParty.getMembers();
|
||||
if (members != null) {
|
||||
for (UUID m : members) {
|
||||
if (localPlayerId.equals(m)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static boolean isPlayerInCurrentParty(Party currentParty, UUID playerId) {
|
||||
if (currentParty == null || playerId == null) return false;
|
||||
UUID leader = currentParty.getLeader();
|
||||
if (leader != null && leader.equals(playerId)) return true;
|
||||
List<UUID> members = currentParty.getMembers();
|
||||
if (members != null) {
|
||||
for (UUID memberId : members) {
|
||||
if (playerId.equals(memberId)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static boolean isPlayerInOtherParty(Party currentParty, UUID playerId) {
|
||||
if (playerId == null) return false;
|
||||
Party party = ClientPartyData.getParty(playerId);
|
||||
if (party == null) return false;
|
||||
return !isPlayerInCurrentParty(currentParty, playerId);
|
||||
}
|
||||
|
||||
static void pruneCooldowns(Map<UUID, Long> cooldownMap, long nowMs) {
|
||||
if (cooldownMap == null || cooldownMap.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
java.util.Iterator<Map.Entry<UUID, Long>> it = cooldownMap.entrySet().iterator();
|
||||
while (it.hasNext()) {
|
||||
Map.Entry<UUID, Long> e = it.next();
|
||||
if (e.getValue() == null || e.getValue() <= nowMs) {
|
||||
it.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static RowState resolveRowState(
|
||||
OnlinePlayer player,
|
||||
Party currentParty,
|
||||
UUID localPlayerId,
|
||||
Map<UUID, Long> inviteCooldownUntilMs
|
||||
) {
|
||||
if (player == null || player.id == null) return RowState.NO_ACTION;
|
||||
if (localPlayerId != null && localPlayerId.equals(player.id)) return RowState.SELF;
|
||||
if (isPlayerInOtherParty(currentParty, player.id)) return RowState.OTHER_PARTY;
|
||||
if (!isLocalPlayerInParty(currentParty, localPlayerId)) return RowState.NO_ACTION;
|
||||
if (isPlayerInCurrentParty(currentParty, player.id)) return RowState.PARTY_MEMBER;
|
||||
Long until = inviteCooldownUntilMs == null ? null : inviteCooldownUntilMs.get(player.id);
|
||||
if (until != null && until > System.currentTimeMillis()) return RowState.COOLDOWN;
|
||||
return RowState.INVITEABLE;
|
||||
}
|
||||
|
||||
private static int rowStatePriority(RowState state) {
|
||||
if (state == null) return 99;
|
||||
switch (state) {
|
||||
case INVITEABLE:
|
||||
return 0;
|
||||
case COOLDOWN:
|
||||
return 1;
|
||||
case PARTY_MEMBER:
|
||||
return 2;
|
||||
case OTHER_PARTY:
|
||||
return 3;
|
||||
case SELF:
|
||||
return 4;
|
||||
default:
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.ClientPartySettings;
|
||||
import iskallia.vault.client.data.ClientPartyData;
|
||||
import iskallia.vault.client.data.ClientPartyInviteState;
|
||||
import iskallia.vault.network.message.ServerboundPartyInviteResponseMessage;
|
||||
@@ -24,20 +25,25 @@ import net.minecraft.util.Mth;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
|
||||
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_TOP = 124;
|
||||
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 static final int HEAD_SIZE = 8;
|
||||
private static final long INVITE_COOLDOWN_MS = 8000L;
|
||||
private static final int STATE_REFRESH_INTERVAL_TICKS = 4;
|
||||
|
||||
private final Screen parentScreen;
|
||||
|
||||
@@ -51,7 +57,12 @@ public class PartyScreen extends Screen {
|
||||
private Button inviteAllButton;
|
||||
private Button acceptInviteButton;
|
||||
private Button declineInviteButton;
|
||||
private Button autoAcceptToggleButton;
|
||||
private int onlineScrollOffset;
|
||||
private int selectedOnlineIndex = -1;
|
||||
private int stateRefreshTicks;
|
||||
private final Map<UUID, Long> inviteCooldownUntilMs = new HashMap<>();
|
||||
private final List<UiToast> toasts = new ArrayList<>();
|
||||
|
||||
public PartyScreen(Screen parentScreen) {
|
||||
super(new TranslatableComponent("screen.vaultpartyui.title"));
|
||||
@@ -67,18 +78,23 @@ public class PartyScreen extends Screen {
|
||||
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.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")));
|
||||
|
||||
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")));
|
||||
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")));
|
||||
|
||||
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()));
|
||||
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 -> {
|
||||
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;
|
||||
@@ -93,7 +109,14 @@ public class PartyScreen extends Screen {
|
||||
@Override
|
||||
public void tick() {
|
||||
super.tick();
|
||||
this.stateRefreshTicks++;
|
||||
if (this.stateRefreshTicks >= STATE_REFRESH_INTERVAL_TICKS) {
|
||||
this.stateRefreshTicks = 0;
|
||||
rebuildState();
|
||||
}
|
||||
|
||||
pruneTransientState();
|
||||
|
||||
if (this.targetBox != null) {
|
||||
this.targetBox.tick();
|
||||
}
|
||||
@@ -154,6 +177,8 @@ public class PartyScreen extends Screen {
|
||||
this.font.drawShadow(poseStack, tip, tipX, tipY, 0xFFFFFF);
|
||||
}
|
||||
|
||||
renderToasts(poseStack);
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -194,7 +219,7 @@ public class PartyScreen extends Screen {
|
||||
return super.mouseScrolled(mouseX, mouseY, scrollDelta);
|
||||
}
|
||||
|
||||
List<OnlinePlayer> visiblePlayers = filteredOnlinePlayers();
|
||||
List<OnlineRow> visiblePlayers = filteredOnlineRows();
|
||||
int maxOffset = Math.max(0, visiblePlayers.size() - VISIBLE_ONLINE_ROWS);
|
||||
if (maxOffset == 0) {
|
||||
return true;
|
||||
@@ -210,21 +235,68 @@ public class PartyScreen extends Screen {
|
||||
this.minecraft.setScreen(this.parentScreen);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
|
||||
if (this.targetBox != null && this.targetBox.isFocused()) {
|
||||
return super.keyPressed(keyCode, scanCode, modifiers);
|
||||
}
|
||||
|
||||
List<OnlineRow> rows = filteredOnlineRows();
|
||||
if (rows.isEmpty()) {
|
||||
return super.keyPressed(keyCode, scanCode, modifiers);
|
||||
}
|
||||
|
||||
if (keyCode == GLFW.GLFW_KEY_DOWN) {
|
||||
if (this.selectedOnlineIndex < 0) {
|
||||
this.selectedOnlineIndex = 0;
|
||||
} else {
|
||||
this.selectedOnlineIndex = Math.min(rows.size() - 1, this.selectedOnlineIndex + 1);
|
||||
}
|
||||
ensureSelectedRowVisible(rows.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
if (keyCode == GLFW.GLFW_KEY_UP) {
|
||||
if (this.selectedOnlineIndex < 0) {
|
||||
this.selectedOnlineIndex = rows.size() - 1;
|
||||
} else {
|
||||
this.selectedOnlineIndex = Math.max(0, this.selectedOnlineIndex - 1);
|
||||
}
|
||||
ensureSelectedRowVisible(rows.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
if (keyCode == GLFW.GLFW_KEY_ENTER || keyCode == GLFW.GLFW_KEY_KP_ENTER) {
|
||||
if (this.selectedOnlineIndex >= 0 && this.selectedOnlineIndex < rows.size()) {
|
||||
performPrimaryRowAction(rows.get(this.selectedOnlineIndex));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return super.keyPressed(keyCode, scanCode, modifiers);
|
||||
}
|
||||
|
||||
private void rebuildState() {
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
if (minecraft.player == null) {
|
||||
this.currentParty = null;
|
||||
this.onlinePlayers = Collections.emptyList();
|
||||
this.onlineScrollOffset = 0;
|
||||
this.selectedOnlineIndex = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentParty = ClientPartyData.getParty(minecraft.player.getUUID());
|
||||
this.onlinePlayers = gatherOnlinePlayers(minecraft.getConnection());
|
||||
|
||||
List<OnlinePlayer> visiblePlayers = filteredOnlinePlayers();
|
||||
List<OnlineRow> visiblePlayers = filteredOnlineRows();
|
||||
int maxOffset = Math.max(0, visiblePlayers.size() - VISIBLE_ONLINE_ROWS);
|
||||
this.onlineScrollOffset = Mth.clamp(this.onlineScrollOffset, 0, maxOffset);
|
||||
if (visiblePlayers.isEmpty()) {
|
||||
this.selectedOnlineIndex = -1;
|
||||
} else {
|
||||
this.selectedOnlineIndex = Mth.clamp(this.selectedOnlineIndex, 0, visiblePlayers.size() - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private List<OnlinePlayer> gatherOnlinePlayers(ClientPacketListener connection) {
|
||||
@@ -263,6 +335,16 @@ public class PartyScreen extends Screen {
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private List<OnlineRow> filteredOnlineRows() {
|
||||
return PartyRosterService.buildRows(
|
||||
filteredOnlinePlayers(),
|
||||
FilterMode.ALL,
|
||||
this.currentParty,
|
||||
getLocalPlayerId(),
|
||||
this.inviteCooldownUntilMs
|
||||
);
|
||||
}
|
||||
|
||||
private void sendPartyCommand(String command) {
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
ClientPacketListener connection = minecraft.getConnection();
|
||||
@@ -298,7 +380,7 @@ public class PartyScreen extends Screen {
|
||||
}
|
||||
|
||||
private void updateInviteButtons() {
|
||||
boolean hasInvite = ClientPartyInviteState.hasPendingInvite() && this.currentParty == null;
|
||||
boolean hasInvite = ClientPartyInviteState.hasPendingInvite() && this.currentParty == null && !ClientPartySettings.isAutoAcceptInvitesEnabled();
|
||||
if (this.acceptInviteButton != null) {
|
||||
this.acceptInviteButton.visible = hasInvite;
|
||||
}
|
||||
@@ -308,7 +390,7 @@ public class PartyScreen extends Screen {
|
||||
}
|
||||
|
||||
private void updateActionVisibility() {
|
||||
boolean inParty = this.currentParty != null;
|
||||
boolean inParty = isLocalPlayerInParty();
|
||||
int centerX = this.width / 2;
|
||||
|
||||
if (this.createPartyButton != null) {
|
||||
@@ -336,7 +418,10 @@ public class PartyScreen extends Screen {
|
||||
if (this.inviteAllButton != null) {
|
||||
this.inviteAllButton.visible = inParty;
|
||||
}
|
||||
|
||||
if (this.autoAcceptToggleButton != null) {
|
||||
this.autoAcceptToggleButton.visible = true;
|
||||
this.autoAcceptToggleButton.active = true;
|
||||
}
|
||||
// Keep the second action row centered as a pair.
|
||||
if (this.inviteNearbyButton != null && this.inviteAllButton != null) {
|
||||
int rowWidth = BUTTON_WIDTH * 2 + BUTTON_GAP;
|
||||
@@ -346,6 +431,17 @@ public class PartyScreen extends Screen {
|
||||
}
|
||||
|
||||
updateInviteButtons();
|
||||
updateAutoAcceptToggleLabel();
|
||||
}
|
||||
|
||||
private Component autoAcceptToggleLabel() {
|
||||
return new TranslatableComponent(ClientPartySettings.isAutoAcceptInvitesEnabled() ? "screen.vaultpartyui.auto_accept_on" : "screen.vaultpartyui.auto_accept_off");
|
||||
}
|
||||
|
||||
private void updateAutoAcceptToggleLabel() {
|
||||
if (this.autoAcceptToggleButton != null) {
|
||||
this.autoAcceptToggleButton.setMessage(autoAcceptToggleLabel());
|
||||
}
|
||||
}
|
||||
|
||||
private void renderPartyPanel(PoseStack poseStack, int panelX, int panelWidth, int mouseX, int mouseY) {
|
||||
@@ -382,7 +478,7 @@ public class PartyScreen extends Screen {
|
||||
color = statusColor(cachedMember.status);
|
||||
}
|
||||
|
||||
drawPlayerHead(poseStack, memberId, textX, textY + 1);
|
||||
drawPlayerHead(poseStack, memberId, textX, textY);
|
||||
this.font.draw(poseStack, line.toString(), textX + HEAD_SIZE + 4, textY, color);
|
||||
textY += 14;
|
||||
}
|
||||
@@ -390,16 +486,20 @@ public class PartyScreen extends Screen {
|
||||
|
||||
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();
|
||||
List<OnlineRow> visiblePlayers = filteredOnlineRows();
|
||||
int maxOffset = Math.max(0, visiblePlayers.size() - VISIBLE_ONLINE_ROWS);
|
||||
this.onlineScrollOffset = Mth.clamp(this.onlineScrollOffset, 0, maxOffset);
|
||||
if (visiblePlayers.isEmpty()) {
|
||||
this.selectedOnlineIndex = -1;
|
||||
} else {
|
||||
this.selectedOnlineIndex = Mth.clamp(this.selectedOnlineIndex, 0, visiblePlayers.size() - 1);
|
||||
}
|
||||
|
||||
int startIndex = this.onlineScrollOffset;
|
||||
int endIndex = Math.min(visiblePlayers.size(), startIndex + VISIBLE_ONLINE_ROWS);
|
||||
@@ -408,35 +508,33 @@ public class PartyScreen extends Screen {
|
||||
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);
|
||||
this.font.draw(poseStack, new TranslatableComponent("screen.vaultpartyui.no_matching").getString(), textX, listTop + 6, 0xA0A0A0);
|
||||
return;
|
||||
}
|
||||
|
||||
int rowY = listTop + 4;
|
||||
for (int index = startIndex; index < endIndex; index++) {
|
||||
OnlinePlayer player = visiblePlayers.get(index);
|
||||
OnlineRow row = visiblePlayers.get(index);
|
||||
OnlinePlayer player = row.player;
|
||||
boolean hovered = mouseX >= panelX + 10 && mouseX <= panelX + panelWidth - 10 && mouseY >= rowY - 2 && mouseY < rowY + ONLINE_ROW_HEIGHT - 2;
|
||||
int background = hovered ? 0x663C3122 : 0x00000000;
|
||||
boolean selected = index == this.selectedOnlineIndex;
|
||||
int background = RowPresentation.backgroundColor(row.state, hovered, selected);
|
||||
|
||||
// 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 + 1);
|
||||
this.font.draw(poseStack, player.name, panelX + 12 + HEAD_SIZE + 4, rowY, 0xFFFFFF);
|
||||
drawPlayerHead(poseStack, player.id, panelX + 12, rowY);
|
||||
this.font.draw(poseStack, player.name, panelX + 12 + HEAD_SIZE + 4, rowY, RowPresentation.nameColor(row.state));
|
||||
|
||||
// skip drawing actions for self
|
||||
if (!player.id.equals(getLocalPlayerId())) {
|
||||
int actionX = panelX + panelWidth - 84;
|
||||
boolean targetIsInParty = isPlayerInCurrentParty(player.id);
|
||||
if (isLocalPlayerInParty()) {
|
||||
if (targetIsInParty) {
|
||||
// Remove button for current members, leader only
|
||||
if (isPartyLeader()) {
|
||||
this.font.draw(poseStack, new TranslatableComponent("screen.vaultpartyui.remove").getString(), actionX, rowY, 0xE0A0A0);
|
||||
}
|
||||
} else {
|
||||
// Invite button for non-members, visible to any party member
|
||||
this.font.draw(poseStack, new TranslatableComponent("screen.vaultpartyui.invite").getString(), actionX, rowY, 0xA0E0A0);
|
||||
int actionX = panelX + panelWidth - 110;
|
||||
Component action = RowPresentation.actionLabel(row, isPartyLeader());
|
||||
if (action != null) {
|
||||
this.font.draw(poseStack, action.getString(), actionX, rowY, RowPresentation.actionColor(row.state));
|
||||
}
|
||||
|
||||
if (hovered) {
|
||||
Component hint = RowPresentation.tooltip(row, isPartyLeader());
|
||||
if (hint != null) {
|
||||
renderTooltip(poseStack, hint, mouseX, mouseY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -462,70 +560,114 @@ public class PartyScreen extends Screen {
|
||||
int listTop = PANEL_TOP + 48;
|
||||
int relativeY = (int)mouseY - listTop - 4;
|
||||
int index = this.onlineScrollOffset + (relativeY / ONLINE_ROW_HEIGHT);
|
||||
List<OnlinePlayer> visiblePlayers = filteredOnlinePlayers();
|
||||
List<OnlineRow> visiblePlayers = filteredOnlineRows();
|
||||
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;
|
||||
this.selectedOnlineIndex = index;
|
||||
|
||||
OnlineRow row = visiblePlayers.get(index);
|
||||
OnlinePlayer player = row.player;
|
||||
int actionY = listTop + (index - this.onlineScrollOffset) * ONLINE_ROW_HEIGHT + 4;
|
||||
// Use the same action bounds and logic as the renderer. The renderer places the action at
|
||||
// panelX + panelWidth - 84. Recompute panelWidth here for clarity.
|
||||
int panelWidth = (this.width - 40 - PANEL_PADDING) / 2;
|
||||
actionX = panelX + panelWidth - 84;
|
||||
if (mouseX >= actionX && mouseX <= actionX + 70 && mouseY >= actionY && mouseY <= actionY + ONLINE_ROW_HEIGHT - 2) {
|
||||
boolean targetIsInParty = isPlayerInCurrentParty(player.id);
|
||||
if (isLocalPlayerInParty()) {
|
||||
if (targetIsInParty) {
|
||||
if (isPartyLeader()) {
|
||||
sendPartyCommand("party remove " + player.name);
|
||||
}
|
||||
} else {
|
||||
sendPartyCommand("party invite " + player.name);
|
||||
}
|
||||
}
|
||||
int actionX = panelX + panelWidth - 110;
|
||||
if (mouseX >= actionX && mouseX <= actionX + 104 && mouseY >= actionY && mouseY <= actionY + ONLINE_ROW_HEIGHT - 2) {
|
||||
performPrimaryRowAction(row);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
// Clicking anywhere on row selects it for keyboard action.
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isPartyLeader() {
|
||||
if (this.currentParty == null) return false;
|
||||
UUID leader = this.currentParty.getLeader();
|
||||
return leader != null && leader.equals(getLocalPlayerId());
|
||||
return PartyRosterService.isPartyLeader(this.currentParty, getLocalPlayerId());
|
||||
}
|
||||
|
||||
private boolean isLocalPlayerInParty() {
|
||||
if (this.currentParty == null) return false;
|
||||
UUID local = getLocalPlayerId();
|
||||
if (local == null) return false;
|
||||
UUID leader = this.currentParty.getLeader();
|
||||
if (leader != null && leader.equals(local)) return true;
|
||||
List<UUID> members = this.currentParty.getMembers();
|
||||
if (members != null) {
|
||||
for (UUID m : members) {
|
||||
if (local.equals(m)) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return PartyRosterService.isLocalPlayerInParty(this.currentParty, getLocalPlayerId());
|
||||
}
|
||||
|
||||
private boolean isPlayerInCurrentParty(UUID playerId) {
|
||||
if (this.currentParty == null || playerId == null) return false;
|
||||
UUID leader = this.currentParty.getLeader();
|
||||
if (leader != null && leader.equals(playerId)) return true;
|
||||
List<UUID> members = this.currentParty.getMembers();
|
||||
if (members != null) {
|
||||
for (UUID memberId : members) {
|
||||
if (playerId.equals(memberId)) return true;
|
||||
}
|
||||
private boolean performPrimaryRowAction(OnlineRow row) {
|
||||
if (row == null || row.player == null) return false;
|
||||
OnlinePlayer player = row.player;
|
||||
switch (row.state) {
|
||||
case INVITEABLE:
|
||||
sendPartyCommand("party invite " + player.name);
|
||||
this.inviteCooldownUntilMs.put(player.id, System.currentTimeMillis() + INVITE_COOLDOWN_MS);
|
||||
pushToast(new TranslatableComponent("screen.vaultpartyui.toast_invited", player.name), 0xA0E0A0);
|
||||
return true;
|
||||
case PARTY_MEMBER:
|
||||
if (isPartyLeader()) {
|
||||
sendPartyCommand("party remove " + player.name);
|
||||
pushToast(new TranslatableComponent("screen.vaultpartyui.toast_removed", player.name), 0xE0A0A0);
|
||||
return true;
|
||||
}
|
||||
pushToast(new TranslatableComponent("screen.vaultpartyui.tip_member"), 0xE3C38C);
|
||||
return false;
|
||||
case OTHER_PARTY:
|
||||
Component msg = new TranslatableComponent("screen.vaultpartyui.already_in_party_local", player.name);
|
||||
showClientMessage(msg);
|
||||
pushToast(msg, 0xE3C38C);
|
||||
return false;
|
||||
case COOLDOWN:
|
||||
pushToast(new TranslatableComponent("screen.vaultpartyui.tip_cooldown"), 0xB0B0B0);
|
||||
return false;
|
||||
case NO_ACTION:
|
||||
pushToast(new TranslatableComponent("screen.vaultpartyui.tip_no_action"), 0xB0B0B0);
|
||||
return false;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureSelectedRowVisible(int rowCount) {
|
||||
if (rowCount <= 0 || this.selectedOnlineIndex < 0) return;
|
||||
if (this.selectedOnlineIndex < this.onlineScrollOffset) {
|
||||
this.onlineScrollOffset = this.selectedOnlineIndex;
|
||||
}
|
||||
int maxVisible = this.onlineScrollOffset + VISIBLE_ONLINE_ROWS - 1;
|
||||
if (this.selectedOnlineIndex > maxVisible) {
|
||||
this.onlineScrollOffset = this.selectedOnlineIndex - VISIBLE_ONLINE_ROWS + 1;
|
||||
}
|
||||
int maxOffset = Math.max(0, rowCount - VISIBLE_ONLINE_ROWS);
|
||||
this.onlineScrollOffset = Mth.clamp(this.onlineScrollOffset, 0, maxOffset);
|
||||
}
|
||||
|
||||
private void pruneTransientState() {
|
||||
long now = System.currentTimeMillis();
|
||||
PartyRosterService.pruneCooldowns(this.inviteCooldownUntilMs, now);
|
||||
|
||||
this.toasts.removeIf(t -> t.expiresAt <= now);
|
||||
}
|
||||
|
||||
private void pushToast(Component message, int color) {
|
||||
if (message == null) return;
|
||||
this.toasts.add(new UiToast(message, color, System.currentTimeMillis() + 2600L));
|
||||
if (this.toasts.size() > 3) {
|
||||
this.toasts.remove(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void renderToasts(PoseStack poseStack) {
|
||||
if (this.toasts.isEmpty()) return;
|
||||
int y = 34;
|
||||
for (UiToast toast : this.toasts) {
|
||||
String text = toast.message.getString();
|
||||
int w = this.font.width(text) + 10;
|
||||
int x = this.width - w - 10;
|
||||
fill(poseStack, x, y - 1, x + w, y + this.font.lineHeight + 3, 0xCC111111);
|
||||
this.font.drawShadow(poseStack, text, x + 5, y + 1, toast.color);
|
||||
y += this.font.lineHeight + 6;
|
||||
}
|
||||
}
|
||||
|
||||
private void showClientMessage(Component message) {
|
||||
Minecraft minecraft = Minecraft.getInstance();
|
||||
if (minecraft.player != null && message != null) {
|
||||
minecraft.player.displayClientMessage(message, false);
|
||||
}
|
||||
}
|
||||
|
||||
private UUID getLocalPlayerId() {
|
||||
@@ -616,13 +758,4 @@ public class PartyScreen extends Screen {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class OnlinePlayer {
|
||||
final UUID id;
|
||||
final String name;
|
||||
|
||||
OnlinePlayer(UUID id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
package dev.massuus.vaultpartyui.client.screen;
|
||||
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.TranslatableComponent;
|
||||
|
||||
final class RowPresentation {
|
||||
private RowPresentation() {
|
||||
}
|
||||
|
||||
static int backgroundColor(RowState state, boolean hovered, boolean selected) {
|
||||
if (selected) {
|
||||
return 0x66594A2D;
|
||||
}
|
||||
if (hovered) {
|
||||
return 0x663C3122;
|
||||
}
|
||||
if (state == RowState.OTHER_PARTY) {
|
||||
return 0x221A1A1A;
|
||||
}
|
||||
return 0x00000000;
|
||||
}
|
||||
|
||||
static int nameColor(RowState state) {
|
||||
if (state == RowState.OTHER_PARTY) return 0xC8B9A3;
|
||||
if (state == RowState.NO_ACTION) return 0xBBBBBB;
|
||||
return 0xFFFFFF;
|
||||
}
|
||||
|
||||
static int actionColor(RowState state) {
|
||||
if (state == RowState.INVITEABLE) return 0xA0E0A0;
|
||||
if (state == RowState.PARTY_MEMBER) return 0xE0A0A0;
|
||||
if (state == RowState.COOLDOWN) return 0xA0A0A0;
|
||||
if (state == RowState.OTHER_PARTY) return 0xE3C38C;
|
||||
return 0x909090;
|
||||
}
|
||||
|
||||
static Component actionLabel(OnlineRow row, boolean isPartyLeader) {
|
||||
if (row == null) return null;
|
||||
switch (row.state) {
|
||||
case INVITEABLE:
|
||||
return new TranslatableComponent("screen.vaultpartyui.invite");
|
||||
case PARTY_MEMBER:
|
||||
return isPartyLeader ? new TranslatableComponent("screen.vaultpartyui.remove") : new TranslatableComponent("screen.vaultpartyui.in_your_party");
|
||||
case OTHER_PARTY:
|
||||
return new TranslatableComponent("screen.vaultpartyui.in_party");
|
||||
case COOLDOWN:
|
||||
return new TranslatableComponent("screen.vaultpartyui.invited");
|
||||
case SELF:
|
||||
return new TranslatableComponent("screen.vaultpartyui.self");
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static Component tooltip(OnlineRow row, boolean isPartyLeader) {
|
||||
if (row == null) return null;
|
||||
switch (row.state) {
|
||||
case INVITEABLE:
|
||||
return new TranslatableComponent("screen.vaultpartyui.tip_invite");
|
||||
case PARTY_MEMBER:
|
||||
return isPartyLeader ? new TranslatableComponent("screen.vaultpartyui.tip_remove") : new TranslatableComponent("screen.vaultpartyui.tip_member");
|
||||
case OTHER_PARTY:
|
||||
return new TranslatableComponent("screen.vaultpartyui.tip_other_party");
|
||||
case COOLDOWN:
|
||||
return new TranslatableComponent("screen.vaultpartyui.tip_cooldown");
|
||||
case NO_ACTION:
|
||||
return new TranslatableComponent("screen.vaultpartyui.tip_no_action");
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package dev.massuus.vaultpartyui.client.screen;
|
||||
|
||||
enum RowState {
|
||||
SELF,
|
||||
PARTY_MEMBER,
|
||||
OTHER_PARTY,
|
||||
INVITEABLE,
|
||||
COOLDOWN,
|
||||
NO_ACTION
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package dev.massuus.vaultpartyui.client.screen;
|
||||
|
||||
import net.minecraft.network.chat.Component;
|
||||
|
||||
final class UiToast {
|
||||
final Component message;
|
||||
final int color;
|
||||
final long expiresAt;
|
||||
|
||||
UiToast(Component message, int color, long expiresAt) {
|
||||
this.message = message;
|
||||
this.color = color;
|
||||
this.expiresAt = expiresAt;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,11 @@
|
||||
"screen.vaultpartyui.remove_selected": "Remove Selected",
|
||||
"screen.vaultpartyui.accept_invite": "Accept Invite",
|
||||
"screen.vaultpartyui.decline_invite": "Decline Invite",
|
||||
"screen.vaultpartyui.auto_accept_on": "Auto accept invites: On",
|
||||
"screen.vaultpartyui.auto_accept_off": "Auto accept invites: Off",
|
||||
"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.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",
|
||||
@@ -25,6 +30,19 @@
|
||||
"screen.vaultpartyui.leader": "Leader",
|
||||
"screen.vaultpartyui.invite": "Invite",
|
||||
"screen.vaultpartyui.remove": "Remove",
|
||||
"screen.vaultpartyui.self": "You"
|
||||
,"screen.vaultpartyui.credit_tooltip": "Click to open the GitHub repo"
|
||||
"screen.vaultpartyui.in_party": "In Other Party",
|
||||
"screen.vaultpartyui.in_your_party": "In Party",
|
||||
"screen.vaultpartyui.invited": "Invited",
|
||||
"screen.vaultpartyui.already_in_party_local": "%s is already in another party.",
|
||||
"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_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",
|
||||
"screen.vaultpartyui.toast_invited": "Invited %s",
|
||||
"screen.vaultpartyui.toast_removed": "Removed %s",
|
||||
"screen.vaultpartyui.no_matching": "No matching players.",
|
||||
"screen.vaultpartyui.self": "You",
|
||||
"screen.vaultpartyui.credit_tooltip": "Click to open the GitHub repo"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user