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

32
.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
# Gradle
.gradle/
build/
out/
# Java
*.class
/scripts/
/local-maven/
hs_err_pid*
replay_pid*
# IDE/editor
.idea/
*.iml
*.ipr
*.iws
# VS Code local state
.vscode/*.log
# Forge/Minecraft run dirs
run/
logs/
crash-reports/
# OS files
.DS_Store
Thumbs.db
# Local helper artifacts
bin/

13
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,13 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "java",
"name": "Attach to Forge Client",
"request": "attach",
"hostName": "localhost",
"port": 5005,
"preLaunchTask": "runClientDebug"
}
]
}

4
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,4 @@
{
"java.compile.nullAnalysis.mode": "automatic",
"java.configuration.updateBuildConfiguration": "automatic"
}

61
.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,61 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "runClient",
"type": "shell",
"command": "${workspaceFolder}\\gradlew.bat",
"args": ["runClient"],
"group": "build",
"problemMatcher": []
},
{
"label": "runClientDebug",
"type": "shell",
"command": "${workspaceFolder}\\gradlew.bat",
"args": ["runClient", "--debug-jvm"],
"group": "build",
"isBackground": true,
"problemMatcher": {
"owner": "gradle-debug",
"pattern": {
"regexp": "^$"
},
"background": {
"activeOnStart": true,
"beginsPattern": ".*",
"endsPattern": "Listening for transport dt_socket at address: 5005"
}
}
}
,
{
"label": "build",
"type": "shell",
"command": "${workspaceFolder}\\gradlew.bat",
"args": ["build"],
"group": "build",
"problemMatcher": []
},
{
"label": "deployToCurseForge",
"type": "shell",
"command": "${workspaceFolder}\\scripts\\deploy.ps1",
"args": [],
"options": {
"shell": {
"executable": "powershell",
"args": ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File"]
}
},
"problemMatcher": []
},
{
"label": "build-and-deploy",
"type": "shell",
"dependsOn": ["build", "deployToCurseForge"],
"group": "build",
"problemMatcher": []
}
]
}

48
README.md Normal file
View File

@@ -0,0 +1,48 @@
# Vault Party UI
Client-side Forge mod for Vault Hunters (Minecraft 1.18.2) that provides a party management screen.
## Features
- Open a party UI with a keybind (default: `I`, rebindable in Controls).
- Create/leave/disband party actions.
- Invite nearby/all actions.
- Invite handling (accept/decline) when not already in a party.
- Party member list panel.
- Online player list with per-player `Invite` / `Remove` actions.
## Requirements
- Minecraft `1.18.2`
- Forge `40.x`
- Vault Hunters modpack (client)
## Development
Build in workspace root:
```powershell
.\gradlew.bat build
```
Compile only:
```powershell
.\gradlew.bat compileJava
```
## Deploy To CurseForge Instance
A helper script is included:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File .\scripts\deploy.ps1
```
Or use VS Code task: `build-and-deploy`.
## Notes
- This mod is client-side UI logic.
- Server still enforces party rules and command permissions.
- For real testing, run it inside the Vault Hunters pack client instance.

97
build.gradle Normal file
View File

@@ -0,0 +1,97 @@
plugins {
id 'eclipse'
id 'maven-publish'
id 'net.minecraftforge.gradle' version '5.1.+'
}
group = mod_group_id
version = mod_version
base {
archivesName = mod_id
}
java {
toolchain.languageVersion = JavaLanguageVersion.of(17)
}
minecraft {
mappings channel: mapping_channel, version: mapping_version
runs {
client {
workingDirectory project.file('run')
property 'forge.logging.console.level', 'debug'
mods {
vaultpartyui {
source sourceSets.main
}
}
}
server {
workingDirectory project.file('run')
property 'forge.logging.console.level', 'debug'
mods {
vaultpartyui {
source sourceSets.main
}
}
}
data {
workingDirectory project.file('run')
property 'forge.logging.console.level', 'debug'
args '--mod', mod_id, '--all', '--output', file('src/generated/resources/'), '--existing', file('src/main/resources/')
mods {
vaultpartyui {
source sourceSets.main
}
}
}
}
}
def vaultHuntersJar = file(providers.gradleProperty('vault_hunters_jar').orElse('C:/Users/samva/curseforge/minecraft/Instances/Vault Hunters Third Edition - Remastered/mods/the_vault-1.18.2-3.21.5-remastered.6574.jar').get())
def vaultHuntersGroup = 'dev.copilot.vault'
def vaultHuntersArtifact = 'the_vault'
def vaultHuntersVersion = '1.18.2-3.21.5-remastered.6574'
def vaultHuntersRepo = file("$rootDir/local-maven")
if (!vaultHuntersJar.exists()) {
throw new GradleException("Vault Hunters jar not found at ${vaultHuntersJar}. Set -Pvault_hunters_jar to the mod jar path.")
}
repositories {
maven {
url = 'https://maven.minecraftforge.net'
}
maven {
url = uri(vaultHuntersRepo)
}
}
dependencies {
minecraft "net.minecraftforge:forge:${minecraft_version}-${forge_version}"
compileOnly fg.deobf("${vaultHuntersGroup}:${vaultHuntersArtifact}:${vaultHuntersVersion}")
runtimeOnly fg.deobf("${vaultHuntersGroup}:${vaultHuntersArtifact}:${vaultHuntersVersion}")
}
tasks.withType(JavaCompile).configureEach {
options.encoding = 'UTF-8'
options.release = 17
}
jar {
manifest {
attributes([
'Specification-Title' : mod_name,
'Specification-Vendor' : 'Copilot',
'Specification-Version' : '1',
'Implementation-Title' : project.name,
'Implementation-Version' : mod_version,
'Implementation-Vendor' : 'Copilot',
'Implementation-Timestamp': new Date().format("yyyy-MM-dd'T'HH:mm:ssZ")
])
}
}

12
gradle.properties Normal file
View File

@@ -0,0 +1,12 @@
org.gradle.jvmargs=-Xmx3G
org.gradle.daemon=false
minecraft_version=1.18.2
forge_version=40.2.17
mapping_channel=official
mapping_version=1.18.2
mod_id=vaultpartyui
mod_name=Vault Party UI
mod_version=1.0.0
mod_group_id=dev.copilot.vaultpartyui

BIN
gradle/wrapper/gradle-cli.jar vendored Normal file

Binary file not shown.

BIN
gradle/wrapper/gradle-wrapper-shared.jar vendored Normal file

Binary file not shown.

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

4
gradlew vendored Normal file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env sh
APP_HOME=$(cd "$(dirname "$0")" && pwd -P)
CLASSPATH="$APP_HOME/gradle/wrapper/gradle-wrapper.jar"
exec "${JAVA_HOME:-java}" -Xmx64m -Xms64m -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"

14
gradlew.bat vendored Normal file
View File

@@ -0,0 +1,14 @@
@ECHO OFF
SETLOCAL
SET APP_HOME=%~dp0
SET APP_BASE_NAME=%~n0
SET DIRNAME=%~dp0
IF "%DIRNAME%"=="" SET DIRNAME=.
SET DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
SET CLASSPATH=%APP_HOME%gradle\wrapper\gradle-wrapper-shared.jar;%APP_HOME%gradle\wrapper\gradle-cli.jar;%APP_HOME%gradle\wrapper\gradle-wrapper.jar
IF DEFINED JAVA_HOME (
SET JAVA_EXE=%JAVA_HOME%\bin\java.exe
) ELSE (
SET JAVA_EXE=java.exe
)
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% -Dorg.gradle.appname=%APP_BASE_NAME% -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*

View File

@@ -0,0 +1,9 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dev.copilot.vault</groupId>
<artifactId>the_vault</artifactId>
<version>1.18.2-3.21.5-remastered.6574</version>
<packaging>jar</packaging>
</project>

8
settings.gradle Normal file
View File

@@ -0,0 +1,8 @@
pluginManagement {
repositories {
gradlePluginPortal()
maven { url = 'https://maven.minecraftforge.net' }
}
}
rootProject.name = 'vault-party-ui'

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;
}
}
}

View File

@@ -0,0 +1,27 @@
modLoader="javafml"
loaderVersion="[40,)"
license="All Rights Reserved"
[[mods]]
modId="vaultpartyui"
version="${file.jarVersion}"
displayName="Vault Party UI"
authors="Massuus"
description='''
Adds a client-side party management screen for Vault Hunters 1.18.2.
'''
displayTest="IGNORE_ALL_VERSION"
[[dependencies.vaultpartyui]]
modId="forge"
mandatory=true
versionRange="[40,)"
ordering="NONE"
side="CLIENT"
[[dependencies.vaultpartyui]]
modId="minecraft"
mandatory=true
versionRange="[1.18.2,1.19)"
ordering="NONE"
side="CLIENT"

View File

@@ -0,0 +1,27 @@
{
"key.vaultpartyui.open_party_ui": "Open Party UI",
"key.categories.vaultpartyui": "Vault Party UI",
"screen.vaultpartyui.title": "Party Manager",
"screen.vaultpartyui.party": "Your Party",
"screen.vaultpartyui.players": "Online Players",
"screen.vaultpartyui.target": "Player Name",
"screen.vaultpartyui.search_hint": "Type a name to filter or invite",
"screen.vaultpartyui.create": "Create Party",
"screen.vaultpartyui.leave": "Leave Party",
"screen.vaultpartyui.disband": "Disband Party",
"screen.vaultpartyui.list": "List Party",
"screen.vaultpartyui.invite_selected": "Invite Selected",
"screen.vaultpartyui.invite_nearby": "Invite Nearby",
"screen.vaultpartyui.invite_all": "Invite All",
"screen.vaultpartyui.remove_selected": "Remove Selected",
"screen.vaultpartyui.accept_invite": "Accept Invite",
"screen.vaultpartyui.decline_invite": "Decline Invite",
"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",
"screen.vaultpartyui.members": "Members",
"screen.vaultpartyui.online": "Online players",
"screen.vaultpartyui.leader": "Leader",
"screen.vaultpartyui.self": "You"
}

View File

@@ -0,0 +1,6 @@
{
"pack": {
"pack_format": 8,
"description": "Vault Party UI resources"
}
}