commit 5a6bb2c2fb0228a897395156f589325871adbf96 Author: Sam van Remortel Date: Sun May 24 15:52:40 2026 +0200 Initial commit: Vault Party UI (massuus) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d706099 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..1e73a60 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "java", + "name": "Attach to Forge Client", + "request": "attach", + "hostName": "localhost", + "port": 5005, + "preLaunchTask": "runClientDebug" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d53ecaf --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "java.compile.nullAnalysis.mode": "automatic", + "java.configuration.updateBuildConfiguration": "automatic" +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..04a593c --- /dev/null +++ b/.vscode/tasks.json @@ -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": [] + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..4daa71e --- /dev/null +++ b/README.md @@ -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. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..02a7a66 --- /dev/null +++ b/build.gradle @@ -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") + ]) + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..5bad4fa --- /dev/null +++ b/gradle.properties @@ -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 diff --git a/gradle/wrapper/gradle-cli.jar b/gradle/wrapper/gradle-cli.jar new file mode 100644 index 0000000..79e8388 Binary files /dev/null and b/gradle/wrapper/gradle-cli.jar differ diff --git a/gradle/wrapper/gradle-wrapper-shared.jar b/gradle/wrapper/gradle-wrapper-shared.jar new file mode 100644 index 0000000..e7f282d Binary files /dev/null and b/gradle/wrapper/gradle-wrapper-shared.jar differ diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..0323660 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..c7d437b --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..7bb12bd --- /dev/null +++ b/gradlew @@ -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 "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..fce14fe --- /dev/null +++ b/gradlew.bat @@ -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 %* diff --git a/local-maven/dev/copilot/vault/the_vault/1.18.2-3.21.5-remastered.6574/the_vault-1.18.2-3.21.5-remastered.6574.pom b/local-maven/dev/copilot/vault/the_vault/1.18.2-3.21.5-remastered.6574/the_vault-1.18.2-3.21.5-remastered.6574.pom new file mode 100644 index 0000000..60219f4 --- /dev/null +++ b/local-maven/dev/copilot/vault/the_vault/1.18.2-3.21.5-remastered.6574/the_vault-1.18.2-3.21.5-remastered.6574.pom @@ -0,0 +1,9 @@ + + 4.0.0 + dev.copilot.vault + the_vault + 1.18.2-3.21.5-remastered.6574 + jar + diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..6f5b05f --- /dev/null +++ b/settings.gradle @@ -0,0 +1,8 @@ +pluginManagement { + repositories { + gradlePluginPortal() + maven { url = 'https://maven.minecraftforge.net' } + } +} + +rootProject.name = 'vault-party-ui' diff --git a/src/main/java/dev/massuus/vaultpartyui/VaultPartyUiMod.java b/src/main/java/dev/massuus/vaultpartyui/VaultPartyUiMod.java new file mode 100644 index 0000000..c39d1db --- /dev/null +++ b/src/main/java/dev/massuus/vaultpartyui/VaultPartyUiMod.java @@ -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() { + } +} diff --git a/src/main/java/dev/massuus/vaultpartyui/client/ClientKeyMappings.java b/src/main/java/dev/massuus/vaultpartyui/client/ClientKeyMappings.java new file mode 100644 index 0000000..dcb4a2b --- /dev/null +++ b/src/main/java/dev/massuus/vaultpartyui/client/ClientKeyMappings.java @@ -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); + } +} diff --git a/src/main/java/dev/massuus/vaultpartyui/client/ClientSetupEvents.java b/src/main/java/dev/massuus/vaultpartyui/client/ClientSetupEvents.java new file mode 100644 index 0000000..6285a55 --- /dev/null +++ b/src/main/java/dev/massuus/vaultpartyui/client/ClientSetupEvents.java @@ -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); + } +} diff --git a/src/main/java/dev/massuus/vaultpartyui/client/ClientTickEvents.java b/src/main/java/dev/massuus/vaultpartyui/client/ClientTickEvents.java new file mode 100644 index 0000000..86c7245 --- /dev/null +++ b/src/main/java/dev/massuus/vaultpartyui/client/ClientTickEvents.java @@ -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)); + } + } + } +} diff --git a/src/main/java/dev/massuus/vaultpartyui/client/screen/PartyScreen.java b/src/main/java/dev/massuus/vaultpartyui/client/screen/PartyScreen.java new file mode 100644 index 0000000..8ba4df9 --- /dev/null +++ b/src/main/java/dev/massuus/vaultpartyui/client/screen/PartyScreen.java @@ -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 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 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 visiblePlayers = filteredOnlinePlayers(); + int maxOffset = Math.max(0, visiblePlayers.size() - VISIBLE_ONLINE_ROWS); + this.onlineScrollOffset = Mth.clamp(this.onlineScrollOffset, 0, maxOffset); + } + + private List gatherOnlinePlayers(ClientPacketListener connection) { + if (connection == null) { + return Collections.emptyList(); + } + + List 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 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 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 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 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 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; + } + } +} diff --git a/src/main/resources/META-INF/mods.toml b/src/main/resources/META-INF/mods.toml new file mode 100644 index 0000000..6672ccf --- /dev/null +++ b/src/main/resources/META-INF/mods.toml @@ -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" diff --git a/src/main/resources/assets/vaultpartyui/lang/en_us.json b/src/main/resources/assets/vaultpartyui/lang/en_us.json new file mode 100644 index 0000000..9099a42 --- /dev/null +++ b/src/main/resources/assets/vaultpartyui/lang/en_us.json @@ -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" +} diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta new file mode 100644 index 0000000..e1d4627 --- /dev/null +++ b/src/main/resources/pack.mcmeta @@ -0,0 +1,6 @@ +{ + "pack": { + "pack_format": 8, + "description": "Vault Party UI resources" + } +}