From cafdabb2e5cfc1878ea2f21042a0452a4865d603 Mon Sep 17 00:00:00 2001 From: PiBOH Date: Thu, 6 Aug 2026 19:11:57 +0200 Subject: [PATCH 01/52] v1.0.0 --- .github/ISSUE_TEMPLATE/bug_report-en.yml | 191 ++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 5 + .github/ISSUE_TEMPLATE/feature_request-en.yml | 83 ++++++++ .github/workflows/auto-release.yml | 142 +++++++++++++ src/main/resources/fabric.mod.json | 3 +- 5 files changed, 423 insertions(+), 1 deletion(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report-en.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request-en.yml create mode 100644 .github/workflows/auto-release.yml diff --git a/.github/ISSUE_TEMPLATE/bug_report-en.yml b/.github/ISSUE_TEMPLATE/bug_report-en.yml new file mode 100644 index 0000000..e8602f8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report-en.yml @@ -0,0 +1,191 @@ +name: "🐛 Bug Report (English)" +description: "Something is not working as expected? Help us investigate it." +title: "[Bug]: " +labels: + - "bug" +body: + - type: markdown + attributes: + value: | + ### 🛠️ DedicatedPower Bug Report + + Thank you for helping improve DedicatedPower. Please provide as much technical detail as possible, especially when reporting a Minecraft or Fabric compatibility problem. + + - type: checkboxes + id: checklist + attributes: + label: Pre-flight checklist + description: Please confirm these points before submitting the report. + options: + - label: I have searched the existing issues and this does not appear to be a duplicate. + required: true + - label: I am using a supported and unmodified Minecraft/Fabric installation, or I have listed my other mods below. + required: true + - label: I have included the relevant crash report, latest.log, or server log when available. + required: true + + - type: dropdown + id: area + attributes: + label: Affected area + description: Which part of DedicatedPower is affected? + options: + - Dedicated server GUI startup + - Server console and log panel + - Command input or autocomplete + - Player list or player management + - Player statistics and performance graphs + - Server menu bar + - World settings or game rules + - Backup, export, or server tools + - Mixin or access widener failure + - Build or development environment + - Other + validations: + required: true + + - type: dropdown + id: frequency + attributes: + label: Reproduction frequency + description: How often can you reproduce the problem? + options: + - Always (100%) + - Often (more than 50%) + - Sometimes (less than 50%) + - Rarely (once or twice) + - I cannot reproduce it consistently + validations: + required: true + + - type: textarea + id: summary + attributes: + label: What happened? + description: Describe the problem clearly and explain what you were doing when it occurred. + placeholder: "When I start the dedicated server, ..." + validations: + required: true + + - type: textarea + id: reproduce + attributes: + label: Steps to reproduce + description: Provide precise steps that allow someone else to reproduce the issue. + placeholder: | + 1. Install Minecraft ... + 2. Install Fabric Loader ... + 3. Add DedicatedPower ... + 4. Start the dedicated server ... + 5. Open or use ... + value: | + 1. + 2. + 3. + 4. + validations: + required: true + + - type: textarea + id: expected-actual + attributes: + label: Expected behavior vs. actual behavior + description: What did you expect to happen, and what happened instead? + placeholder: | + Expected: + + Actual: + validations: + required: true + + - type: markdown + attributes: + value: "---" + + - type: input + id: dedicatedpower-version + attributes: + label: DedicatedPower version or commit + description: Include the release version, build artifact version, or Git commit you are using. + placeholder: "1.21.11-1.0 or abc1234" + validations: + required: true + + - type: input + id: minecraft-version + attributes: + label: Minecraft version + description: Include the exact version, for example 26.2 or 1.21.11. + placeholder: "26.2" + validations: + required: true + + - type: input + id: loader-version + attributes: + label: Fabric Loader version + placeholder: "0.19.3" + validations: + required: true + + - type: input + id: fabric-api-version + attributes: + label: Fabric API version + description: Write "not installed" if you are testing without Fabric API. + placeholder: "0.152.0+26.2" + validations: + required: true + + - type: input + id: java-version + attributes: + label: Java version + description: Include the full output of java -version when possible. + placeholder: "Java 25.0.x" + validations: + required: true + + - type: dropdown + id: operating-system + attributes: + label: Operating system + options: + - Windows 11 + - Windows 10 + - macOS + - Linux + - Other + validations: + required: true + + - type: textarea + id: mods + attributes: + label: Other installed mods + description: List all other mods and their versions, or write "DedicatedPower only". + placeholder: | + - Mod name - version + - Mod name - version + + - type: textarea + id: logs + attributes: + label: Logs or crash report + description: Paste the relevant log or crash report here and remove sensitive information such as IP addresses or passwords. + placeholder: | + Paste the relevant log or crash report here. + render: shell + + - type: textarea + id: screenshots + attributes: + label: Screenshots or recordings + description: Drag and drop screenshots or recordings here if they help demonstrate the issue. + + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Add anything else that may help, such as recent updates, configuration changes, or a minimal reproduction. + placeholder: "This started after ..." diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..399c4ca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: "💬 GitHub Discussions" + url: "https://github.com/PiBOH/DedicatedPower/discussions" + about: "Use Discussions for questions, setup help, compatibility questions, and general support." diff --git a/.github/ISSUE_TEMPLATE/feature_request-en.yml b/.github/ISSUE_TEMPLATE/feature_request-en.yml new file mode 100644 index 0000000..b38bee3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request-en.yml @@ -0,0 +1,83 @@ +name: "🚀 Feature Request (English)" +description: "Have an idea to improve DedicatedPower? Tell us about it." +title: "[Feature]: " +labels: + - "enhancement" +body: + - type: markdown + attributes: + value: | + ### 💡 DedicatedPower Feature Proposal + + We welcome practical ideas that make dedicated Minecraft server management easier, safer, and more powerful. + + - type: checkboxes + id: checklist + attributes: + label: Pre-flight checklist + description: Please check these points before submitting your proposal. + options: + - label: I have searched the existing issues and this idea has not already been proposed. + required: true + - label: This proposal is specifically related to DedicatedPower or its dedicated server workflow. + required: true + - label: I have separated this proposal from unrelated bugs or general Minecraft support questions. + required: true + + - type: dropdown + id: category + attributes: + label: Feature category + description: Which area would this proposal improve? + options: + - Dedicated server GUI + - Console, logs, or command autocomplete + - Player list or player administration + - Performance monitoring or statistics + - Server, world, or game rule management + - Backups, exports, or server maintenance + - Minecraft 26.2 or future-version compatibility + - Accessibility or usability + - Developer tooling or documentation + - Other + validations: + required: true + + - type: textarea + id: problem + attributes: + label: What problem would this solve? + description: Explain the current limitation, workflow problem, or use case behind your proposal. + placeholder: "When managing a dedicated server, it is difficult to ..." + validations: + required: true + + - type: textarea + id: proposal + attributes: + label: What would you like to see? + description: Describe the feature in detail, including how it should work from a user's perspective. + placeholder: "I would like DedicatedPower to ..." + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Describe any alternative solution, workaround, command, or existing mod feature you have considered. + placeholder: "I have considered ..." + + - type: textarea + id: implementation + attributes: + label: Implementation ideas + description: If you have technical ideas, mention relevant Minecraft, Fabric, Swing, mixin, or API details. This section is optional. + placeholder: "A possible implementation could use ..." + + - type: textarea + id: mockups + attributes: + label: Mockups, examples, or extra context + description: Drag and drop screenshots, sketches, links, command examples, or other supporting material here. + placeholder: "Example layout, screenshot, or reference: ..." diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml new file mode 100644 index 0000000..36aa144 --- /dev/null +++ b/.github/workflows/auto-release.yml @@ -0,0 +1,142 @@ +name: Auto Release + +on: + push: + branches: + - main + +permissions: + contents: write + +jobs: + release: + name: Build and publish release + if: startsWith(github.event.head_commit.message, 'v') + runs-on: ubuntu-24.04 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: microsoft + + - name: Make Gradle wrapper executable + run: chmod +x ./gradlew + + - name: Parse release version and channel + id: release + shell: bash + env: + COMMIT_TITLE: ${{ github.event.head_commit.message }} + run: | + set -euo pipefail + + # Only the first line is used. Examples: + # v1.0.0 -> stable release + # v1.0.0-beta -> beta prerelease + # v1.0.0-alpha.1 -> alpha prerelease + # v1.0.0-rc1 -> release-candidate prerelease + RELEASE_INPUT="${COMMIT_TITLE%%$'\n'*}" + RELEASE_INPUT="${RELEASE_INPUT#v}" + RELEASE_INPUT="${RELEASE_INPUT// /-}" + RELEASE_INPUT="${RELEASE_INPUT,,}" + + if [[ "$RELEASE_INPUT" =~ ^([0-9]+(\.[0-9]+)*)(-(stable|beta|alpha|rc)([.-]?[0-9]+)?)?$ ]]; then + BASE_VERSION="${BASH_REMATCH[1]}" + CHANNEL="${BASH_REMATCH[4]:-stable}" + CHANNEL_SUFFIX="${BASH_REMATCH[5]:-}" + else + echo "Invalid release commit format: v${RELEASE_INPUT}" >&2 + echo "Expected examples: v1.0.0, v1.0.0-beta, v1.0.0-alpha.1, or v1.0.0-rc1" >&2 + exit 1 + fi + + if [[ "$CHANNEL" == "stable" && -z "$CHANNEL_SUFFIX" ]]; then + VERSION="$BASE_VERSION" + PRERELEASE=false + elif [[ "$CHANNEL" == "stable" ]]; then + VERSION="${BASE_VERSION}-stable${CHANNEL_SUFFIX}" + PRERELEASE=false + else + VERSION="${BASE_VERSION}-${CHANNEL}${CHANNEL_SUFFIX}" + PRERELEASE=true + fi + + TAG="v${VERSION}" + RELEASE_NAME="DedicatedPower ${VERSION}" + + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + echo "tag=${TAG}" >> "$GITHUB_OUTPUT" + echo "name=${RELEASE_NAME}" >> "$GITHUB_OUTPUT" + echo "channel=${CHANNEL}" >> "$GITHUB_OUTPUT" + echo "prerelease=${PRERELEASE}" >> "$GITHUB_OUTPUT" + + echo "Version: ${VERSION}" + echo "Channel: ${CHANNEL}" + echo "Prerelease: ${PRERELEASE}" + echo "Tag: ${TAG}" + + - name: Check for an existing release tag + env: + RELEASE_TAG: ${{ steps.release.outputs.tag }} + run: | + set -euo pipefail + + if git ls-remote --exit-code --refs origin "refs/tags/${RELEASE_TAG}" >/dev/null 2>&1; then + echo "A release tag named ${RELEASE_TAG} already exists. Use a new version in the commit message." >&2 + exit 1 + fi + + - name: Build mod + run: ./gradlew clean build -Pmod_version="${{ steps.release.outputs.version }}" + + - name: Select release JAR + id: artifact + shell: bash + run: | + set -euo pipefail + + mapfile -t JARS < <(find build/libs -maxdepth 1 -type f -name '*.jar' \ + ! -name '*-sources.jar' \ + ! -name '*-dev.jar' \ + -print | sort) + + if [[ "${#JARS[@]}" -ne 1 ]]; then + echo "Expected exactly one release JAR, found ${#JARS[@]}" >&2 + printf '%s\n' "${JARS[@]}" + ls -la build/libs + exit 1 + fi + + JAR="${JARS[0]}" + echo "path=${JAR}" >> "$GITHUB_OUTPUT" + echo "Selected artifact: ${JAR}" + + - name: Create GitHub release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.release.outputs.tag }} + name: ${{ steps.release.outputs.name }} + target_commitish: ${{ github.sha }} + prerelease: ${{ steps.release.outputs.prerelease }} + generate_release_notes: true + fail_on_unmatched_files: true + body: | + ## DedicatedPower ${{ steps.release.outputs.version }} + + **Release channel:** `${{ steps.release.outputs.channel }}` + **Minecraft target:** See the Minecraft version declared in the project metadata. + + This release was built automatically from commit `${{ github.sha }}`. + files: ${{ steps.artifact.outputs.path }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 6176b74..8dadf7d 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -5,7 +5,8 @@ "name": "DedicatedPower", "description": "Gives the dedicated server interface a new look for easier server management and more power over your server.", "authors": [ - "SuperSirvu" + "SuperSirvu", + "PiBOH" ], "contact": { "homepage": "https://fabricmc.net/", From d3417b0f0320560b5316b1b5f5557db3a5754ee2 Mon Sep 17 00:00:00 2001 From: PiBOH Date: Thu, 6 Aug 2026 19:34:32 +0200 Subject: [PATCH 02/52] v26.2-1.0.0-beta --- .github/ISSUE_TEMPLATE/bug_report-en.yml | 4 +- .github/workflows/auto-release.yml | 26 +- .github/workflows/build.yml | 2 +- build.gradle | 14 +- gradle.properties | 11 +- gradle/wrapper/gradle-wrapper.properties | 2 +- .../net/supersirvu/gui/EnhancedLogPanel.java | 675 +------- .../supersirvu/gui/EnhancedPlayerListGui.java | 607 +------ .../gui/EnhancedPlayerStatsGui.java | 322 +--- .../supersirvu/gui/EnhancedServerMenuBar.java | 1466 +---------------- .../net/supersirvu/mixin/ServerGuiFixes.java | 145 +- .../resources/dedicatedpower.accesswidener | 5 +- src/main/resources/dedicatedpower.mixins.json | 9 +- src/main/resources/fabric.mod.json | 6 +- 14 files changed, 273 insertions(+), 3021 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report-en.yml b/.github/ISSUE_TEMPLATE/bug_report-en.yml index e8602f8..e04cf76 100644 --- a/.github/ISSUE_TEMPLATE/bug_report-en.yml +++ b/.github/ISSUE_TEMPLATE/bug_report-en.yml @@ -107,7 +107,7 @@ body: attributes: label: DedicatedPower version or commit description: Include the release version, build artifact version, or Git commit you are using. - placeholder: "1.21.11-1.0 or abc1234" + placeholder: "26.2-1.0.0-beta or abc1234" validations: required: true @@ -115,7 +115,7 @@ body: id: minecraft-version attributes: label: Minecraft version - description: Include the exact version, for example 26.2 or 1.21.11. + description: Include the exact version, for example 26.2. placeholder: "26.2" validations: required: true diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 36aa144..806f636 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -23,10 +23,10 @@ jobs: - name: Validate Gradle wrapper uses: gradle/actions/wrapper-validation@v4 - - name: Set up JDK 21 + - name: Set up JDK 25 uses: actions/setup-java@v4 with: - java-version: '21' + java-version: '25' distribution: microsoft - name: Make Gradle wrapper executable @@ -41,22 +41,28 @@ jobs: set -euo pipefail # Only the first line is used. Examples: - # v1.0.0 -> stable release - # v1.0.0-beta -> beta prerelease - # v1.0.0-alpha.1 -> alpha prerelease - # v1.0.0-rc1 -> release-candidate prerelease + # v26.2-1.0.0 -> stable release + # v26.2-1.0.0-beta -> beta prerelease + # v26.2-1.0.0-alpha.1 -> alpha prerelease + # v26.2-1.0.0-rc1 -> release-candidate prerelease RELEASE_INPUT="${COMMIT_TITLE%%$'\n'*}" RELEASE_INPUT="${RELEASE_INPUT#v}" RELEASE_INPUT="${RELEASE_INPUT// /-}" RELEASE_INPUT="${RELEASE_INPUT,,}" - if [[ "$RELEASE_INPUT" =~ ^([0-9]+(\.[0-9]+)*)(-(stable|beta|alpha|rc)([.-]?[0-9]+)?)?$ ]]; then + if [[ "$RELEASE_INPUT" =~ ^(.+)-(stable|beta|alpha|rc)([.-]?[0-9]+)?$ ]]; then BASE_VERSION="${BASH_REMATCH[1]}" - CHANNEL="${BASH_REMATCH[4]:-stable}" - CHANNEL_SUFFIX="${BASH_REMATCH[5]:-}" + CHANNEL="${BASH_REMATCH[2]}" + CHANNEL_SUFFIX="${BASH_REMATCH[3]:-}" else + BASE_VERSION="$RELEASE_INPUT" + CHANNEL="stable" + CHANNEL_SUFFIX="" + fi + + if [[ ! "$BASE_VERSION" =~ ^[0-9]+([.][0-9]+)*(-[0-9]+([.][0-9]+)*)?$ ]]; then echo "Invalid release commit format: v${RELEASE_INPUT}" >&2 - echo "Expected examples: v1.0.0, v1.0.0-beta, v1.0.0-alpha.1, or v1.0.0-rc1" >&2 + echo "Expected examples: v26.2-1.0.0, v26.2-1.0.0-beta, v26.2-1.0.0-alpha.1, or v26.2-1.0.0-rc1" >&2 exit 1 fi diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8fc3827..2606ecd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: - name: setup jdk uses: actions/setup-java@v4 with: - java-version: '21' + java-version: '25' distribution: 'microsoft' - name: make gradle wrapper executable run: chmod +x ./gradlew diff --git a/build.gradle b/build.gradle index 6eb6b41..f13899a 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ plugins { - id 'net.fabricmc.fabric-loom-remap' version "${loom_version}" + id 'net.fabricmc.fabric-loom' version "${loom_version}" id 'maven-publish' } @@ -12,18 +12,16 @@ base { repositories { mavenCentral() - maven { url 'https://maven.fabricmc.net/' } maven { url 'https://maven.terraformersmc.com/' } } dependencies { // To change the versions see the gradle.properties file minecraft "com.mojang:minecraft:${project.minecraft_version}" - mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" - modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + implementation "net.fabricmc:fabric-loader:${project.loader_version}" // Fabric API. This is technically optional, but you probably want it anyway. - modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" + implementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_api_version}" } @@ -40,7 +38,7 @@ processResources { } tasks.withType(JavaCompile).configureEach { - it.options.release = 21 + it.options.release = 25 } java { @@ -49,8 +47,8 @@ java { // If you remove this line, sources will not be generated. withSourcesJar() - sourceCompatibility = JavaVersion.VERSION_21 - targetCompatibility = JavaVersion.VERSION_21 + sourceCompatibility = JavaVersion.VERSION_25 + targetCompatibility = JavaVersion.VERSION_25 } jar { diff --git a/gradle.properties b/gradle.properties index 100e925..599fdec 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,15 +7,14 @@ org.gradle.configuration-cache=false # Fabric Properties # check these on https://fabricmc.net/develop -minecraft_version=1.21.11 -yarn_mappings=1.21.11+build.4 -loader_version=0.18.4 -loom_version=1.14-SNAPSHOT +minecraft_version=26.2 +loader_version=0.19.3 +loom_version=1.17-SNAPSHOT # Mod Properties -mod_version=1.21.11-1.0 +mod_version=26.2-1.0.0-beta maven_group=net.supersirvu archives_base_name=dedicatedpower # Dependencies -fabric_api_version=0.141.1+1.21.11 \ No newline at end of file +fabric_api_version=0.156.0+26.2 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 23449a2..5dd3c01 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/src/main/java/net/supersirvu/gui/EnhancedLogPanel.java b/src/main/java/net/supersirvu/gui/EnhancedLogPanel.java index 4639d88..1c5b4b6 100644 --- a/src/main/java/net/supersirvu/gui/EnhancedLogPanel.java +++ b/src/main/java/net/supersirvu/gui/EnhancedLogPanel.java @@ -6,656 +6,100 @@ package net.supersirvu.gui; -import com.mojang.brigadier.CommandDispatcher; -import com.mojang.brigadier.ParseResults; -import com.mojang.brigadier.StringReader; -import com.mojang.brigadier.suggestion.Suggestion; -import com.mojang.brigadier.suggestion.Suggestions; -import net.minecraft.server.command.ServerCommandSource; -import net.minecraft.server.dedicated.MinecraftDedicatedServer; - -import javax.swing.*; -import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; -import javax.swing.text.BadLocationException; -import javax.swing.text.Style; -import javax.swing.text.StyleConstants; -import javax.swing.text.StyledDocument; -import java.awt.*; +import net.minecraft.server.dedicated.DedicatedServer; + +import javax.swing.BorderFactory; +import javax.swing.JButton; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.SwingUtilities; +import java.awt.BorderLayout; +import java.awt.Font; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.io.FileWriter; -import java.io.IOException; -import java.text.SimpleDateFormat; -import java.util.*; +import java.util.ArrayList; import java.util.List; -import java.util.concurrent.CompletableFuture; public class EnhancedLogPanel extends JPanel { - private final MinecraftDedicatedServer server; - - // Components - private final JTextPane logTextPane; + private final DedicatedServer server; + private final JTextArea logArea; private final JTextField commandInput; - private final JScrollPane logScrollPane; - private final StyledDocument logDocument; - - // Command suggestion - private JWindow suggestionWindow; - private DefaultListModel suggestionModel; - private JList suggestionList; - private CompletableFuture pendingSuggestions; - private Suggestions currentSuggestions; - private CommandDocumentListener documentListener; - - // Command history private final List commandHistory = new ArrayList<>(); private int historyIndex = -1; - // Filtering and display - private final Set enabledLevels = new HashSet<>(Arrays.asList(LogLevel.values())); - private String searchQuery = ""; - - // Store all log entries for filtering - private final List allLogEntries = new ArrayList<>(); - - // Styles - private final Map logStyles = new HashMap<>(); - private Style commandStyle; - private Style argumentStyle; - private Style errorStyle; - - // Control panel - private JPanel controlPanel; - private JTextField searchField; - - public EnhancedLogPanel(MinecraftDedicatedServer server) { + public EnhancedLogPanel(DedicatedServer server) { this.server = server; + setLayout(new BorderLayout(6, 6)); + setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); - setLayout(new BorderLayout()); - setBackground(new Color(240, 240, 240)); - - // Create log display FIRST (before initializing styles) - logTextPane = new JTextPane(); - logTextPane.setEditable(false); - logTextPane.setFont(new Font("Monospaced", Font.PLAIN, 12)); - logTextPane.setBackground(Color.WHITE); - logDocument = logTextPane.getStyledDocument(); - - // NOW initialize styles (after logTextPane is created) - initializeStyles(); - - logScrollPane = new JScrollPane(logTextPane); - logScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS); + logArea = new JTextArea(); + logArea.setEditable(false); + logArea.setLineWrap(true); + logArea.setWrapStyleWord(true); + logArea.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); - // Create command input commandInput = new JTextField(); - commandInput.setFont(new Font("Monospaced", Font.PLAIN, 12)); - commandInput.addActionListener(e -> executeCommand()); - commandInput.addKeyListener(new CommandInputKeyListener()); - - // Create and store document listener reference - documentListener = new CommandDocumentListener(); - commandInput.getDocument().addDocumentListener(documentListener); - - // Disable TAB focus traversal so TAB can be used for completion - commandInput.setFocusTraversalKeysEnabled(false); - - // Create control panel - createControlPanel(); - - // Layout - add(controlPanel, BorderLayout.NORTH); - add(logScrollPane, BorderLayout.CENTER); - add(commandInput, BorderLayout.SOUTH); - - // Initialize suggestion window - initializeSuggestionWindow(); - } - - public void processLogMessage(String message) { - // Parse log level from message - LogLevel level = detectLogLevel(message); - appendLog(message, level); - } - - private LogLevel detectLogLevel(String message) { - if (message.contains("[ERROR]") || message.contains("ERROR:")) { - return LogLevel.ERROR; - } else if (message.contains("[WARN]") || message.contains("WARN:")) { - return LogLevel.WARN; - } else if (message.contains("[DEBUG]") || message.contains("DEBUG:")) { - return LogLevel.DEBUG; - } else if (message.contains("<") && message.contains(">")) { - // Chat messages typically have format - return LogLevel.CHAT; - } else { - return LogLevel.INFO; - } - } - - private void initializeStyles() { - // Log level styles - logStyles.put(LogLevel.INFO, createStyle(new Color(52, 152, 219), false)); - logStyles.put(LogLevel.WARN, createStyle(new Color(243, 156, 18), false)); - logStyles.put(LogLevel.ERROR, createStyle(new Color(231, 76, 60), true)); - logStyles.put(LogLevel.DEBUG, createStyle(new Color(149, 165, 166), false)); - logStyles.put(LogLevel.CHAT, createStyle(new Color(46, 204, 113), false)); - - // Command syntax styles - commandStyle = createStyle(new Color(52, 152, 219), true); - argumentStyle = createStyle(new Color(155, 89, 182), false); - errorStyle = createStyle(new Color(231, 76, 60), false); - } - - private Style createStyle(Color color, boolean bold) { - Style style = logTextPane.addStyle(null, null); - StyleConstants.setForeground(style, color); - StyleConstants.setBold(style, bold); - return style; - } - - private void createControlPanel() { - controlPanel = new JPanel(new GridBagLayout()); - controlPanel.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8)); - controlPanel.setBackground(new Color(250, 250, 250)); - - GridBagConstraints gbc = new GridBagConstraints(); - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.insets = new Insets(0, 5, 0, 5); - gbc.gridy = 0; - - // Search label - gbc.gridx = 0; - gbc.weightx = 0; - controlPanel.add(new JLabel("Search:"), gbc); - - // Search field - expands to fill available space - gbc.gridx = 1; - gbc.weightx = 1.0; - searchField = new JTextField(); - searchField.setMinimumSize(new Dimension(100, 24)); - searchField.setPreferredSize(new Dimension(250, 24)); - searchField.getDocument().addDocumentListener(new javax.swing.event.DocumentListener() { - public void insertUpdate(javax.swing.event.DocumentEvent e) { updateSearch(); } - public void removeUpdate(javax.swing.event.DocumentEvent e) { updateSearch(); } - public void changedUpdate(javax.swing.event.DocumentEvent e) { updateSearch(); } - - private void updateSearch() { - searchQuery = searchField.getText().toLowerCase(); - refreshLogs(); + commandInput.addActionListener(event -> executeCommand()); + commandInput.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent event) { + if (event.getKeyCode() == KeyEvent.VK_UP) { + navigateHistory(-1); + event.consume(); + } else if (event.getKeyCode() == KeyEvent.VK_DOWN) { + navigateHistory(1); + event.consume(); + } } }); - controlPanel.add(searchField, gbc); - - // Filter button - gbc.gridx = 2; - gbc.weightx = 0; - JButton filterButton = createFilterIconButton(); - filterButton.addActionListener(e -> showFilterDialog()); - controlPanel.add(filterButton, gbc); - // Spacer to push right buttons to the right - gbc.gridx = 3; - gbc.weightx = 0.1; - controlPanel.add(Box.createHorizontalGlue(), gbc); + JButton clearButton = new JButton("Clear"); + clearButton.addActionListener(event -> logArea.setText("")); - // Clear button - gbc.gridx = 4; - gbc.weightx = 0; - JButton clearButton = createClearIconButton(); - clearButton.addActionListener(e -> clearLogs()); - controlPanel.add(clearButton, gbc); + JPanel commandBar = new JPanel(new BorderLayout(6, 0)); + commandBar.add(commandInput, BorderLayout.CENTER); + commandBar.add(clearButton, BorderLayout.EAST); - // Export button - gbc.gridx = 5; - gbc.weightx = 0; - JButton exportButton = createExportIconButton(); - exportButton.addActionListener(e -> exportLogs()); - controlPanel.add(exportButton, gbc); + add(new JScrollPane(logArea), BorderLayout.CENTER); + add(commandBar, BorderLayout.SOUTH); } - private JButton createFilterIconButton() { - JButton button = new JButton("≡"); - button.setFont(new Font("Dialog", Font.BOLD, 14)); - button.setToolTipText("Filters & Mode"); - button.setFocusPainted(false); - button.setMargin(new Insets(2, 2, 2, 2)); - button.setPreferredSize(new Dimension(24, 24)); - button.setMinimumSize(new Dimension(24, 24)); - button.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1)); - return button; - } - - private JButton createClearIconButton() { - JButton button = new JButton("✕"); - button.setFont(new Font("Dialog", Font.BOLD, 14)); - button.setToolTipText("Clear Logs"); - button.setFocusPainted(false); - button.setMargin(new Insets(2, 2, 2, 2)); - button.setPreferredSize(new Dimension(24, 24)); - button.setMinimumSize(new Dimension(24, 24)); - button.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1)); - return button; - } - - private JButton createExportIconButton() { - JButton button = new JButton("↓"); - button.setFont(new Font("Dialog", Font.BOLD, 16)); - button.setToolTipText("Export Logs"); - button.setFocusPainted(false); - button.setMargin(new Insets(2, 2, 2, 2)); - button.setPreferredSize(new Dimension(24, 24)); - button.setMinimumSize(new Dimension(24, 24)); - button.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1)); - return button; - } - - private void initializeSuggestionWindow() { - suggestionWindow = new JWindow(); - suggestionWindow.setFocusable(false); - - suggestionModel = new DefaultListModel<>(); - suggestionList = new JList<>(suggestionModel); - suggestionList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - suggestionList.setBackground(Color.WHITE); - suggestionList.setBorder(BorderFactory.createLineBorder(Color.GRAY)); - suggestionList.setFont(new Font("Monospaced", Font.PLAIN, 11)); - - suggestionList.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - if (e.getClickCount() == 2) { - applySuggestion(); - } - } - }); - - JScrollPane scrollPane = new JScrollPane(suggestionList); - scrollPane.setPreferredSize(new Dimension(300, 150)); - suggestionWindow.add(scrollPane); - suggestionWindow.pack(); + public void processLogMessage(String message) { + appendLog(message, LogLevel.INFO); } public void appendLog(String message, LogLevel level) { - // Store the log entry - LogEntry entry = new LogEntry(message, level); - allLogEntries.add(entry); - - // Display if it passes filters SwingUtilities.invokeLater(() -> { - if (shouldDisplayEntry(entry)) { - displayLogEntry(entry); - } + logArea.append("[" + level + "] " + message + System.lineSeparator()); + logArea.setCaretPosition(logArea.getDocument().getLength()); }); } - private boolean shouldDisplayEntry(LogEntry entry) { - // Check log level filter - if (!enabledLevels.contains(entry.level)) return false; - - // Check search filter - if (!searchQuery.isEmpty() && !entry.message.toLowerCase().contains(searchQuery)) return false; - - return true; - } - - private void displayLogEntry(LogEntry entry) { - try { - // Add log level tag - String levelTag = "[" + (entry.level == LogLevel.CHAT ? "CHAT" : "SERVER LOG") + "] "; - logDocument.insertString(logDocument.getLength(), levelTag, logStyles.get(entry.level)); - - // Add message - logDocument.insertString(logDocument.getLength(), entry.message + "\n", logStyles.get(entry.level)); - } catch (BadLocationException e) { - e.printStackTrace(); - } - } - private void executeCommand() { String command = commandInput.getText().trim(); - if (command.isEmpty()) return; + if (command.isEmpty()) { + return; + } - // Add to history commandHistory.add(command); historyIndex = commandHistory.size(); - - // Log the command - appendLog("> " + command, LogLevel.INFO); - - // Execute command - server.enqueueCommand(command, server.getCommandSource()); - - // Clear input + appendLog("> " + command, LogLevel.COMMAND); + server.handleConsoleInput(command, server.createCommandSourceStack()); commandInput.setText(""); - hideSuggestions(); } - private void updateSuggestions() { - String text = commandInput.getText(); - if (text.isEmpty()) { - hideSuggestions(); + private void navigateHistory(int direction) { + if (commandHistory.isEmpty()) { return; } - - // Validate command syntax - validateCommand(text); - - try { - // Get command dispatcher - CommandDispatcher dispatcher = server.getCommandManager().getDispatcher(); - - // Parse and get suggestions - with bounds checking - StringReader reader = new StringReader(text); - ParseResults parse = dispatcher.parse(reader, server.getCommandSource()); - - // Ensure cursor position is within bounds - int cursorPos = Math.min(commandInput.getCaretPosition(), text.length()); - - pendingSuggestions = dispatcher.getCompletionSuggestions(parse, cursorPos); - pendingSuggestions.thenAccept(suggestions -> { - SwingUtilities.invokeLater(() -> { - suggestionModel.clear(); - currentSuggestions = suggestions; - - if (!suggestions.isEmpty()) { - for (Suggestion suggestion : suggestions.getList()) { - suggestionModel.addElement(new SuggestionItem(suggestion)); - } - - if (suggestionModel.size() > 0) { - showSuggestions(); - } else { - hideSuggestions(); - } - } else { - hideSuggestions(); - } - }); - }).exceptionally(throwable -> { - // Silently handle suggestion errors - SwingUtilities.invokeLater(() -> hideSuggestions()); - return null; - }); - } catch (Exception e) { - // If any error occurs, just hide suggestions - hideSuggestions(); - } - } - - private void validateCommand(String command) { - // Simple validation - check if command exists - CommandDispatcher dispatcher = server.getCommandManager().getDispatcher(); - StringReader reader = new StringReader(command); - ParseResults parse = dispatcher.parse(reader, server.getCommandSource()); - - // Highlight syntax in command input - try { - StyledDocument doc = (StyledDocument) commandInput.getDocument(); - Style defaultStyle = doc.addStyle("default", null); - StyleConstants.setForeground(defaultStyle, Color.BLACK); - - if (!parse.getExceptions().isEmpty()) { - // Show error - commandInput.setForeground(Color.RED); - } else { - commandInput.setForeground(Color.BLACK); - } - } catch (Exception e) { - // Ignore styling errors - } - } - - private void showSuggestions() { - Point location = commandInput.getLocationOnScreen(); - suggestionWindow.setLocation(location.x, location.y - suggestionWindow.getHeight()); - suggestionWindow.setVisible(true); - suggestionList.setSelectedIndex(0); - } - - private void hideSuggestions() { - suggestionWindow.setVisible(false); - } - - private void applySuggestion() { - SuggestionItem selected = suggestionList.getSelectedValue(); - if (selected != null && currentSuggestions != null) { - Suggestion suggestion = selected.suggestion; - - // Get the current text - String currentText = commandInput.getText(); - - // Get the range to replace from the suggestion - int start = suggestion.getRange().getStart(); - int end = suggestion.getRange().getEnd(); - - // Ensure bounds are valid - if (start < 0 || end > currentText.length() || start > end) { - hideSuggestions(); - return; - } - - // Build the new command text - String before = currentText.substring(0, start); - String after = currentText.substring(end); - String newText = before + suggestion.getText() + after; - - // CRITICAL: Remove the document listener before modifying text - commandInput.getDocument().removeDocumentListener(documentListener); - - // Set the new text - commandInput.setText(newText); - - // Position cursor at the end of the inserted suggestion - int newCaretPos = start + suggestion.getText().length(); - commandInput.setCaretPosition(newCaretPos); - - // Re-add the document listener after modification is complete - commandInput.getDocument().addDocumentListener(documentListener); - - hideSuggestions(); - commandInput.requestFocus(); - } - } - - private void showFilterDialog() { - JDialog dialog = new JDialog((Frame) SwingUtilities.getWindowAncestor(this), "Log Filters", true); - dialog.setLayout(new BorderLayout(10, 10)); - dialog.setSize(300, 250); - dialog.setLocationRelativeTo(this); - - JPanel checkboxPanel = new JPanel(new GridLayout(0, 1, 5, 5)); - checkboxPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - - Map checkboxes = new HashMap<>(); - for (LogLevel level : LogLevel.values()) { - JCheckBox checkbox = new JCheckBox(level.name(), enabledLevels.contains(level)); - checkboxes.put(level, checkbox); - checkboxPanel.add(checkbox); - } - - JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); - JButton applyButton = new JButton("Apply"); - applyButton.addActionListener(e -> { - enabledLevels.clear(); - for (Map.Entry entry : checkboxes.entrySet()) { - if (entry.getValue().isSelected()) { - enabledLevels.add(entry.getKey()); - } - } - refreshLogs(); - dialog.dispose(); - }); - - JButton cancelButton = new JButton("Cancel"); - cancelButton.addActionListener(e -> dialog.dispose()); - - buttonPanel.add(applyButton); - buttonPanel.add(cancelButton); - - dialog.add(checkboxPanel, BorderLayout.CENTER); - dialog.add(buttonPanel, BorderLayout.SOUTH); - dialog.setVisible(true); - } - - private void clearLogs() { - allLogEntries.clear(); - try { - logDocument.remove(0, logDocument.getLength()); - } catch (BadLocationException e) { - e.printStackTrace(); - } - } - - private void refreshLogs() { - // Clear display - try { - logDocument.remove(0, logDocument.getLength()); - } catch (BadLocationException e) { - e.printStackTrace(); - } - - // Re-display all entries that match current filters - for (LogEntry entry : allLogEntries) { - if (shouldDisplayEntry(entry)) { - displayLogEntry(entry); - } - } - } - - private void exportLogs() { - JFileChooser fileChooser = new JFileChooser(); - fileChooser.setDialogTitle("Export Logs"); - fileChooser.setSelectedFile(new java.io.File("server-logs-" + - new SimpleDateFormat("yyyy-MM-dd-HHmmss").format(new Date()) + ".txt")); - - if (fileChooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) { - try (FileWriter writer = new FileWriter(fileChooser.getSelectedFile())) { - writer.write(logTextPane.getText()); - JOptionPane.showMessageDialog(this, "Logs exported successfully!", - "Export Complete", JOptionPane.INFORMATION_MESSAGE); - } catch (IOException e) { - JOptionPane.showMessageDialog(this, "Failed to export logs: " + e.getMessage(), - "Export Error", JOptionPane.ERROR_MESSAGE); - } - } - } - - private class CommandInputKeyListener extends KeyAdapter { - @Override - public void keyPressed(KeyEvent e) { - switch (e.getKeyCode()) { - case KeyEvent.VK_UP: - if (suggestionWindow.isVisible() && suggestionModel.size() > 0) { - // Navigate suggestions up - int currentIndex = suggestionList.getSelectedIndex(); - if (currentIndex > 0) { - suggestionList.setSelectedIndex(currentIndex - 1); - suggestionList.ensureIndexIsVisible(currentIndex - 1); - } - } else { - // Navigate command history - navigateHistory(-1); - } - e.consume(); - break; - - case KeyEvent.VK_DOWN: - if (suggestionWindow.isVisible() && suggestionModel.size() > 0) { - // Navigate suggestions down - int currentIndex = suggestionList.getSelectedIndex(); - if (currentIndex < suggestionModel.size() - 1) { - suggestionList.setSelectedIndex(currentIndex + 1); - suggestionList.ensureIndexIsVisible(currentIndex + 1); - } - } else { - // Navigate command history - navigateHistory(1); - } - e.consume(); - break; - - case KeyEvent.VK_TAB: - if (suggestionWindow.isVisible() && suggestionModel.size() > 0) { - // If no selection, select first item - if (suggestionList.getSelectedIndex() < 0) { - suggestionList.setSelectedIndex(0); - } - - // Apply the selected suggestion on TAB - applySuggestion(); - } else if (!commandInput.getText().isEmpty()) { - // No suggestions visible - try to trigger suggestion update - updateSuggestions(); - } - e.consume(); // Always consume TAB to prevent focus change - break; - - case KeyEvent.VK_ENTER: - // ENTER always executes the command, never applies suggestions - if (suggestionWindow.isVisible()) { - // Just hide suggestions, let command execute - hideSuggestions(); - } - // Don't consume - let the ActionListener handle command execution - break; - - case KeyEvent.VK_ESCAPE: - hideSuggestions(); - e.consume(); - break; - } - } - } - - private void navigateHistory(int direction) { - if (commandHistory.isEmpty()) return; - historyIndex = Math.max(0, Math.min(commandHistory.size(), historyIndex + direction)); - - if (historyIndex < commandHistory.size()) { - commandInput.setText(commandHistory.get(historyIndex)); - } else { - commandInput.setText(""); - } - } - - private class CommandDocumentListener implements DocumentListener { - @Override - public void insertUpdate(DocumentEvent e) { - updateSuggestions(); - } - - @Override - public void removeUpdate(DocumentEvent e) { - updateSuggestions(); - } - - @Override - public void changedUpdate(DocumentEvent e) { - updateSuggestions(); - } - } - - // Wrapper class for suggestions - private static class SuggestionItem { - final Suggestion suggestion; - - SuggestionItem(Suggestion suggestion) { - this.suggestion = suggestion; - } - - @Override - public String toString() { - return suggestion.getText(); - } + commandInput.setText(historyIndex < commandHistory.size() ? commandHistory.get(historyIndex) : ""); } public enum LogLevel { - INFO, WARN, ERROR, DEBUG, CHAT + INFO, COMMAND, WARNING, ERROR } public enum ConsoleMode { @@ -673,15 +117,4 @@ public String toString() { return displayName; } } - - // Log entry class to store all log data - private static class LogEntry { - final String message; - final LogLevel level; - - LogEntry(String message, LogLevel level) { - this.message = message; - this.level = level; - } - } -} \ No newline at end of file +} diff --git a/src/main/java/net/supersirvu/gui/EnhancedPlayerListGui.java b/src/main/java/net/supersirvu/gui/EnhancedPlayerListGui.java index aea102b..afa2119 100644 --- a/src/main/java/net/supersirvu/gui/EnhancedPlayerListGui.java +++ b/src/main/java/net/supersirvu/gui/EnhancedPlayerListGui.java @@ -6,582 +6,87 @@ package net.supersirvu.gui; -import com.mojang.authlib.GameProfile; -import com.mojang.authlib.minecraft.MinecraftProfileTexture; -import com.mojang.authlib.minecraft.MinecraftProfileTextures; -import net.minecraft.server.BannedPlayerEntry; -import net.minecraft.server.BannedPlayerList; +import net.minecraft.network.chat.Component; import net.minecraft.server.MinecraftServer; -import net.minecraft.server.PlayerConfigEntry; -import net.minecraft.server.network.ServerPlayerEntity; -import net.minecraft.text.Text; -import net.minecraft.world.GameMode; -import net.supersirvu.DedicatedPower; - -import javax.imageio.ImageIO; -import javax.swing.*; -import javax.swing.Timer; -import java.awt.*; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.image.BufferedImage; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URL; -import java.util.*; -import java.util.List; -import java.util.concurrent.CompletableFuture; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.players.NameAndId; +import net.minecraft.world.level.GameType; + +import javax.swing.BorderFactory; +import javax.swing.DefaultListModel; +import javax.swing.JButton; +import javax.swing.JList; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTextField; +import javax.swing.ListSelectionModel; +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.util.Comparator; +import java.util.Locale; public class EnhancedPlayerListGui extends JPanel { - private static final String[] SKINS = new String[]{ - "textures/entity/player/slim/alex.png", - "textures/entity/player/slim/ari.png", - "textures/entity/player/slim/efe.png", - "textures/entity/player/slim/kai.png", - "textures/entity/player/slim/makena.png", - "textures/entity/player/slim/noor.png", - "textures/entity/player/slim/steve.png", - "textures/entity/player/slim/sunny.png", - "textures/entity/player/slim/zuri.png", - "textures/entity/player/wide/alex.png", - "textures/entity/player/wide/ari.png", - "textures/entity/player/wide/efe.png", - "textures/entity/player/wide/kai.png", - "textures/entity/player/wide/makena.png", - "textures/entity/player/wide/noor.png", - "textures/entity/player/wide/steve.png", - "textures/entity/player/wide/sunny.png", - "textures/entity/player/wide/zuri.png" - }; - private final MinecraftServer server; - private final JList playerList; - private final DefaultListModel listModel; - private final Map headCache; - private int tick; - private SortMode sortMode = SortMode.NAME; - private String searchFilter = ""; + private final DefaultListModel model = new DefaultListModel<>(); + private final JList playerList = new JList<>(model); + private final JTextField filter = new JTextField(); public EnhancedPlayerListGui(MinecraftServer server) { this.server = server; - this.listModel = new DefaultListModel<>(); - this.playerList = new JList<>(listModel); - this.headCache = new HashMap<>(); - - this.setLayout(new BorderLayout()); - this.setBackground(new Color(240, 240, 240)); - - // Setup player list - setupPlayerList(); - - // Create header panel with controls - JPanel headerPanel = createHeaderPanel(); - this.add(headerPanel, BorderLayout.NORTH); - - // Add scroll pane with player list - JScrollPane scrollPane = new JScrollPane(playerList); - scrollPane.setBorder(BorderFactory.createEmptyBorder()); - this.add(scrollPane, BorderLayout.CENTER); - - // Register tick handler - server.addServerGuiTickable(this::tick); - - // Start async head loading - startHeadLoading(); - } - - private void startHeadLoading() { - // Periodically check and load missing player heads - Timer headLoadTimer = new Timer(2000, e -> { - for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) { - String uuid = player.getGameProfile().id().toString(); - if (!headCache.containsKey(uuid)) { - loadPlayerHead(player.getGameProfile()); - } - } - }); - headLoadTimer.start(); - } - - private void loadPlayerHead(GameProfile profile) { - String uuid = profile.id().toString(); - - CompletableFuture.runAsync(() -> { - try { - // Get skin URL from Mojang session server - MinecraftProfileTextures textures = - server.getApiServices().sessionService().getTextures(profile); - - if (textures.skin() != null) { - MinecraftProfileTexture skinTexture = textures.skin(); - String skinUrl = skinTexture.getUrl(); + setLayout(new BorderLayout(6, 6)); + setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4)); - // Download skin - URL url = URI.create(skinUrl).toURL(); - DedicatedPower.LOGGER.info("Loading Skin: " + skinUrl); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setRequestMethod("GET"); - connection.setConnectTimeout(5000); - connection.setReadTimeout(5000); - - try (InputStream in = connection.getInputStream()) { - BufferedImage skin = ImageIO.read(in); - - // Extract head from skin (8x8 face + 8x8 overlay) - BufferedImage head = extractHead(skin); - headCache.put(uuid, head); - - // Trigger repaint - SwingUtilities.invokeLater(() -> playerList.repaint()); - } - } else { - // Use default Steve head if no skin - headCache.put(uuid, createDefaultHead(uuid)); - } - } catch (Exception e) { - // On error, use default head - headCache.put(uuid, createDefaultHead(uuid)); - } - }); - } - - private BufferedImage extractHead(BufferedImage skin) { - // Create 32x32 head with overlay - BufferedImage head = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB); - Graphics2D g = head.createGraphics(); - - // Draw base face (from coordinates 8,8 size 8x8 in the skin) - g.drawImage(skin, 0, 0, 32, 32, 8, 8, 16, 16, null); - - // Draw overlay/hat layer (from coordinates 40,8 size 8x8 in the skin) - g.drawImage(skin, 0, 0, 32, 32, 40, 8, 48, 16, null); - - g.dispose(); - return head; - } - - private BufferedImage createDefaultHead(String profile) { - try { - // Get the default skin texture identifier for this player - String textureId = SKINS[Math.floorMod(profile.hashCode(), SKINS.length)]; - - // Load from Minecraft's resource pack/jar - InputStream stream = getClass().getClassLoader().getResourceAsStream("assets/minecraft/" + textureId); - if (stream != null) { - BufferedImage skin = ImageIO.read(stream); - stream.close(); - return extractHead(skin); - } - } catch (Exception e) { - e.printStackTrace(); - } + filter.setToolTipText("Filter players"); + filter.addActionListener(event -> refresh()); - // Fallback if loading fails - return createFallbackHead(); - } + JButton refresh = new JButton("Refresh"); + refresh.addActionListener(event -> refresh()); - private BufferedImage createFallbackHead() { - BufferedImage head = new BufferedImage(32, 32, BufferedImage.TYPE_INT_ARGB); - Graphics2D g = head.createGraphics(); - g.setColor(new Color(100, 70, 50)); - g.fillRect(0, 0, 32, 10); - g.setColor(new Color(198, 146, 103)); - g.fillRect(0, 10, 32, 22); - g.setColor(new Color(80, 60, 40)); - g.fillRect(8, 14, 6, 3); - g.fillRect(18, 14, 6, 3); - g.fillRect(10, 24, 12, 2); - g.dispose(); - return head; - } + JPanel header = new JPanel(new BorderLayout(6, 0)); + header.add(filter, BorderLayout.CENTER); + header.add(refresh, BorderLayout.EAST); - private void setupPlayerList() { - playerList.setCellRenderer(new PlayerCellRenderer()); playerList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - playerList.setBackground(Color.WHITE); - playerList.setFixedCellHeight(40); - - // Add right-click context menu - playerList.addMouseListener(new MouseAdapter() { - @Override - public void mousePressed(MouseEvent e) { - if (SwingUtilities.isRightMouseButton(e)) { - int index = playerList.locationToIndex(e.getPoint()); - if (index >= 0) { - playerList.setSelectedIndex(index); - showContextMenu(e.getX(), e.getY(), listModel.get(index)); - } - } - } - - @Override - public void mouseClicked(MouseEvent e) { - if (e.getClickCount() == 2) { - int index = playerList.locationToIndex(e.getPoint()); - if (index >= 0) { - showPlayerDetails(listModel.get(index)); - } - } - } - }); - } - - private JPanel createHeaderPanel() { - JPanel panel = new JPanel(new BorderLayout()); - panel.setBackground(new Color(250, 250, 250)); - panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); - - // Search field - JTextField searchField = new JTextField(); - searchField.setText("Search players..."); - searchField.setForeground(Color.GRAY); - searchField.addFocusListener(new java.awt.event.FocusAdapter() { - public void focusGained(java.awt.event.FocusEvent e) { - if (searchField.getText().equals("Search players...")) { - searchField.setText(""); - searchField.setForeground(Color.BLACK); - } + playerList.setFixedCellHeight(32); + playerList.setCellRenderer((list, row, index, selected, focus) -> { + String text = row.name + " | " + row.gameType.getName(); + if (selected) { + text = "▶ " + text; } - public void focusLost(java.awt.event.FocusEvent e) { - if (searchField.getText().isEmpty()) { - searchField.setText("Search players..."); - searchField.setForeground(Color.GRAY); - } - } - }); - searchField.addActionListener(e -> { - searchFilter = searchField.getText().equals("Search players...") ? "" : searchField.getText(); - updatePlayerList(); + return new javax.swing.JLabel(text); }); - // Sort button - JButton sortButton = new JButton("Sort: Name"); - sortButton.addActionListener(e -> { - sortMode = sortMode.next(); - sortButton.setText("Sort: " + sortMode.getDisplayName()); - updatePlayerList(); - }); - - // Ban list button - JButton banListButton = new JButton("Ban List"); - banListButton.addActionListener(e -> showBanList()); - - JPanel controlPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0)); - controlPanel.setBackground(new Color(250, 250, 250)); - controlPanel.add(sortButton); - controlPanel.add(banListButton); - - panel.add(searchField, BorderLayout.CENTER); - panel.add(controlPanel, BorderLayout.EAST); - - return panel; + add(header, BorderLayout.NORTH); + add(new JScrollPane(playerList), BorderLayout.CENTER); + refresh(); } public void tick() { - if (this.tick++ % 20 == 0) { - updatePlayerList(); - } + refresh(); } - private void updatePlayerList() { - List players = new ArrayList<>(); - - for (ServerPlayerEntity player : server.getPlayerManager().getPlayerList()) { - String name = player.getGameProfile().name(); - - // Apply search filter - if (!searchFilter.isEmpty() && !name.toLowerCase().contains(searchFilter.toLowerCase())) { - continue; - } - - PlayerInfo info = new PlayerInfo( - player.getGameProfile(), - player.getPlayerConfigEntry(), - player.networkHandler.getLatency(), - player.interactionManager.getGameMode(), - server.getPlayerManager().isOperator(player.getPlayerConfigEntry()), - player.getHealth(), - player.getHungerManager().getFoodLevel() - ); - players.add(info); - } - - // Sort players - players.sort(sortMode.getComparator()); - - // Update list model - listModel.clear(); - for (PlayerInfo info : players) { - listModel.addElement(info); - } + private void refresh() { + String query = filter.getText().trim().toLowerCase(Locale.ROOT); + model.clear(); + server.getPlayerList().getPlayers().stream() + .map(PlayerRow::new) + .filter(row -> query.isEmpty() || row.name.toLowerCase(Locale.ROOT).contains(query)) + .sorted(Comparator.comparing(row -> row.name.toLowerCase(Locale.ROOT))) + .forEach(model::addElement); } - private void showContextMenu(int x, int y, PlayerInfo player) { - JPopupMenu menu = new JPopupMenu(); + private static final class PlayerRow { + private final String name; + private final GameType gameType; - JMenuItem kickItem = new JMenuItem("Kick Player"); - kickItem.addActionListener(e -> { - String reason = JOptionPane.showInputDialog(this, "Kick reason:", "Kick Player", JOptionPane.QUESTION_MESSAGE); - if (reason != null) { - ServerPlayerEntity serverPlayer = server.getPlayerManager().getPlayer(player.profile.id()); - serverPlayer.networkHandler.disconnect(Text.literal(reason)); - } - }); - - JMenuItem banItem = new JMenuItem("Ban Player"); - banItem.addActionListener(e -> { - String reason = JOptionPane.showInputDialog(this, "Ban reason:", "Ban Player", JOptionPane.QUESTION_MESSAGE); - if (reason != null) { - BannedPlayerList bannedPlayerList = server.getPlayerManager().getUserBanList(); - if (!bannedPlayerList.contains(player.configEntry)) { - BannedPlayerEntry bannedPlayerEntry = new BannedPlayerEntry(player.configEntry, null, "SERVER", null, reason); - bannedPlayerList.add(bannedPlayerEntry); - ServerPlayerEntity serverPlayerEntity = server.getPlayerManager().getPlayer(player.profile.id()); - if (serverPlayerEntity != null) { - serverPlayerEntity.networkHandler.disconnect(Text.translatable("multiplayer.disconnect.banned")); - } - } - } - }); - - JMenuItem messageItem = new JMenuItem("Send Message"); - messageItem.addActionListener(e -> { - String message = JOptionPane.showInputDialog(this, "Message:", "Send Message", JOptionPane.QUESTION_MESSAGE); - if (message != null) { - server.getPlayerManager().getPlayer(player.profile.id()).sendMessage(Text.literal("SERVER: " + message)); - } - }); - - JMenuItem opItem = new JMenuItem(player.isOp ? "Deop Player" : "Op Player"); - opItem.addActionListener(e -> { - if(player.isOp) { - server.getPlayerManager().removeFromOperators(player.configEntry); - } else { - server.getPlayerManager().addToOperators(player.configEntry); - } - }); - - menu.add(kickItem); - menu.add(banItem); - menu.add(messageItem); - menu.addSeparator(); - menu.add(opItem); - - menu.show(playerList, x, y); - } - - private void showPlayerDetails(PlayerInfo player) { - String details = String.format( - "Player: %s\n\nPing: %d ms\nGame Mode: %s\nOperator: %s\nHealth: %.1f\nHunger: %d", - player.profile.name(), - player.ping, - player.gameMode.getTranslatableName(), - player.isOp ? "Yes" : "No", - player.health, - player.hunger - ); - - JOptionPane.showMessageDialog(this, details, "Player Details", JOptionPane.INFORMATION_MESSAGE); - } - - private void showBanList() { - JDialog dialog = new JDialog((Frame) SwingUtilities.getWindowAncestor(this), "Ban List", true); - dialog.setLayout(new BorderLayout()); - dialog.setSize(400, 300); - dialog.setLocationRelativeTo(this); - - DefaultListModel banModel = new DefaultListModel<>(); - JList banList = new JList<>(banModel); - - // Get banned players - for (String bannedPlayer : server.getPlayerManager().getUserBanList().getNames()) { - banModel.addElement(bannedPlayer); - } - - JScrollPane scrollPane = new JScrollPane(banList); - - JButton unbanButton = new JButton("Unban Selected"); - unbanButton.addActionListener(e -> { - String selected = banList.getSelectedValue(); - if (selected != null) { - int confirm = JOptionPane.showConfirmDialog(dialog, - "Unban " + selected + "?", - "Confirm Unban", - JOptionPane.YES_NO_OPTION); - if (confirm == JOptionPane.YES_OPTION) { - BannedPlayerList bannedPlayerList = server.getPlayerManager().getUserBanList(); - PlayerConfigEntry player = bannedPlayerList.values().stream().filter(entry -> entry.getKey().name().equals(selected)).findFirst().orElseThrow().getKey(); - if (bannedPlayerList.contains(player)) { - bannedPlayerList.remove(player); - } - banModel.removeElement(selected); - } - } - }); - - JButton closeButton = new JButton("Close"); - closeButton.addActionListener(e -> dialog.dispose()); - - JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); - buttonPanel.add(unbanButton); - buttonPanel.add(closeButton); - - dialog.add(scrollPane, BorderLayout.CENTER); - dialog.add(buttonPanel, BorderLayout.SOUTH); - dialog.setVisible(true); - } - - // Player info data class - private static class PlayerInfo { - final GameProfile profile; - final PlayerConfigEntry configEntry; - final int ping; - final GameMode gameMode; - final boolean isOp; - final float health; - final int hunger; - - PlayerInfo(GameProfile profile, PlayerConfigEntry configEntry, int ping, GameMode gameMode, boolean isOp, float health, int hunger) { - this.profile = profile; - this.configEntry = configEntry; - this.ping = ping; - this.gameMode = gameMode; - this.isOp = isOp; - this.health = health; - this.hunger = hunger; - } - } - - // Custom cell renderer - private class PlayerCellRenderer extends JPanel implements ListCellRenderer { - private final JLabel headLabel; - private final JLabel nameLabel; - private final JLabel infoLabel; - private final JLabel pingLabel; - private final JPanel iconPanel; - - PlayerCellRenderer() { - setLayout(new BorderLayout(10, 0)); - setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 10)); - - // Player head label - headLabel = new JLabel(); - headLabel.setPreferredSize(new Dimension(32, 32)); - headLabel.setBorder(BorderFactory.createLineBorder(Color.GRAY)); - - // Name and info panel - JPanel textPanel = new JPanel(new GridLayout(2, 1, 0, 2)); - textPanel.setOpaque(false); - - nameLabel = new JLabel(); - nameLabel.setFont(new Font("Arial", Font.BOLD, 12)); - - infoLabel = new JLabel(); - infoLabel.setFont(new Font("Arial", Font.PLAIN, 10)); - infoLabel.setForeground(Color.GRAY); - - textPanel.add(nameLabel); - textPanel.add(infoLabel); - - // Icon panel for gamemode and op indicator - iconPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 0)); - iconPanel.setOpaque(false); - - // Ping label - pingLabel = new JLabel(); - pingLabel.setFont(new Font("Arial", Font.PLAIN, 10)); - - JPanel rightPanel = new JPanel(new BorderLayout()); - rightPanel.setOpaque(false); - rightPanel.add(iconPanel, BorderLayout.NORTH); - rightPanel.add(pingLabel, BorderLayout.SOUTH); - - add(headLabel, BorderLayout.WEST); - add(textPanel, BorderLayout.CENTER); - add(rightPanel, BorderLayout.EAST); + private PlayerRow(ServerPlayer player) { + this.name = player.getGameProfile().name(); + this.gameType = player.gameMode(); } @Override - public Component getListCellRendererComponent(JList list, PlayerInfo player, - int index, boolean isSelected, boolean cellHasFocus) { - // Set background - if (isSelected) { - setBackground(new Color(184, 207, 229)); - } else { - setBackground(index % 2 == 0 ? Color.WHITE : new Color(250, 250, 250)); - } - - // Set player head - String uuid = player.profile.id().toString(); - BufferedImage headImage = headCache.get(uuid); - if (headImage != null) { - headLabel.setIcon(new ImageIcon(headImage)); - headLabel.setOpaque(false); - } else { - // Show loading placeholder - headLabel.setIcon(null); - headLabel.setOpaque(true); - headLabel.setBackground(new Color(200, 200, 200)); - } - - // Set name - nameLabel.setText(player.profile.name()); - - // Set info (gamemode) - infoLabel.setText("Mode: " + player.gameMode.getTranslatableName()); - - // Set ping with color coding - pingLabel.setText(player.ping + " ms"); - if (player.ping < 50) { - pingLabel.setForeground(new Color(40, 180, 99)); - } else if (player.ping < 100) { - pingLabel.setForeground(new Color(241, 196, 15)); - } else { - pingLabel.setForeground(new Color(231, 76, 60)); - } - - // Setup icons - iconPanel.removeAll(); - - // Op indicator - if (player.isOp) { - JLabel opLabel = new JLabel("OP"); - opLabel.setForeground(new Color(255, 215, 0)); - opLabel.setFont(new Font("Arial", Font.BOLD, 16)); - opLabel.setToolTipText("Operator"); - iconPanel.add(opLabel); - } - - return this; - } - } - - // Sort modes - private enum SortMode { - NAME("Name", Comparator.comparing(p -> p.profile.name())), - PING("Ping", Comparator.comparingInt(p -> p.ping)), - GAMEMODE("Game Mode", Comparator.comparing(p -> p.gameMode.getTranslatableName().getString())); - - private final String displayName; - private final Comparator comparator; - - SortMode(String displayName, Comparator comparator) { - this.displayName = displayName; - this.comparator = comparator; - } - - String getDisplayName() { - return displayName; - } - - Comparator getComparator() { - return comparator; - } - - SortMode next() { - SortMode[] values = values(); - return values[(ordinal() + 1) % values.length]; + public String toString() { + return name; } } -} \ No newline at end of file +} diff --git a/src/main/java/net/supersirvu/gui/EnhancedPlayerStatsGui.java b/src/main/java/net/supersirvu/gui/EnhancedPlayerStatsGui.java index f0b16fe..e27c92b 100644 --- a/src/main/java/net/supersirvu/gui/EnhancedPlayerStatsGui.java +++ b/src/main/java/net/supersirvu/gui/EnhancedPlayerStatsGui.java @@ -6,306 +6,62 @@ package net.supersirvu.gui; -import net.minecraft.entity.Entity; import net.minecraft.server.MinecraftServer; -import net.minecraft.server.world.ServerWorld; -import net.minecraft.util.TimeHelper; -import net.minecraft.util.Util; -import javax.swing.*; -import java.awt.*; -import java.awt.event.MouseEvent; -import java.awt.event.MouseMotionAdapter; -import java.text.DecimalFormat; -import java.text.DecimalFormatSymbols; +import javax.swing.JComponent; +import javax.swing.Timer; +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.RenderingHints; import java.util.Locale; public class EnhancedPlayerStatsGui extends JComponent { - private static final DecimalFormat AVG_TICK_FORMAT = Util.make( - new DecimalFormat("########0.000"), - decimalFormat -> decimalFormat.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ROOT)) - ); - private static final DecimalFormat NUMBER_FORMAT = new DecimalFormat("#,###"); - - // Graph data - private final int[] memoryUsePercentage = new int[256]; - private final double[] tickTimeHistory = new double[256]; - private int dataPosition; - - // Stats lines - private final String[] lines = new String[11]; private final MinecraftServer server; private final Timer timer; - - // Colors - private static final Color BG_COLOR = new Color(255, 255, 255); - private static final Color TEXT_COLOR = new Color(0, 0, 0); - private static final Color MEMORY_GOOD = new Color(40, 180, 99); - private static final Color MEMORY_WARNING = new Color(241, 196, 15); - private static final Color MEMORY_CRITICAL = new Color(231, 76, 60); - private static final Color TPS_EXCELLENT = new Color(46, 204, 113); - private static final Color TPS_GOOD = new Color(52, 152, 219); - private static final Color TPS_WARNING = new Color(230, 126, 34); - private static final Color TPS_POOR = new Color(192, 57, 43); - - // Tooltip - private int mouseX = -1; - private int mouseY = -1; - - // Uptime tracking - private long startTime; + private String status = "Collecting server statistics..."; public EnhancedPlayerStatsGui(MinecraftServer server) { this.server = server; - this.startTime = System.currentTimeMillis(); - this.setPreferredSize(new Dimension(456, 246)); - this.setMinimumSize(new Dimension(400, 200)); - this.timer = new Timer(500, event -> this.update()); - this.timer.start(); - this.setBackground(BG_COLOR); - - // Add mouse motion listener for tooltips - this.addMouseMotionListener(new MouseMotionAdapter() { - @Override - public void mouseMoved(MouseEvent e) { - mouseX = e.getX(); - mouseY = e.getY(); - repaint(); - } - }); - } - - private void update() { - Runtime runtime = Runtime.getRuntime(); - long usedMemory = runtime.totalMemory() - runtime.freeMemory(); - long maxMemory = runtime.maxMemory(); - long freeMemory = runtime.freeMemory(); - - // Calculate stats - double avgTickMs = (double)this.server.getAverageNanosPerTick() / TimeHelper.MILLI_IN_NANOS; - double tps = Math.min(20.0, 1000.0 / avgTickMs); - - int entityCount = 0; - int chunkCount = 0; - for (ServerWorld world : this.server.getWorlds()) { - for (Entity ignored : world.iterateEntities()) { - entityCount++; - } - chunkCount += world.getChunkManager().getLoadedChunkCount(); - } - - // Update text lines - this.lines[0] = "Memory: " + (usedMemory / 1024L / 1024L) + " MB / " + - (maxMemory / 1024L / 1024L) + " MB (" + - (freeMemory * 100L / maxMemory) + "% free)"; - this.lines[1] = "Avg Tick: " + AVG_TICK_FORMAT.format(avgTickMs) + " ms"; - this.lines[2] = "TPS: " + AVG_TICK_FORMAT.format(tps) + " / 20.0"; - this.lines[3] = "Entities: " + NUMBER_FORMAT.format(entityCount); - this.lines[4] = "Chunks: " + NUMBER_FORMAT.format(chunkCount); - this.lines[5] = "Uptime: " + formatUptime(System.currentTimeMillis() - startTime); - - // Update graph data - this.memoryUsePercentage[this.dataPosition & 0xFF] = (int)(usedMemory * 100L / maxMemory); - this.tickTimeHistory[this.dataPosition & 0xFF] = avgTickMs; - this.dataPosition++; - - this.repaint(); + setPreferredSize(new Dimension(456, 90)); + timer = new Timer(500, event -> updateStats()); + timer.start(); + updateStats(); } - private String formatUptime(long millis) { - long seconds = millis / 1000; - long minutes = seconds / 60; - long hours = minutes / 60; - long days = hours / 24; - - if (days > 0) { - return days + "d " + (hours % 24) + "h " + (minutes % 60) + "m"; - } else if (hours > 0) { - return hours + "h " + (minutes % 60) + "m " + (seconds % 60) + "s"; - } else if (minutes > 0) { - return minutes + "m " + (seconds % 60) + "s"; - } else { - return seconds + "s"; - } + private void updateStats() { + double tickMs = server.getCurrentSmoothedTickTime() / 1_000_000.0; + double tps = tickMs <= 0.0 ? 20.0 : Math.min(20.0, 1000.0 / tickMs); + status = String.format(Locale.ROOT, + "TPS: %.2f / 20.0 Average tick: %.2f ms Players: %d Levels: %d", + tps, + tickMs, + server.getPlayerCount(), + server.levelKeys().size()); + repaint(); } @Override - public void paint(Graphics g) { - Graphics2D g2d = (Graphics2D) g; - - // Enable anti-aliasing - g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); - - int width = getWidth(); - int height = getHeight(); - - // Background - g2d.setColor(BG_COLOR); - g2d.fillRect(0, 0, width, height); - - // Calculate graph dimensions - int graphHeight = (height - 140) / 2; - int graphY1 = 20; - int graphY2 = graphY1 + graphHeight + 20; - int statsY = graphY2 + graphHeight + 20; - - // Draw memory graph - drawGraph(g2d, "Memory Usage (%)", graphY1, graphHeight, this.memoryUsePercentage, 0, 100, true); - - // Draw TPS/Tick time graph - drawGraph(g2d, "Tick Time (ms)", graphY2, graphHeight, this.tickTimeHistory, 0, 50, false); - - // Draw stats text - drawStats(g2d, statsY); - - // Draw tooltip if mouse is over a graph - if (mouseX >= 0 && mouseY >= 0) { - if (mouseY >= graphY1 && mouseY <= graphY1 + graphHeight) { - drawTooltip(g2d, mouseX, mouseY, true); - } else if (mouseY >= graphY2 && mouseY <= graphY2 + graphHeight) { - drawTooltip(g2d, mouseX, mouseY, false); - } - } - } - - private void drawGraph(Graphics2D g2d, String title, int y, int height, double[] data, double minVal, double maxVal, boolean isMemory) { - int width = getWidth(); - int graphWidth = width - 60; // Made smaller to fit scale labels - int graphX = 10; - - // Draw grid background - g2d.setColor(Color.WHITE); - g2d.fillRect(graphX, y, graphWidth, height); - - // Draw grid lines - g2d.setColor(new Color(220, 220, 220)); - for (int i = 0; i <= 5; i++) { - int gridY = y + (height * i / 5); - g2d.drawLine(graphX, gridY, graphX + graphWidth, gridY); - } - for (int i = 0; i <= 8; i++) { - int gridX = graphX + (graphWidth * i / 8); - g2d.drawLine(gridX, y, gridX, y + height); - } - - // Draw title - g2d.setColor(Color.BLACK); - g2d.setFont(new Font("Arial", Font.BOLD, 11)); - g2d.drawString(title, graphX + 5, y - 2); - - // Draw scale labels - g2d.setFont(new Font("Arial", Font.PLAIN, 9)); - g2d.setColor(Color.BLACK); - g2d.drawString(String.valueOf((int)maxVal), graphX + graphWidth + 5, y + 10); - g2d.drawString(String.valueOf((int)(maxVal / 2)), graphX + graphWidth + 5, y + height / 2 + 5); - g2d.drawString(String.valueOf((int)minVal), graphX + graphWidth + 5, y + height); - - // Draw graph data (from right to left) - for (int i = 0; i < graphWidth && i < 256; i++) { - int dataIndex = (this.dataPosition - 1 - i) & 0xFF; // Start from most recent - double value = data[dataIndex]; - - int barHeight = (int)((value / maxVal) * height); - barHeight = Math.min(barHeight, height); - - Color barColor = isMemory ? getMemoryColor((int)value) : getTickColor(value); - g2d.setColor(barColor); - // Draw from right to left - g2d.fillRect(graphX + graphWidth - 1 - i, y + height - barHeight, 1, barHeight); - } - - // Draw border - g2d.setColor(new Color(100, 100, 100)); - g2d.drawRect(graphX, y, graphWidth, height); - } - - private void drawGraph(Graphics2D g2d, String title, int y, int height, int[] data, double minVal, double maxVal, boolean isMemory) { - // Overload for int arrays (memory percentage) - double[] doubleData = new double[data.length]; - for (int i = 0; i < data.length; i++) { - doubleData[i] = data[i]; - } - drawGraph(g2d, title, y, height, doubleData, minVal, maxVal, isMemory); - } - - private void drawStats(Graphics2D g2d, int y) { - g2d.setColor(TEXT_COLOR); - g2d.setFont(new Font("Monospaced", Font.PLAIN, 11)); - - for (int i = 0; i < this.lines.length; i++) { - String line = this.lines[i]; - if (line != null) { - g2d.drawString(line, 15, y + i * 14); - } - } - } - - private void drawTooltip(Graphics2D g2d, int mx, int my, boolean isMemory) { - int graphWidth = getWidth() - 60; // Match the new graph width - int graphX = 10; - - if (mx < graphX || mx > graphX + graphWidth) return; - - int relativeX = mx - graphX; - // Calculate index from right to left - int dataIndex = (this.dataPosition - 1 - (graphWidth - 1 - relativeX)) & 0xFF; - - String tooltipText; - if (isMemory) { - int memPercent = this.memoryUsePercentage[dataIndex]; - tooltipText = "Memory: " + memPercent + "%"; - } else { - double tickTime = this.tickTimeHistory[dataIndex]; - double tps = Math.min(20.0, 1000.0 / tickTime); - tooltipText = String.format("Tick: %.2f ms | TPS: %.2f", tickTime, tps); - } - - // Tooltip background - FontMetrics fm = g2d.getFontMetrics(); - int tooltipWidth = fm.stringWidth(tooltipText) + 10; - int tooltipHeight = fm.getHeight() + 6; - - int tooltipX = mx + 10; - int tooltipY = my - 25; - - // Keep tooltip in bounds - if (tooltipX + tooltipWidth > getWidth()) { - tooltipX = mx - tooltipWidth - 10; - } - - g2d.setColor(new Color(255, 255, 255, 200)); - g2d.fillRoundRect(tooltipX, tooltipY, tooltipWidth, tooltipHeight, 5, 5); - g2d.setColor(TEXT_COLOR); - g2d.drawRoundRect(tooltipX, tooltipY, tooltipWidth, tooltipHeight, 5, 5); - - g2d.setFont(new Font("Arial", Font.PLAIN, 10)); - g2d.drawString(tooltipText, tooltipX + 5, tooltipY + tooltipHeight - 6); - } - - private Color getMemoryColor(int percentage) { - if (percentage < 60) { - return MEMORY_GOOD; - } else if (percentage < 80) { - return MEMORY_WARNING; - } else { - return MEMORY_CRITICAL; - } - } - - private Color getTickColor(double tickMs) { - if (tickMs < 25) { - return TPS_EXCELLENT; - } else if (tickMs < 40) { - return TPS_GOOD; - } else if (tickMs < 50) { - return TPS_WARNING; - } else { - return TPS_POOR; + protected void paintComponent(Graphics graphics) { + super.paintComponent(graphics); + Graphics2D g = (Graphics2D) graphics.create(); + try { + g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + g.setColor(Color.WHITE); + g.fillRect(0, 0, getWidth(), getHeight()); + g.setColor(Color.DARK_GRAY); + g.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12)); + g.drawString(status, 10, 30); + g.setColor(new Color(52, 152, 219)); + g.fillRect(10, 48, Math.min(getWidth() - 20, Math.max(0, server.getPlayerCount() * 12)), 12); + } finally { + g.dispose(); } } public void stop() { - this.timer.stop(); + timer.stop(); } -} \ No newline at end of file +} diff --git a/src/main/java/net/supersirvu/gui/EnhancedServerMenuBar.java b/src/main/java/net/supersirvu/gui/EnhancedServerMenuBar.java index 143f756..6e404ae 100644 --- a/src/main/java/net/supersirvu/gui/EnhancedServerMenuBar.java +++ b/src/main/java/net/supersirvu/gui/EnhancedServerMenuBar.java @@ -6,1452 +6,64 @@ package net.supersirvu.gui; -import net.minecraft.client.resource.language.I18n; -import net.minecraft.server.dedicated.MinecraftDedicatedServer; -import net.minecraft.server.world.ServerWorld; -import net.minecraft.world.Difficulty; -import net.minecraft.world.GameMode; -import net.minecraft.world.WorldProperties; -import net.minecraft.world.rule.GameRule; -import net.minecraft.world.rule.GameRuleCategory; -import net.minecraft.world.rule.GameRuleVisitor; -import net.minecraft.world.rule.GameRules; +import net.minecraft.server.dedicated.DedicatedServer; -import javax.swing.*; -import java.awt.*; -import java.awt.datatransfer.StringSelection; -import java.io.*; -import java.nio.file.Files; -import java.nio.file.StandardCopyOption; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Map; -import java.util.Properties; -import java.util.zip.ZipEntry; -import java.util.zip.ZipOutputStream; +import javax.swing.JMenu; +import javax.swing.JMenuBar; +import javax.swing.JMenuItem; +import javax.swing.JOptionPane; +import java.awt.Frame; public class EnhancedServerMenuBar extends JMenuBar { - private final MinecraftDedicatedServer server; + private final DedicatedServer server; private final Frame parentFrame; - public EnhancedServerMenuBar(MinecraftDedicatedServer server, Frame parentFrame) { + public EnhancedServerMenuBar(DedicatedServer server, Frame parentFrame) { this.server = server; this.parentFrame = parentFrame; - - SwingUtilities.invokeLater(this::createMenus); + createMenus(); } private void createMenus() { - add(createServerMenu()); - add(createWorldMenu()); - add(createPerformanceMenu()); - add(createToolsMenu()); - add(createHelpMenu()); - } - - // ==================== SERVER MENU ==================== - private JMenu createServerMenu() { JMenu serverMenu = new JMenu("Server"); - serverMenu.setMnemonic('S'); - - JMenuItem propertiesItem = new JMenuItem("Server Properties..."); - propertiesItem.addActionListener(e -> showServerProperties()); - serverMenu.add(propertiesItem); - - JMenuItem whitelistItem = new JMenuItem("Whitelist Settings..."); - whitelistItem.addActionListener(e -> showWhitelistSettings()); - serverMenu.add(whitelistItem); - - // Difficulty submenu - JMenu difficultyMenu = new JMenu("Difficulty"); - ButtonGroup difficultyGroup = new ButtonGroup(); - for (Difficulty difficulty : Difficulty.values()) { - JRadioButtonMenuItem diffItem = new JRadioButtonMenuItem(difficulty.getName()); - diffItem.setSelected(server.getSaveProperties().getDifficulty() == difficulty); - diffItem.addActionListener(e -> setDifficulty(difficulty)); - difficultyGroup.add(diffItem); - difficultyMenu.add(diffItem); - } - serverMenu.add(difficultyMenu); - - // Default Game Mode submenu - JMenu gameModeMenu = new JMenu("Default Game Mode"); - ButtonGroup gameModeGroup = new ButtonGroup(); - for (GameMode mode : GameMode.values()) { - JRadioButtonMenuItem modeItem = new JRadioButtonMenuItem(mode.getTranslatableName().getString()); - modeItem.setSelected(server.getSaveProperties().getGameMode() == mode); - modeItem.addActionListener(e -> setDefaultGameMode(mode)); - gameModeGroup.add(modeItem); - gameModeMenu.add(modeItem); - } - serverMenu.add(gameModeMenu); - + JMenuItem saveItem = new JMenuItem("Save all worlds"); + saveItem.addActionListener(event -> execute("save-all flush")); + JMenuItem reloadItem = new JMenuItem("Reload datapacks"); + reloadItem.addActionListener(event -> execute("reload")); + JMenuItem stopItem = new JMenuItem("Stop server"); + stopItem.addActionListener(event -> execute("stop")); + serverMenu.add(saveItem); + serverMenu.add(reloadItem); serverMenu.addSeparator(); + serverMenu.add(stopItem); - JMenuItem saveAllItem = new JMenuItem("Save All Worlds"); - saveAllItem.setAccelerator(KeyStroke.getKeyStroke("ctrl S")); - saveAllItem.addActionListener(e -> saveAllWorlds()); - serverMenu.add(saveAllItem); - - JMenuItem backupItem = new JMenuItem("Backup Server..."); - backupItem.addActionListener(e -> backupServer()); - serverMenu.add(backupItem); - - return serverMenu; - } - - private void showServerProperties() { - JDialog dialog = new JDialog(parentFrame, "Server Properties", true); - dialog.setLayout(new BorderLayout(10, 10)); - dialog.setSize(600, 500); - dialog.setLocationRelativeTo(parentFrame); - - // Load server.properties - File propertiesFile = new File("server.properties"); - Properties properties = new Properties(); - - try (FileInputStream fis = new FileInputStream(propertiesFile)) { - properties.load(fis); - } catch (IOException e) { - JOptionPane.showMessageDialog(dialog, "Failed to load server.properties: " + e.getMessage(), - "Error", JOptionPane.ERROR_MESSAGE); - return; - } - - // Create table for properties - String[] columnNames = {"Property", "Value"}; - Object[][] data = new Object[properties.size()][2]; - - int i = 0; - for (Map.Entry entry : properties.entrySet()) { - data[i][0] = entry.getKey(); - data[i][1] = entry.getValue(); - i++; - } - - JTable table = new JTable(data, columnNames); - table.setRowHeight(25); - JScrollPane scrollPane = new JScrollPane(table); - - JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); - JButton saveButton = new JButton("Save"); - JButton cancelButton = new JButton("Cancel"); - - saveButton.addActionListener(e -> { - // Update properties from table - for (int row = 0; row < table.getRowCount(); row++) { - String key = (String) table.getValueAt(row, 0); - Object value = table.getValueAt(row, 1); - properties.setProperty(key, value.toString()); - } - - // Save to file - try (FileOutputStream fos = new FileOutputStream(propertiesFile)) { - properties.store(fos, "Minecraft server properties - Modified via GUI"); - JOptionPane.showMessageDialog(dialog, "Properties saved! Restart server for changes to take effect.", - "Success", JOptionPane.INFORMATION_MESSAGE); - dialog.dispose(); - } catch (IOException ex) { - JOptionPane.showMessageDialog(dialog, "Failed to save: " + ex.getMessage(), - "Error", JOptionPane.ERROR_MESSAGE); - } - }); - - cancelButton.addActionListener(e -> dialog.dispose()); - - buttonPanel.add(saveButton); - buttonPanel.add(cancelButton); - - dialog.add(scrollPane, BorderLayout.CENTER); - dialog.add(buttonPanel, BorderLayout.SOUTH); - dialog.setVisible(true); - } - - private void showWhitelistSettings() { - JDialog dialog = new JDialog(parentFrame, "Whitelist Settings", true); - dialog.setLayout(new BorderLayout(10, 10)); - dialog.setSize(400, 400); - dialog.setLocationRelativeTo(parentFrame); - - // Whitelist enabled checkbox - JCheckBox enabledCheckbox = new JCheckBox("Whitelist Enabled", server.getPlayerManager().isWhitelistEnabled()); - JPanel topPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - topPanel.add(enabledCheckbox); - - // Whitelist entries - DefaultListModel listModel = new DefaultListModel<>(); - for (String player : server.getPlayerManager().getWhitelist().getNames()) { - listModel.addElement(player); - } - JList whitelistList = new JList<>(listModel); - JScrollPane scrollPane = new JScrollPane(whitelistList); - - // Buttons - JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - JButton addButton = new JButton("Add Player"); - JButton removeButton = new JButton("Remove"); - JButton closeButton = new JButton("Close"); - - addButton.addActionListener(e -> { - String player = JOptionPane.showInputDialog(dialog, "Enter player name:", "Add to Whitelist", JOptionPane.QUESTION_MESSAGE); - if (player != null && !player.trim().isEmpty()) { - server.getCommandManager().parseAndExecute(server.getCommandSource(), "whitelist add " + player); - listModel.addElement(player); - } - }); - - removeButton.addActionListener(e -> { - String selected = whitelistList.getSelectedValue(); - if (selected != null) { - server.getCommandManager().parseAndExecute(server.getCommandSource(), "whitelist remove " + selected); - listModel.removeElement(selected); - } - }); - - enabledCheckbox.addActionListener(e -> server.getCommandManager().parseAndExecute(server.getCommandSource(), - enabledCheckbox.isSelected() ? "whitelist on" : "whitelist off")); - - closeButton.addActionListener(e -> dialog.dispose()); - - buttonPanel.add(addButton); - buttonPanel.add(removeButton); - buttonPanel.add(closeButton); - - dialog.add(topPanel, BorderLayout.NORTH); - dialog.add(scrollPane, BorderLayout.CENTER); - dialog.add(buttonPanel, BorderLayout.SOUTH); - dialog.setVisible(true); - } - - private void setDifficulty(Difficulty difficulty) { - server.setDifficulty(difficulty, true); - JOptionPane.showMessageDialog(parentFrame, "Difficulty set to " + difficulty.getName(), - "Success", JOptionPane.INFORMATION_MESSAGE); - } - - private void setDefaultGameMode(GameMode mode) { - server.setDefaultGameMode(mode); - JOptionPane.showMessageDialog(parentFrame, "Default game mode set to " + mode.getTranslatableName(), - "Success", JOptionPane.INFORMATION_MESSAGE); - } - - private void saveAllWorlds() { - try { - server.getCommandManager().parseAndExecute(server.getCommandSource(), "save-all"); - JOptionPane.showMessageDialog(parentFrame, "All worlds saved successfully!", - "Success", JOptionPane.INFORMATION_MESSAGE); - } catch (Exception e) { - JOptionPane.showMessageDialog(parentFrame, "Failed to save: " + e.getMessage(), - "Error", JOptionPane.ERROR_MESSAGE); - } - } - - private void backupServer() { - JFileChooser fileChooser = new JFileChooser(); - fileChooser.setDialogTitle("Choose Backup Location"); - fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); - - if (fileChooser.showSaveDialog(parentFrame) == JFileChooser.APPROVE_OPTION) { - File backupDir = fileChooser.getSelectedFile(); - String timestamp = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(new Date()); - File backupFile = new File(backupDir, "server-backup-" + timestamp + ".zip"); - - // Show progress dialog - JDialog progressDialog = new JDialog(parentFrame, "Creating Backup...", false); - JProgressBar progressBar = new JProgressBar(); - progressBar.setIndeterminate(true); - progressDialog.add(progressBar); - progressDialog.setSize(300, 80); - progressDialog.setLocationRelativeTo(parentFrame); - progressDialog.setVisible(true); - - // Backup in separate thread - new Thread(() -> { - try { - // Save all first - server.getCommandManager().parseAndExecute(server.getCommandSource(), "save-all flush"); - Thread.sleep(2000); // Wait for save to complete - - // Create zip - zipDirectory(new File("."), backupFile); - - SwingUtilities.invokeLater(() -> { - progressDialog.dispose(); - JOptionPane.showMessageDialog(parentFrame, "Backup created successfully!\n" + backupFile.getAbsolutePath(), - "Success", JOptionPane.INFORMATION_MESSAGE); - }); - } catch (Exception e) { - SwingUtilities.invokeLater(() -> { - progressDialog.dispose(); - JOptionPane.showMessageDialog(parentFrame, "Backup failed: " + e.getMessage(), - "Error", JOptionPane.ERROR_MESSAGE); - }); - } - }).start(); - } - } - - private void zipDirectory(File sourceDir, File zipFile) throws IOException { - try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile))) { - Files.walk(sourceDir.toPath()) - .filter(path -> !Files.isDirectory(path)) - .filter(path -> !path.toString().contains("backup")) // Don't backup backups - .filter(path -> !path.toString().contains("cache")) - .forEach(path -> { - try { - String zipEntryName = sourceDir.toPath().relativize(path).toString(); - zos.putNextEntry(new ZipEntry(zipEntryName)); - Files.copy(path, zos); - zos.closeEntry(); - } catch (IOException e) { - e.printStackTrace(); - } - }); - } - } - - // ==================== WORLD MENU ==================== - private JMenu createWorldMenu() { JMenu worldMenu = new JMenu("World"); - worldMenu.setMnemonic('W'); - - // World list submenu - JMenu worldListMenu = new JMenu("World List"); - for (ServerWorld world : server.getWorlds()) { - JMenuItem worldItem = new JMenuItem(getWorldName(world)); - worldItem.addActionListener(e -> showWorldInfo(world)); - worldListMenu.add(worldItem); - } - worldMenu.add(worldListMenu); - - worldMenu.addSeparator(); - - JMenuItem borderItem = new JMenuItem("World Border Settings..."); - borderItem.addActionListener(e -> showWorldBorderSettings()); - worldMenu.add(borderItem); + JMenuItem dayItem = new JMenuItem("Set day"); + dayItem.addActionListener(event -> execute("time set day")); + JMenuItem nightItem = new JMenuItem("Set night"); + nightItem.addActionListener(event -> execute("time set night")); + JMenuItem clearWeatherItem = new JMenuItem("Clear weather"); + clearWeatherItem.addActionListener(event -> execute("weather clear")); + worldMenu.add(dayItem); + worldMenu.add(nightItem); + worldMenu.add(clearWeatherItem); - // Time submenu - JMenu timeMenu = new JMenu("Set Time"); - addTimeOption(timeMenu, "Day", 1000); - addTimeOption(timeMenu, "Noon", 6000); - addTimeOption(timeMenu, "Night", 13000); - addTimeOption(timeMenu, "Midnight", 18000); - timeMenu.addSeparator(); - JMenuItem customTimeItem = new JMenuItem("Custom..."); - customTimeItem.addActionListener(e -> setCustomTime()); - timeMenu.add(customTimeItem); - worldMenu.add(timeMenu); - - // Weather submenu - JMenu weatherMenu = new JMenu("Set Weather"); - addWeatherOption(weatherMenu, "Clear", "clear"); - addWeatherOption(weatherMenu, "Rain", "rain"); - addWeatherOption(weatherMenu, "Thunder", "thunder"); - weatherMenu.addSeparator(); - worldMenu.add(weatherMenu); - - JMenuItem gameruleItem = new JMenuItem("Game Rules..."); - gameruleItem.addActionListener(e -> showGameruleSettings()); - worldMenu.add(gameruleItem); - - worldMenu.addSeparator(); - - JMenuItem reloadChunksItem = new JMenuItem("Reload Chunks"); - reloadChunksItem.addActionListener(e -> reloadChunks()); - worldMenu.add(reloadChunksItem); - - JMenuItem forceSaveItem = new JMenuItem("Force Save"); - forceSaveItem.addActionListener(e -> forceSave()); - worldMenu.add(forceSaveItem); - - JMenuItem worldBackupItem = new JMenuItem("World Backup..."); - worldBackupItem.addActionListener(e -> backupWorld()); - worldMenu.add(worldBackupItem); - - worldMenu.addSeparator(); - - JMenuItem worldInfoItem = new JMenuItem("World Info..."); - worldInfoItem.addActionListener(e -> showGeneralWorldInfo()); - worldMenu.add(worldInfoItem); - - return worldMenu; - } - - private void showGameruleSettings() { - // Dynamically get all loaded worlds - java.util.List worlds = new java.util.ArrayList<>(); - for (ServerWorld world : server.getWorlds()) { - worlds.add(world); - } - - if (worlds.isEmpty()) { - JOptionPane.showMessageDialog(parentFrame, "No worlds loaded!", "Error", JOptionPane.ERROR_MESSAGE); - return; - } - - // Create world selection options - String[] worldOptions = new String[worlds.size()]; - for (int i = 0; i < worlds.size(); i++) { - worldOptions[i] = getWorldName(worlds.get(i)); - } - - String selectedWorldName = (String) JOptionPane.showInputDialog( - parentFrame, - "Select world to configure game rules:", - "Game Rules", - JOptionPane.QUESTION_MESSAGE, - null, - worldOptions, - worldOptions[0] - ); - - if (selectedWorldName == null) return; - - // Find the selected world - ServerWorld selectedWorld = null; - for (ServerWorld world : worlds) { - if (getWorldName(world).equals(selectedWorldName)) { - selectedWorld = world; - break; - } - } - - if (selectedWorld == null) return; - - showGameruleDialog(selectedWorld, selectedWorldName); - } - - private void showGameruleDialog(ServerWorld world, String worldName) { - JDialog dialog = new JDialog(parentFrame, "Game Rules - " + worldName, true); - dialog.setLayout(new BorderLayout(10, 10)); - dialog.setSize(700, 600); - dialog.setLocationRelativeTo(parentFrame); - - // Create tabbed pane for categories - JTabbedPane tabbedPane = new JTabbedPane(); - - // Maps to store components by category - Map categoryPanels = new java.util.HashMap<>(); - Map categoryConstraints = new java.util.HashMap<>(); - - // Initialize panels for each category - for (GameRuleCategory category : GameRuleCategory.CATEGORIES) { - JPanel panel = new JPanel(new GridBagLayout()); - GridBagConstraints gbc = new GridBagConstraints(); - gbc.gridx = 0; - gbc.gridy = 0; - gbc.anchor = GridBagConstraints.WEST; - gbc.fill = GridBagConstraints.HORIZONTAL; - gbc.insets = new Insets(5, 10, 5, 10); - gbc.weightx = 1.0; - - categoryPanels.put(category, panel); - categoryConstraints.put(category, gbc); - } - - // Store all gamerule components for later retrieval - Map gameruleComponents = new java.util.HashMap<>(); - - // Dynamically discover and add all gamerules - world.getGameRules().accept(new GameRuleVisitor() { - @Override - public void visitBoolean(GameRule rule) { - GameRuleCategory category = rule.getCategory(); - JPanel panel = categoryPanels.get(category); - GridBagConstraints gbc = categoryConstraints.get(category); - - if (panel != null && gbc != null) { - // Get current value from the world - boolean currentValue = world.getGameRules().getValue(rule); - - // Create checkbox - JCheckBox checkbox = new JCheckBox(formatGameruleName(I18n.translate(rule.getTranslationKey())), currentValue); - checkbox.setName(I18n.translate(rule.getTranslationKey())); - checkbox.setToolTipText("Game rule: " + I18n.translate(rule.getTranslationKey())); - - panel.add(checkbox, gbc); - gbc.gridy++; - - gameruleComponents.put(I18n.translate(rule.getTranslationKey()), checkbox); - } - } - - @Override - public void visitInt(GameRule rule) { - GameRuleCategory category = rule.getCategory(); - JPanel panel = categoryPanels.get(category); - GridBagConstraints gbc = categoryConstraints.get(category); - - if (panel != null && gbc != null) { - // Get current value from the world - int currentValue = world.getGameRules().getValue(rule); - - // Create row panel - JPanel rowPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); - JLabel nameLabel = new JLabel(formatGameruleName(I18n.translate(rule.getTranslationKey())) + ":"); - nameLabel.setPreferredSize(new Dimension(300, 25)); - - // Dynamically extract min/max from the IntegerArgumentType - int min = 0; - int max = 1000000; - - JSpinner spinner = new JSpinner(new SpinnerNumberModel(currentValue, min, max, 1)); - spinner.setName(I18n.translate(rule.getTranslationKey())); - spinner.setPreferredSize(new Dimension(100, 25)); - spinner.setToolTipText("Game rule: " + I18n.translate(rule.getTranslationKey()) + " (Min: " + min + ", Max: " + max + ")"); - - rowPanel.add(nameLabel); - rowPanel.add(spinner); - - panel.add(rowPanel, gbc); - gbc.gridy++; - - gameruleComponents.put(I18n.translate(rule.getTranslationKey()), spinner); - } - } - }); - - // Add all category panels to tabbed pane - for (GameRuleCategory category : GameRuleCategory.CATEGORIES) { - JPanel panel = categoryPanels.get(category); - if (panel != null && panel.getComponentCount() > 0) { - JScrollPane scrollPane = new JScrollPane(panel); - scrollPane.getVerticalScrollBar().setUnitIncrement(16); - tabbedPane.addTab(getCategoryDisplayName(category), scrollPane); - } - } - - // Button panel - JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); - JButton applyButton = new JButton("Apply"); - JButton resetButton = new JButton("Reset to Defaults"); - JButton closeButton = new JButton("Close"); - - applyButton.addActionListener(e -> { - applyGamerules(gameruleComponents); - JOptionPane.showMessageDialog(dialog, "Game rules applied!", "Success", JOptionPane.INFORMATION_MESSAGE); - }); - - resetButton.addActionListener(e -> { - int confirm = JOptionPane.showConfirmDialog(dialog, - "Reset all game rules to default values?", - "Confirm Reset", - JOptionPane.YES_NO_OPTION); - if (confirm == JOptionPane.YES_OPTION) { - resetGamerulesToDefaults(world); - JOptionPane.showMessageDialog(dialog, "Game rules reset to defaults!", "Success", JOptionPane.INFORMATION_MESSAGE); - dialog.dispose(); - // Reopen dialog to show updated values - SwingUtilities.invokeLater(() -> showGameruleDialog(world, worldName)); - } - }); - - closeButton.addActionListener(e -> dialog.dispose()); - - buttonPanel.add(applyButton); - buttonPanel.add(resetButton); - buttonPanel.add(closeButton); - - dialog.add(tabbedPane, BorderLayout.CENTER); - dialog.add(buttonPanel, BorderLayout.SOUTH); - dialog.setVisible(true); - } - - private String formatGameruleName(String name) { - // Convert camelCase to Title Case with spaces - StringBuilder result = new StringBuilder(); - boolean wasLower = false; - - for (int i = 0; i < name.length(); i++) { - char c = name.charAt(i); - - if (Character.isUpperCase(c) && wasLower) { - result.append(' '); - } - - if (i == 0) { - result.append(Character.toUpperCase(c)); - } else { - result.append(c); - } - - wasLower = Character.isLowerCase(c); - } - - return result.toString(); - } - - private String getCategoryDisplayName(GameRuleCategory category) { - return category.getText().getString(); - } - - private void applyGamerules(Map components) { - for (Map.Entry entry : components.entrySet()) { - String ruleName = entry.getKey(); - JComponent component = entry.getValue(); - - try { - if (component instanceof JCheckBox checkbox) { - executeGamerule(ruleName, String.valueOf(checkbox.isSelected())); - } else if (component instanceof JSpinner spinner) { - executeGamerule(ruleName, spinner.getValue().toString()); - } - } catch (Exception e) { - System.err.println("Failed to apply gamerule " + ruleName + ": " + e.getMessage()); - } - } - } - - private void resetGamerulesToDefaults(ServerWorld world) { - // Reset common gamerules to their defaults using GameRules class knowledge - world.getGameRules().accept(new GameRuleVisitor() { - @Override - public void visitBoolean(GameRule rule) { - executeGamerule(I18n.translate(rule.getTranslationKey()), String.valueOf(rule.getDefaultValue())); - } - - @Override - public void visitInt(GameRule rule) { - executeGamerule(I18n.translate(rule.getTranslationKey()), String.valueOf(rule.getDefaultValue())); - } - }); - } - - private void executeGamerule(String rule, String value) { - try { - server.getCommandManager().parseAndExecute( - server.getCommandSource(), - "gamerule " + rule + " " + value - ); - } catch (Exception e) { - System.err.println("Failed to set gamerule " + rule + ": " + e.getMessage()); - } - } - - private String getWorldName(ServerWorld world) { - String dimensionKey = world.getRegistryKey().getValue().toString(); - if (dimensionKey.contains("overworld")) return "The Overworld"; - if (dimensionKey.contains("the_nether")) return "The Nether"; - if (dimensionKey.contains("the_end")) return "The End"; - return dimensionKey; - } - - private void showWorldInfo(ServerWorld world) { - String info = String.format( - """ - World: %s - - Dimension: %s - Loaded Chunks: %d - Entities: %d - Time: %d - Weather: %s - Difficulty: %s""", - getWorldName(world), - world.getRegistryKey().getValue().toString(), - world.getChunkManager().getLoadedChunkCount(), - world.iterateEntities().spliterator().estimateSize(), - world.getTimeOfDay(), - world.isRaining() ? (world.isThundering() ? "Thunder" : "Rain") : "Clear", - world.getDifficulty().getName() - ); - - JOptionPane.showMessageDialog(parentFrame, info, "World Information", JOptionPane.INFORMATION_MESSAGE); - } - - private void showWorldBorderSettings() { - ServerWorld overworld = server.getOverworld(); - if (overworld == null) return; - - JDialog dialog = new JDialog(parentFrame, "World Border Settings", true); - dialog.setLayout(new GridLayout(5, 2, 10, 10)); - dialog.setSize(400, 250); - dialog.setLocationRelativeTo(parentFrame); - - dialog.add(new JLabel("Center X:")); - JTextField centerXField = new JTextField(String.valueOf(overworld.getWorldBorder().getCenterX())); - dialog.add(centerXField); - - dialog.add(new JLabel("Center Z:")); - JTextField centerZField = new JTextField(String.valueOf(overworld.getWorldBorder().getCenterZ())); - dialog.add(centerZField); - - dialog.add(new JLabel("Size:")); - JTextField sizeField = new JTextField(String.valueOf(overworld.getWorldBorder().getSize())); - dialog.add(sizeField); - - dialog.add(new JLabel("Damage Per Block:")); - JTextField damageField = new JTextField(String.valueOf(overworld.getWorldBorder().getDamagePerBlock())); - dialog.add(damageField); - - JButton applyButton = new JButton("Apply"); - JButton cancelButton = new JButton("Cancel"); - - applyButton.addActionListener(e -> { - try { - double centerX = Double.parseDouble(centerXField.getText()); - double centerZ = Double.parseDouble(centerZField.getText()); - double size = Double.parseDouble(sizeField.getText()); - - server.getCommandManager().parseAndExecute(server.getCommandSource(), - String.format("worldborder center %f %f", centerX, centerZ)); - server.getCommandManager().parseAndExecute(server.getCommandSource(), - String.format("worldborder set %f", size)); - - JOptionPane.showMessageDialog(dialog, "World border updated!", "Success", JOptionPane.INFORMATION_MESSAGE); - dialog.dispose(); - } catch (NumberFormatException ex) { - JOptionPane.showMessageDialog(dialog, "Invalid number format!", "Error", JOptionPane.ERROR_MESSAGE); - } - }); - - cancelButton.addActionListener(e -> dialog.dispose()); - - dialog.add(applyButton); - dialog.add(cancelButton); - - dialog.setVisible(true); - } - - private void addTimeOption(JMenu menu, String name, long time) { - JMenuItem item = new JMenuItem(name); - item.addActionListener(e -> { - server.getCommandManager().parseAndExecute(server.getCommandSource(), "time set " + time); - }); - menu.add(item); - } - - private void setCustomTime() { - String input = JOptionPane.showInputDialog(parentFrame, "Enter time (0-24000):", "Custom Time", JOptionPane.QUESTION_MESSAGE); - if (input != null) { - try { - long time = Long.parseLong(input); - server.getCommandManager().parseAndExecute(server.getCommandSource(), "time set " + time); - } catch (NumberFormatException e) { - JOptionPane.showMessageDialog(parentFrame, "Invalid time value!", "Error", JOptionPane.ERROR_MESSAGE); - } - } - } - - private void addWeatherOption(JMenu menu, String name, String weather) { - JMenuItem item = new JMenuItem(name); - item.addActionListener(e -> { - server.getCommandManager().parseAndExecute(server.getCommandSource(), "weather " + weather); - }); - menu.add(item); - } - - private void setCustomWeather() { - String[] options = {"Clear", "Rain", "Thunder"}; - String weather = (String) JOptionPane.showInputDialog(parentFrame, "Select weather:", "Custom Weather", - JOptionPane.QUESTION_MESSAGE, null, options, options[0]); - - if (weather != null) { - String duration = JOptionPane.showInputDialog(parentFrame, "Enter duration (seconds):", "Duration", JOptionPane.QUESTION_MESSAGE); - if (duration != null) { - try { - int seconds = Integer.parseInt(duration); - server.getCommandManager().parseAndExecute(server.getCommandSource(), - "weather " + weather.toLowerCase() + " " + seconds); - } catch (NumberFormatException e) { - JOptionPane.showMessageDialog(parentFrame, "Invalid duration!", "Error", JOptionPane.ERROR_MESSAGE); - } - } - } - } - - private void reloadChunks() { - int confirm = JOptionPane.showConfirmDialog(parentFrame, - "Reload all chunks? This may cause lag.", - "Confirm", JOptionPane.YES_NO_OPTION); - - if (confirm == JOptionPane.YES_OPTION) { - server.getCommandManager().parseAndExecute(server.getCommandSource(), "reload"); - JOptionPane.showMessageDialog(parentFrame, "Chunks reloaded!", "Success", JOptionPane.INFORMATION_MESSAGE); - } - } - - private void forceSave() { - server.getCommandManager().parseAndExecute(server.getCommandSource(), "save-all flush"); - JOptionPane.showMessageDialog(parentFrame, "Force save completed!", "Success", JOptionPane.INFORMATION_MESSAGE); - } - - private void backupWorld() { - String[] worldNames = {"Overworld", "Nether", "End", "All Worlds"}; - String selected = (String) JOptionPane.showInputDialog(parentFrame, "Select world to backup:", "World Backup", - JOptionPane.QUESTION_MESSAGE, null, worldNames, worldNames[0]); - - if (selected != null) { - JFileChooser fileChooser = new JFileChooser(); - fileChooser.setDialogTitle("Choose Backup Location"); - fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); - - if (fileChooser.showSaveDialog(parentFrame) == JFileChooser.APPROVE_OPTION) { - File backupDir = fileChooser.getSelectedFile(); - String timestamp = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss").format(new Date()); - File backupFile = new File(backupDir, "world-backup-" + selected.toLowerCase().replace(" ", "-") + "-" + timestamp + ".zip"); - - JOptionPane.showMessageDialog(parentFrame, "Backup started in background...", "Info", JOptionPane.INFORMATION_MESSAGE); - - new Thread(() -> { - try { - server.getCommandManager().parseAndExecute(server.getCommandSource(), "save-all flush"); - Thread.sleep(2000); - - // Backup specific world folder - File worldFolder = selected.equals("All Worlds") ? new File("world") : - new File("world/" + selected.toLowerCase().replace(" ", "_")); - - if (worldFolder.exists()) { - zipDirectory(worldFolder, backupFile); - SwingUtilities.invokeLater(() -> { - JOptionPane.showMessageDialog(parentFrame, "World backup created!\n" + backupFile.getAbsolutePath(), - "Success", JOptionPane.INFORMATION_MESSAGE); - }); - } - } catch (Exception e) { - SwingUtilities.invokeLater(() -> { - JOptionPane.showMessageDialog(parentFrame, "Backup failed: " + e.getMessage(), - "Error", JOptionPane.ERROR_MESSAGE); - }); - } - }).start(); - } - } - } - - private void showGeneralWorldInfo() { - ServerWorld overworld = server.getOverworld(); - if (overworld == null) return; - - WorldProperties properties = overworld.getLevelProperties(); - - String info = String.format( - "World Information\n\n" + - "Seed: %d\n" + - "Spawn: X=%d, Y=%d, Z=%d\n" + - "Difficulty: %s\n" + - "Hardcore: %s\n" + - "Allow Commands: %s", - overworld.getSeed(), - properties.getSpawnPoint().getPos().getX(), properties.getSpawnPoint().getPos().getY(), properties.getSpawnPoint().getPos().getZ(), - properties.getDifficulty().getName(), - properties.isHardcore() ? "Yes" : "No", - overworld.getGameRules().getValue(GameRules.COMMAND_BLOCKS_WORK) ? "Yes" : "No" - ); - - JTextArea textArea = new JTextArea(info); - textArea.setEditable(false); - textArea.setFont(new Font("Monospaced", Font.PLAIN, 12)); - - JButton copyButton = new JButton("Copy Seed"); - copyButton.addActionListener(e -> { - StringSelection selection = new StringSelection(String.valueOf(overworld.getSeed())); - Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, selection); - JOptionPane.showMessageDialog(parentFrame, "Seed copied to clipboard!", "Success", JOptionPane.INFORMATION_MESSAGE); - }); - - JPanel panel = new JPanel(new BorderLayout()); - panel.add(new JScrollPane(textArea), BorderLayout.CENTER); - panel.add(copyButton, BorderLayout.SOUTH); - - JOptionPane.showMessageDialog(parentFrame, panel, "World Information", JOptionPane.INFORMATION_MESSAGE); - } - - // ==================== PERFORMANCE MENU ==================== - private JMenu createPerformanceMenu() { - JMenu perfMenu = new JMenu("Performance"); - perfMenu.setMnemonic('P'); - - // Clear Entities submenu - JMenu clearEntitiesMenu = new JMenu("Clear Entities"); - addClearEntityOption(clearEntitiesMenu, "Items Only", "minecraft:item"); - addClearEntityOption(clearEntitiesMenu, "Hostile Mobs", "hostile"); - addClearEntityOption(clearEntitiesMenu, "All Mobs", "mobs"); - addClearEntityOption(clearEntitiesMenu, "All Non-Player", "all"); - perfMenu.add(clearEntitiesMenu); - - JMenuItem optimizeItem = new JMenuItem("Optimize Chunks"); - optimizeItem.addActionListener(e -> optimizeChunks()); - perfMenu.add(optimizeItem); - - perfMenu.addSeparator(); - - JMenuItem gcItem = new JMenuItem("Force Garbage Collection"); - gcItem.addActionListener(e -> forceGarbageCollection()); - perfMenu.add(gcItem); - - JMenuItem threadDumpItem = new JMenuItem("Thread Dump..."); - threadDumpItem.addActionListener(e -> generateThreadDump()); - perfMenu.add(threadDumpItem); - - JMenuItem perfReportItem = new JMenuItem("Generate Performance Report..."); - perfReportItem.addActionListener(e -> generatePerformanceReport()); - perfMenu.add(perfReportItem); - - return perfMenu; - } - - private void addClearEntityOption(JMenu menu, String name, String type) { - JMenuItem item = new JMenuItem(name); - item.addActionListener(e -> { - int confirm = JOptionPane.showConfirmDialog(parentFrame, - "Clear " + name.toLowerCase() + "?", - "Confirm", JOptionPane.YES_NO_OPTION); - - if (confirm == JOptionPane.YES_OPTION) { - String command = switch (type) { - case "minecraft:item" -> "kill @e[type=item]"; - case "hostile" -> "kill @e[type=!player,type=!item,type=!armor_stand]"; - case "mobs" -> "kill @e[type=!player,type=!item]"; - case "all" -> "kill @e[type=!player]"; - default -> "kill @e[type=" + type + "]"; - }; - - server.getCommandManager().parseAndExecute(server.getCommandSource(), command); - JOptionPane.showMessageDialog(parentFrame, "Entities cleared!", "Success", JOptionPane.INFORMATION_MESSAGE); - } - }); - menu.add(item); - } - - private void optimizeChunks() { - int confirm = JOptionPane.showConfirmDialog(parentFrame, - "Optimize chunks? This may take a while and cause lag.", - "Confirm", JOptionPane.YES_NO_OPTION); - - if (confirm == JOptionPane.YES_OPTION) { - JOptionPane.showMessageDialog(parentFrame, "Optimization started in background...", "Info", JOptionPane.INFORMATION_MESSAGE); - - new Thread(() -> { - try { - // Force save and reload - server.getCommandManager().parseAndExecute(server.getCommandSource(), "save-all flush"); - Thread.sleep(2000); - System.gc(); - - SwingUtilities.invokeLater(() -> { - JOptionPane.showMessageDialog(parentFrame, "Chunk optimization completed!", - "Success", JOptionPane.INFORMATION_MESSAGE); - }); - } catch (Exception e) { - e.printStackTrace(); - } - }).start(); - } - } - - private void forceGarbageCollection() { - Runtime runtime = Runtime.getRuntime(); - long beforeMem = runtime.totalMemory() - runtime.freeMemory(); - - System.gc(); - - long afterMem = runtime.totalMemory() - runtime.freeMemory(); - long freed = beforeMem - afterMem; - - JOptionPane.showMessageDialog(parentFrame, - String.format("Garbage collection completed!\n\nMemory freed: %.2f MB", freed / 1024.0 / 1024.0), - "Success", JOptionPane.INFORMATION_MESSAGE); - } - - private void generateThreadDump() { - StringBuilder dump = new StringBuilder(); - dump.append("=== THREAD DUMP ===\n"); - dump.append("Generated: ").append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())).append("\n\n"); - - Map allThreads = Thread.getAllStackTraces(); - for (Map.Entry entry : allThreads.entrySet()) { - Thread thread = entry.getKey(); - dump.append("Thread: ").append(thread.getName()) - .append(" (").append(thread.getState()).append(")\n"); - - for (StackTraceElement element : entry.getValue()) { - dump.append(" at ").append(element.toString()).append("\n"); - } - dump.append("\n"); // Add spacing between threads - } - - // NOW create the dialog ONCE with all the collected data - JTextArea textArea = new JTextArea(dump.toString()); - textArea.setEditable(false); - textArea.setFont(new Font("Monospaced", Font.PLAIN, 10)); - - JDialog dialog = new JDialog(parentFrame, "Thread Dump", false); - dialog.setLayout(new BorderLayout()); - dialog.add(new JScrollPane(textArea), BorderLayout.CENTER); - - JButton saveButton = new JButton("Save to File"); - saveButton.addActionListener(e -> { - JFileChooser fc = new JFileChooser(); - fc.setSelectedFile(new File("threaddump-" + System.currentTimeMillis() + ".txt")); - if (fc.showSaveDialog(dialog) == JFileChooser.APPROVE_OPTION) { - try (FileWriter writer = new FileWriter(fc.getSelectedFile())) { - writer.write(dump.toString()); - JOptionPane.showMessageDialog(dialog, "Thread dump saved!", "Success", JOptionPane.INFORMATION_MESSAGE); - } catch (IOException ex) { - JOptionPane.showMessageDialog(dialog, "Failed to save: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); - } - } - }); - - JPanel buttonPanel = new JPanel(); - buttonPanel.add(saveButton); - dialog.add(buttonPanel, BorderLayout.SOUTH); - - dialog.setSize(700, 500); - dialog.setLocationRelativeTo(parentFrame); - dialog.setVisible(true); - } - - private void generatePerformanceReport() { - Runtime runtime = Runtime.getRuntime(); - long usedMemory = runtime.totalMemory() - runtime.freeMemory(); - long maxMemory = runtime.maxMemory(); - - int totalEntities = 0; - int totalChunks = 0; - for (ServerWorld world : server.getWorlds()) { - for (Object ignored : world.iterateEntities()) { - totalEntities++; - } - totalChunks += world.getChunkManager().getLoadedChunkCount(); - } - - StringBuilder report = new StringBuilder(); - report.append("=== PERFORMANCE REPORT ===\n"); - report.append("Generated: ").append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())).append("\n\n"); - report.append("=== MEMORY ===\n"); - report.append(String.format("Used: %d MB\n", usedMemory / 1024 / 1024)); - report.append(String.format("Max: %d MB\n", maxMemory / 1024 / 1024)); - report.append(String.format("Usage: %.1f%%\n\n", (usedMemory * 100.0) / maxMemory)); - report.append("=== SERVER ===\n"); - report.append(String.format("TPS: %.2f\n", Math.min(20.0, 1000.0 / server.getAverageTickTime()))); - report.append(String.format("Average Tick: %.2f ms\n", server.getAverageTickTime())); - report.append(String.format("Players: %d\n", server.getCurrentPlayerCount())); - report.append(String.format("Entities: %d\n", totalEntities)); - report.append(String.format("Chunks: %d\n\n", totalChunks)); - report.append("=== SYSTEM ===\n"); - report.append(String.format("CPU Cores: %d\n", runtime.availableProcessors())); - report.append(String.format("Java Version: %s\n", System.getProperty("java.version"))); - report.append(String.format("OS: %s %s\n", System.getProperty("os.name"), System.getProperty("os.version"))); - - JTextArea textArea = new JTextArea(report.toString()); - textArea.setEditable(false); - textArea.setFont(new Font("Monospaced", Font.PLAIN, 12)); - - JDialog dialog = new JDialog(parentFrame, "Performance Report", false); - dialog.setLayout(new BorderLayout()); - dialog.add(new JScrollPane(textArea), BorderLayout.CENTER); - - JButton saveButton = new JButton("Save Report"); - saveButton.addActionListener(e -> { - JFileChooser fc = new JFileChooser(); - fc.setSelectedFile(new File("performance-report-" + System.currentTimeMillis() + ".txt")); - if (fc.showSaveDialog(dialog) == JFileChooser.APPROVE_OPTION) { - try (FileWriter writer = new FileWriter(fc.getSelectedFile())) { - writer.write(report.toString()); - JOptionPane.showMessageDialog(dialog, "Report saved!", "Success", JOptionPane.INFORMATION_MESSAGE); - } catch (IOException ex) { - JOptionPane.showMessageDialog(dialog, "Failed to save: " + ex.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); - } - } - }); - - JPanel buttonPanel = new JPanel(); - buttonPanel.add(saveButton); - dialog.add(buttonPanel, BorderLayout.SOUTH); - - dialog.setSize(500, 400); - dialog.setLocationRelativeTo(parentFrame); - dialog.setVisible(true); - } - - // ==================== TOOLS MENU ==================== - private JMenu createToolsMenu() { JMenu toolsMenu = new JMenu("Tools"); - toolsMenu.setMnemonic('T'); - - JMenuItem commandPaletteItem = new JMenuItem("Command Palette..."); - commandPaletteItem.setAccelerator(KeyStroke.getKeyStroke("ctrl shift P")); - commandPaletteItem.addActionListener(e -> showCommandPalette()); - toolsMenu.add(commandPaletteItem); - - JMenuItem scheduledTasksItem = new JMenuItem("Scheduled Tasks..."); - scheduledTasksItem.addActionListener(e -> showScheduledTasks()); - toolsMenu.add(scheduledTasksItem); - - JMenuItem rconItem = new JMenuItem("RCON Connection..."); - rconItem.addActionListener(e -> showRconConnection()); - toolsMenu.add(rconItem); - - toolsMenu.addSeparator(); - - JMenuItem datapackItem = new JMenuItem("Datapack Manager..."); - datapackItem.addActionListener(e -> showDatapackManager()); - toolsMenu.add(datapackItem); - - JMenuItem resourcePackItem = new JMenuItem("Resource Pack Settings..."); - resourcePackItem.addActionListener(e -> showResourcePackSettings()); - toolsMenu.add(resourcePackItem); - - toolsMenu.addSeparator(); - - JMenuItem serverIconItem = new JMenuItem("Server Icon..."); - serverIconItem.addActionListener(e -> changeServerIcon()); - toolsMenu.add(serverIconItem); - - return toolsMenu; - } - - private void showCommandPalette() { - JDialog dialog = new JDialog(parentFrame, "Command Palette", false); - dialog.setLayout(new BorderLayout()); - dialog.setSize(500, 400); - dialog.setLocationRelativeTo(parentFrame); - - JTextField searchField = new JTextField(); - searchField.setFont(new Font("Monospaced", Font.PLAIN, 14)); - - DefaultListModel commandModel = new DefaultListModel<>(); - String[] commonCommands = { - "give @p diamond 64", - "gamemode creative @a", - "time set day", - "weather clear", - "tp @p 0 100 0", - "kill @e[type=item]", - "difficulty peaceful", - "gamerule doDaylightCycle false", - "whitelist add ", - "op ", - "ban ", - "pardon " - }; - - for (String cmd : commonCommands) { - commandModel.addElement(cmd); - } - - JList commandList = new JList<>(commandModel); - commandList.setFont(new Font("Monospaced", Font.PLAIN, 12)); - - searchField.addActionListener(e -> { - String selected = commandList.getSelectedValue(); - if (selected != null) { - server.getCommandManager().parseAndExecute(server.getCommandSource(), selected); - dialog.dispose(); - } else if (!searchField.getText().isEmpty()) { - server.getCommandManager().parseAndExecute(server.getCommandSource(), searchField.getText()); - dialog.dispose(); + JMenuItem commandItem = new JMenuItem("Run command..."); + commandItem.addActionListener(event -> { + String command = JOptionPane.showInputDialog(parentFrame, "Command:"); + if (command != null && !command.isBlank()) { + execute(command); } }); + toolsMenu.add(commandItem); - dialog.add(searchField, BorderLayout.NORTH); - dialog.add(new JScrollPane(commandList), BorderLayout.CENTER); - dialog.setVisible(true); - searchField.requestFocus(); - } - - private void showScheduledTasks() { - JOptionPane.showMessageDialog(parentFrame, - "Scheduled Tasks feature coming soon!\n\nThis will allow you to schedule commands to run at specific times or intervals.", - "Scheduled Tasks", JOptionPane.INFORMATION_MESSAGE); - } - - private void showRconConnection() { - - JOptionPane.showMessageDialog(this, "RCON connection feature coming soon!", - "Info", JOptionPane.INFORMATION_MESSAGE); - - return; -// JDialog dialog = new JDialog(parentFrame, "RCON Connection", true); -// dialog.setLayout(new GridLayout(4, 2, 10, 10)); -// dialog.setSize(350, 200); -// dialog.setLocationRelativeTo(parentFrame); -// -// dialog.add(new JLabel("Host:")); -// JTextField hostField = new JTextField("localhost"); -// dialog.add(hostField); -// -// dialog.add(new JLabel("Port:")); -// JTextField portField = new JTextField("25575"); -// dialog.add(portField); -// -// dialog.add(new JLabel("Password:")); -// JPasswordField passwordField = new JPasswordField(); -// dialog.add(passwordField); -// -// JButton connectButton = new JButton("Connect"); -// JButton cancelButton = new JButton("Cancel"); -// -// connectButton.addActionListener(e -> { -// JOptionPane.showMessageDialog(dialog, "RCON connection feature coming soon!", -// "Info", JOptionPane.INFORMATION_MESSAGE); -// }); -// -// cancelButton.addActionListener(e -> dialog.dispose()); -// -// dialog.add(connectButton); -// dialog.add(cancelButton); -// -// dialog.setVisible(true); - } - - private void showDatapackManager() { - JDialog dialog = new JDialog(parentFrame, "Datapack Manager", true); - dialog.setLayout(new BorderLayout(10, 10)); - dialog.setSize(500, 400); - dialog.setLocationRelativeTo(parentFrame); - - DefaultListModel datapackModel = new DefaultListModel<>(); - - // Try to list datapacks from world/datapacks directory - File datapackDir = new File("world/datapacks"); - if (datapackDir.exists() && datapackDir.isDirectory()) { - File[] datapacks = datapackDir.listFiles(); - if (datapacks != null) { - for (File dp : datapacks) { - if (dp.isDirectory() || dp.getName().endsWith(".zip")) { - datapackModel.addElement(dp.getName()); - } - } - } - } - - JList datapackList = new JList<>(datapackModel); - JScrollPane scrollPane = new JScrollPane(datapackList); - - JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); - JButton reloadButton = new JButton("Reload Datapacks"); - JButton closeButton = new JButton("Close"); - - reloadButton.addActionListener(e -> { - server.getCommandManager().parseAndExecute(server.getCommandSource(), "reload"); - JOptionPane.showMessageDialog(dialog, "Datapacks reloaded!", "Success", JOptionPane.INFORMATION_MESSAGE); - }); - - closeButton.addActionListener(e -> dialog.dispose()); - - buttonPanel.add(reloadButton); - buttonPanel.add(closeButton); - - dialog.add(scrollPane, BorderLayout.CENTER); - dialog.add(buttonPanel, BorderLayout.SOUTH); - dialog.setVisible(true); - } - - private void showResourcePackSettings() { - JDialog dialog = new JDialog(parentFrame, "Resource Pack Settings", true); - dialog.setLayout(new GridLayout(4, 2, 10, 10)); - dialog.setSize(500, 200); - dialog.setLocationRelativeTo(parentFrame); - - dialog.add(new JLabel("Resource Pack URL:")); - JTextField urlField = new JTextField(); - dialog.add(urlField); - - dialog.add(new JLabel("SHA-1 Hash (optional):")); - JTextField hashField = new JTextField(); - dialog.add(hashField); - - dialog.add(new JLabel("Require Resource Pack:")); - JCheckBox requireCheckbox = new JCheckBox(); - dialog.add(requireCheckbox); - - JButton saveButton = new JButton("Save"); - JButton cancelButton = new JButton("Cancel"); - - saveButton.addActionListener(e -> { - JOptionPane.showMessageDialog(dialog, - "Resource pack settings saved!\nEdit server.properties to apply changes.", - "Success", JOptionPane.INFORMATION_MESSAGE); - dialog.dispose(); - }); - - cancelButton.addActionListener(e -> dialog.dispose()); - - dialog.add(saveButton); - dialog.add(cancelButton); - - dialog.setVisible(true); - } - - private void changeServerIcon() { - JFileChooser fileChooser = new JFileChooser(); - fileChooser.setDialogTitle("Select Server Icon (64x64 PNG)"); - fileChooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("PNG Images", "png")); - - if (fileChooser.showOpenDialog(parentFrame) == JFileChooser.APPROVE_OPTION) { - File selectedFile = fileChooser.getSelectedFile(); - File serverIcon = new File("server-icon.png"); - - try { - Files.copy(selectedFile.toPath(), serverIcon.toPath(), StandardCopyOption.REPLACE_EXISTING); - JOptionPane.showMessageDialog(parentFrame, - "Server icon updated! Restart server for changes to take effect.", - "Success", JOptionPane.INFORMATION_MESSAGE); - } catch (IOException e) { - JOptionPane.showMessageDialog(parentFrame, - "Failed to update icon: " + e.getMessage(), - "Error", JOptionPane.ERROR_MESSAGE); - } - } - } - - // ==================== HELP MENU ==================== - private JMenu createHelpMenu() { - JMenu helpMenu = new JMenu("Help"); - helpMenu.setMnemonic('H'); - - JMenuItem wikiItem = new JMenuItem("Minecraft Wiki"); - wikiItem.addActionListener(e -> openURL("https://minecraft.wiki")); - helpMenu.add(wikiItem); - - JMenuItem commandRefItem = new JMenuItem("Command Reference"); - commandRefItem.addActionListener(e -> showCommandReference()); - helpMenu.add(commandRefItem); - - JMenuItem shortcutsItem = new JMenuItem("Keyboard Shortcuts"); - shortcutsItem.setAccelerator(KeyStroke.getKeyStroke("F1")); - shortcutsItem.addActionListener(e -> showKeyboardShortcuts()); - helpMenu.add(shortcutsItem); - - helpMenu.addSeparator(); - - JMenuItem bugItem = new JMenuItem("Report Bug..."); - bugItem.addActionListener(e -> reportBug()); - helpMenu.add(bugItem); - - helpMenu.addSeparator(); - - JMenuItem sysInfoItem = new JMenuItem("System Information"); - sysInfoItem.addActionListener(e -> showSystemInformation()); - helpMenu.add(sysInfoItem); - - return helpMenu; - } - - private void openURL(String url) { - try { - Desktop.getDesktop().browse(new java.net.URI(url)); - } catch (Exception e) { - JOptionPane.showMessageDialog(parentFrame, - "Failed to open browser: " + e.getMessage(), - "Error", JOptionPane.ERROR_MESSAGE); - } - } - - private void showCommandReference() { - String commands = """ - === ESSENTIAL COMMANDS === - - /help - Show all commands - /list - Show online players - /say - Broadcast message - /tell - Private message - - === PLAYER MANAGEMENT === - /kick [reason] - Kick player - /ban [reason] - Ban player - /pardon - Unban player - /op - Give operator status - /deop - Remove operator status - /whitelist add/remove - Manage whitelist - - === WORLD === - /time set