From a00886fe508035652748d17fea3d3ca38801d5ae Mon Sep 17 00:00:00 2001 From: SlickTorpedo Date: Sun, 26 Oct 2025 18:13:02 -0700 Subject: [PATCH 1/3] Add party coordination for secret waypoints - Send /pc command when secrets are found (chest clicks, item pickups, double-sneak) - Listen for SS-FOUND messages from party members - Queue and auto-remove waypoints based on party chat coordinates - Add 750ms cooldown to prevent command spam - Hide SS-FOUND messages from chat to keep it clean - Add user-friendly [Dungeon Rooms] notifications for important events - Clear pending secrets at start of new dungeon run --- .../dungeons/catacombs/Waypoints.java | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/src/main/java/io/github/quantizr/dungeonrooms/dungeons/catacombs/Waypoints.java b/src/main/java/io/github/quantizr/dungeonrooms/dungeons/catacombs/Waypoints.java index 63f7053..c1c79c5 100644 --- a/src/main/java/io/github/quantizr/dungeonrooms/dungeons/catacombs/Waypoints.java +++ b/src/main/java/io/github/quantizr/dungeonrooms/dungeons/catacombs/Waypoints.java @@ -75,6 +75,10 @@ public class Waypoints { public static List secretsList = new ArrayList<>(Arrays.asList(new Boolean[10])); static long lastSneakTime = 0; + static long lastCommandTime = 0; + + // Store coordinates of found secrets from chat messages + public static Set foundSecretCoords = new HashSet<>(); Frustum frustum = new Frustum(); @@ -105,6 +109,29 @@ public void onWorldRender(RenderWorldLastEvent event) { BlockPos relative = new BlockPos(secretsObject.get("x").getAsInt(), secretsObject.get("y").getAsInt(), secretsObject.get("z").getAsInt()); BlockPos pos = MapUtils.relativeToActual(relative, RoomDetection.roomDirection, RoomDetection.roomCorner); + + // Check if this waypoint was marked as found from chat + if (foundSecretCoords.contains(pos)) { + // Mark it as found in the current room + for(int j = 1; j <= secretNum; j++) { + if (secretsObject.get("secretName").getAsString().substring(0,2).replaceAll("[\\D]", "").equals(String.valueOf(j))) { + if (Waypoints.secretsList.get(j-1)) { + Waypoints.secretsList.set(j-1, false); + Waypoints.allSecretsMap.replace(roomName, Waypoints.secretsList); + foundSecretCoords.remove(pos); // Remove from queue once applied + + // Notify player that queued secret was applied + Minecraft.getMinecraft().thePlayer.addChatMessage( + new net.minecraft.util.ChatComponentText("§6[Dungeon Rooms] §aApplied queued secret #" + j + " from party") + ); + DungeonRooms.logger.info("DungeonRooms: Applied queued secret found for #" + j + " at " + pos.getX() + "," + pos.getY() + "," + pos.getZ()); + } + break; + } + } + continue; // Don't render this waypoint + } + Entity viewer = Minecraft.getMinecraft().getRenderViewEntity(); frustum.setPosition(viewer.posX, viewer.posY, viewer.posZ); if (!frustum.isBoxInFrustum(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, 255, pos.getZ() + 1)){ @@ -183,6 +210,22 @@ public void onWorldRender(RenderWorldLastEvent event) { @SubscribeEvent(priority = EventPriority.HIGHEST) public void onChat(ClientChatReceivedEvent event) { if (!Utils.inCatacombs || !enabled) return; + + String message = event.message.getFormattedText(); + + // Check if dungeon is starting (Mort's message) + if (message.startsWith("§e[NPC] §bMort§f: §rHere, I found this map when I first entered the dungeon.§r")) { + // Clear found secrets from previous run + int clearedCount = foundSecretCoords.size(); + foundSecretCoords.clear(); + if (clearedCount > 0) { + Minecraft.getMinecraft().thePlayer.addChatMessage( + new net.minecraft.util.ChatComponentText("§6[Dungeon Rooms] §eCleared " + clearedCount + " pending secret(s) from party for new run") + ); + } + DungeonRooms.logger.info("DungeonRooms: Cleared found secret coordinates for new dungeon run"); + } + // Action Bar if (event.type == 2) { String[] actionBarSections = event.message.getUnformattedText().split(" {3,}"); @@ -200,6 +243,81 @@ public void onChat(ClientChatReceivedEvent event) { } } } + + // Regular chat messages (type 0) + if (event.type == 0) { + String unformattedMessage = StringUtils.stripControlCodes(event.message.getUnformattedText()); + + // Check for SS-FOUND pattern with coordinates + if (unformattedMessage.contains("SS-FOUND-")) { + // Cancel the event to hide the message from chat + event.setCanceled(true); + + try { + // Extract everything after "SS-FOUND-" + String coordsStr = unformattedMessage.split("SS-FOUND-")[1].trim(); + String[] coords = coordsStr.split(","); + + if (coords.length == 3) { + int x = Integer.parseInt(coords[0].trim()); + int y = Integer.parseInt(coords[1].trim()); + int z = Integer.parseInt(coords[2].trim()); + BlockPos foundPos = new BlockPos(x, y, z); + + // Add to found secrets + boolean isNewSecret = foundSecretCoords.add(foundPos); + + // Try to disable waypoint immediately if in the right room + boolean removed = removeWaypointAtPosition(foundPos); + + // Only notify if this is a new secret and wasn't immediately removed + if (isNewSecret && !removed) { + Minecraft.getMinecraft().thePlayer.addChatMessage( + new net.minecraft.util.ChatComponentText("§6[Dungeon Rooms] §aQueued secret from party chat (will remove when you enter that room)") + ); + } + + DungeonRooms.logger.info("DungeonRooms: Received secret found message for coordinates: " + x + "," + y + "," + z); + } + } catch (Exception e) { + // Ignore parsing errors + } + } + } + } + + // Helper method to remove waypoint at specific position + // Returns true if a waypoint was removed, false otherwise + private boolean removeWaypointAtPosition(BlockPos targetPos) { + String roomName = RoomDetection.roomName; + if (roomName.equals("undefined") || DungeonRooms.roomsJson.get(roomName) == null || secretsList == null) return false; + if (DungeonRooms.waypointsJson.get(roomName) != null) { + JsonArray secretsArray = DungeonRooms.waypointsJson.get(roomName).getAsJsonArray(); + int arraySize = secretsArray.size(); + for(int i = 0; i < arraySize; i++) { + JsonObject secretsObject = secretsArray.get(i).getAsJsonObject(); + BlockPos relative = new BlockPos(secretsObject.get("x").getAsInt(), secretsObject.get("y").getAsInt(), secretsObject.get("z").getAsInt()); + BlockPos pos = MapUtils.relativeToActual(relative, RoomDetection.roomDirection, RoomDetection.roomCorner); + + if (pos.equals(targetPos)) { + for(int j = 1; j <= secretNum; j++) { + if (secretsObject.get("secretName").getAsString().substring(0,2).replaceAll("[\\D]", "").equals(String.valueOf(j))) { + if (!Waypoints.secretsList.get(j-1)) return false; // Already removed + Waypoints.secretsList.set(j-1, false); + Waypoints.allSecretsMap.replace(roomName, Waypoints.secretsList); + + // Send feedback to player + Minecraft.getMinecraft().thePlayer.addChatMessage( + new net.minecraft.util.ChatComponentText("§6[Dungeon Rooms] §aRemoved secret #" + j + " waypoint from party chat") + ); + DungeonRooms.logger.info("DungeonRooms: Marked secret #" + j + " as found from party chat at " + pos.getX() + "," + pos.getY() + "," + pos.getZ()); + return true; + } + } + } + } + } + return false; } @SubscribeEvent @@ -227,6 +345,13 @@ public void onInteract(PlayerInteractEvent event) { Waypoints.secretsList.set(j-1, false); Waypoints.allSecretsMap.replace(roomName, Waypoints.secretsList); DungeonRooms.logger.info("DungeonRooms: Detected " + secretsObject.get("category").getAsString() + " click, turning off waypoint for secret #" + j); + + // Send command with cooldown check + long currentTime = System.currentTimeMillis(); + if (currentTime - lastCommandTime >= 750) { + Minecraft.getMinecraft().thePlayer.sendChatMessage("/pc SS-FOUND-" + pos.getX() + "," + pos.getY() + "," + pos.getZ()); + lastCommandTime = currentTime; + } break; } } @@ -276,6 +401,13 @@ public void onReceivePacket(PacketEvent.ReceiveEvent event) { Waypoints.secretsList.set(j-1, false); Waypoints.allSecretsMap.replace(roomName, Waypoints.secretsList); DungeonRooms.logger.info("DungeonRooms: " + entity.getCommandSenderEntity().getName() + " picked up " + StringUtils.stripControlCodes(name) + " from a " + secretsObject.get("category").getAsString() + " secret, turning off waypoint for secret #" + j); + + // Send command with cooldown check + long currentTime = System.currentTimeMillis(); + if (currentTime - lastCommandTime >= 750) { + mc.thePlayer.sendChatMessage("/pc SS-FOUND-" + pos.getX() + "," + pos.getY() + "," + pos.getZ()); + lastCommandTime = currentTime; + } return; } } @@ -315,6 +447,13 @@ public void onKey(InputEvent.KeyInputEvent event) { Waypoints.secretsList.set(j-1, false); Waypoints.allSecretsMap.replace(roomName, Waypoints.secretsList); DungeonRooms.logger.info("DungeonRooms: Player sneaked near " + secretsObject.get("category").getAsString() + " secret, turning off waypoint for secret #" + j); + + // Send command with cooldown check + long currentTime = System.currentTimeMillis(); + if (currentTime - lastCommandTime >= 750) { + player.sendChatMessage("/pc SS-FOUND-" + pos.getX() + "," + pos.getY() + "," + pos.getZ()); + lastCommandTime = currentTime; + } return; } } From bb6366c8c4b7b3f61f7080be0d080885283f19c5 Mon Sep 17 00:00:00 2001 From: Philip Ehrbright <55722867+SlickTorpedo@users.noreply.github.com> Date: Sun, 26 Oct 2025 18:14:01 -0700 Subject: [PATCH 2/3] Document new waypoint removal feature Added information about waypoint removal feature in the fork. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 9b03a23..36ed4dd 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,9 @@ Short answer: This mod follows the general interpretation of Hypixel's rules, do Long Answer: https://quantizr.github.io/posts/is-it-bannable/ +### What is new in this fork? +When a secret is opened it will remove the waypoint for anyone else using this fork. That way, if you have those people that open a room, get 2 or 3 secrets and leave, you can see (or rather no longer can see) which ones they opened! + ### Discord: [![Discord](https://img.shields.io/discord/804143990869590066?color=%239f00ff&label=Discord&style=for-the-badge)](https://discord.gg/7B5RbsArYK) From cc91410d7ca9d3506f093a11e46915e1aa78e9cd Mon Sep 17 00:00:00 2001 From: SlickTorpedo Date: Fri, 31 Oct 2025 23:15:57 -0700 Subject: [PATCH 3/3] save: video player test --- JAVAFX_FIX.md | 132 +++++++++++ VIDEO_PLAYER_GUIDE.md | 97 ++++++++ .../quantizr/dungeonrooms/DungeonRooms.java | 14 +- .../dungeonrooms/gui/VideoPlayerGUI.java | 176 ++++++++++++++ .../dungeonrooms/video/VideoEventHandler.java | 123 ++++++++++ .../dungeonrooms/video/VideoPlayer.java | 217 ++++++++++++++++++ .../dungeonrooms/video/VideoRenderer.java | 171 ++++++++++++++ 7 files changed, 929 insertions(+), 1 deletion(-) create mode 100644 JAVAFX_FIX.md create mode 100644 VIDEO_PLAYER_GUIDE.md create mode 100644 src/main/java/io/github/quantizr/dungeonrooms/gui/VideoPlayerGUI.java create mode 100644 src/main/java/io/github/quantizr/dungeonrooms/video/VideoEventHandler.java create mode 100644 src/main/java/io/github/quantizr/dungeonrooms/video/VideoPlayer.java create mode 100644 src/main/java/io/github/quantizr/dungeonrooms/video/VideoRenderer.java diff --git a/JAVAFX_FIX.md b/JAVAFX_FIX.md new file mode 100644 index 0000000..67e39c4 --- /dev/null +++ b/JAVAFX_FIX.md @@ -0,0 +1,132 @@ +# JavaFX Compatibility Fix + +## Problem +The video player crashed with `java.lang.NoClassDefFoundError: javafx/scene/image/Image` because JavaFX classes were not available in the Minecraft launcher's Java runtime. + +## Root Cause +- Minecraft launchers (especially modded ones) often use stripped-down JRE distributions that don't include JavaFX +- JavaFX was bundled with Oracle JDK 8 but not with all JRE distributions +- Starting with Java 11, JavaFX was completely removed from the JDK and became a separate module + +## Solution Implemented +Added comprehensive JavaFX availability checks throughout the video player system to gracefully handle missing JavaFX: + +### 1. **VideoPlayer.java** - Core availability detection +```java +private static boolean javafxAvailable = false; + +static { + try { + Class.forName("javafx.application.Platform"); + javafxAvailable = true; + } catch (ClassNotFoundException e) { + System.err.println("JavaFX is not available - Video player will be disabled"); + javafxAvailable = false; + } +} + +public static boolean isJavaFXAvailable() { + return javafxAvailable; +} +``` + +- Added static initializer block that checks for JavaFX classes at class load time +- All methods now check `javafxAvailable` before executing JavaFX code +- Prevents `NoClassDefFoundError` by detecting missing classes early + +### 2. **VideoRenderer.java** - Safe rendering +```java +public void renderVideoOverlay() { + if (!enabled || !VideoPlayer.isJavaFXAvailable()) { + return; + } + // ... rest of rendering code +} +``` + +- Checks JavaFX availability before attempting any rendering operations +- Prevents crashes during the render loop + +### 3. **VideoEventHandler.java** - User-friendly messaging +```java +@SubscribeEvent +public void onRenderOverlay(RenderGameOverlayEvent.Post event) { + if (!VideoPlayer.isJavaFXAvailable()) { + return; // Silently skip if JavaFX not available + } + // ... rendering code +} + +// In key handler: +if (key == Keyboard.KEY_V) { + if (!VideoPlayer.isJavaFXAvailable()) { + Minecraft.getMinecraft().thePlayer.addChatMessage( + new ChatComponentText("§6[Dungeon Rooms] §cVideo player unavailable - JavaFX not found") + ); + return; + } + // ... toggle code +} +``` + +- Added early return in render event handler (line 34 where crash occurred) +- Shows helpful error message to user when they try to use video player +- Prevents all video-related key handlers from executing without JavaFX + +### 4. **VideoPlayerGUI.java** - Informative error screen +```java +@Override +public void drawScreen(int mouseX, int mouseY, float partialTicks) { + this.drawDefaultBackground(); + + if (!VideoPlayer.isJavaFXAvailable()) { + // Display error message + this.drawCenteredString("§c§lJavaFX Not Available", ...); + this.drawCenteredString("§eThe video player requires JavaFX which is not available", ...); + this.drawCenteredString("§ein your current Java installation.", ...); + return; + } + // ... normal GUI rendering +} +``` + +- Shows clear error message when GUI is opened without JavaFX +- Prevents initialization of GUI components that require JavaFX +- User-friendly explanation instead of a crash + +## Benefits + +1. **No More Crashes**: Mod loads successfully even without JavaFX +2. **Graceful Degradation**: Video player feature is disabled, but all other mod features work +3. **User Feedback**: Clear messages explain why video player isn't working +4. **Backward Compatible**: Works on all Minecraft launcher distributions +5. **Future-Proof**: Handles Java 11+ environments where JavaFX is always separate + +## Testing + +Build successful with all safety checks in place: +``` +BUILD SUCCESSFUL in 14s +14 actionable tasks: 10 executed, 4 up-to-date +``` + +## User Experience + +### With JavaFX available: +- Video player works normally +- All features functional + +### Without JavaFX (most users): +- Mod loads without errors +- Pressing 'V' key shows: "§6[Dungeon Rooms] §cVideo player unavailable - JavaFX not found" +- Opening video player GUI (U key) shows informative error screen +- All other mod features (waypoints, party coordination, etc.) work perfectly + +## Future Considerations + +To make the video player work for all users, would need to either: +1. Bundle JavaFX libraries with the mod (increases mod size significantly) +2. Use a different video rendering approach (VLC, native codecs, etc.) +3. Document JavaFX installation instructions for users who want the feature + +For now, the graceful degradation approach ensures the mod works for everyone, with video player as an optional bonus feature for users who have JavaFX. diff --git a/VIDEO_PLAYER_GUIDE.md b/VIDEO_PLAYER_GUIDE.md new file mode 100644 index 0000000..541940a --- /dev/null +++ b/VIDEO_PLAYER_GUIDE.md @@ -0,0 +1,97 @@ +# Video Player Feature Guide + +## Overview +The video player overlay allows you to watch videos while playing Minecraft dungeons. It uses JavaFX (built into Java 8) to render video content over the game. + +## Files Created/Modified + +### New Files: +1. **VideoPlayer.java** - Singleton managing JavaFX MediaPlayer +2. **VideoRenderer.java** - OpenGL overlay rendering with DynamicTexture +3. **VideoEventHandler.java** - Forge event handler for rendering and input +4. **VideoPlayerGUI.java** - GUI with file browser and playback controls + +### Modified Files: +1. **DungeonRooms.java** - Added video event handler registration and keybinding + +## Controls + +### Opening the Video Player GUI: +- Press **U** (default keybinding) +- Configurable in Minecraft Controls menu under "Dungeon Rooms Mod" + +### Video Overlay Controls: +- **V** - Toggle video overlay on/off +- **Space** - Play/Pause +- **Left Arrow** - Seek backward 5 seconds +- **Right Arrow** - Seek forward 5 seconds +- **Up Arrow** - Increase volume +- **Down Arrow** - Decrease volume +- **+** (Plus/Equals key) - Increase opacity +- **-** (Minus key) - Decrease opacity + +## Usage + +1. **Load a Video:** + - Press **U** to open the Video Player GUI + - Click "Browse" to select a video file OR manually enter a file path + - Supported formats: MP4, AVI, MKV, MOV, FLV, WMV + - Click "Load Video" + +2. **Control Playback:** + - Use the GUI buttons OR close the GUI and use keyboard shortcuts + - The video will loop automatically when it reaches the end + +3. **Toggle Overlay:** + - Press **V** to show/hide the video while playing + - Video continues playing in the background when hidden + +## Technical Details + +- **Video Size:** Default 854x480 pixels (centered on screen) +- **Opacity:** Default 0.6 (60% transparent), adjustable 0.1-1.0 +- **Performance:** Uses hardware-accelerated JavaFX rendering +- **Thread Safety:** All JavaFX operations wrapped in Platform.runLater +- **Looping:** Videos automatically loop indefinitely + +## Building + +The video player is fully integrated. Simply build the mod as usual: +``` +.\gradlew.bat build +``` + +The compiled mod will be in: `build\libs\dungeonrooms-1.0.jar` + +## Tips + +1. **Pre-download Videos:** Download videos locally for best performance (no streaming) +2. **Adjust Opacity:** Use +/- keys to find the right transparency for your needs +3. **Practice Mode:** Works great with waypoint practice mode - watch tutorials while learning! +4. **Keybind Conflicts:** Check Minecraft Controls menu if keys don't work + +## Troubleshooting + +**Video won't load:** +- Ensure file path is correct +- Check file format is supported +- Try a different video file + +**Overlay not showing:** +- Press V to toggle overlay +- Check if video is actually loaded and playing +- Look for chat messages indicating video state + +**Performance issues:** +- Use smaller video resolutions (720p or lower recommended) +- Close video player when not in use +- Lower video opacity can improve visibility with less distraction + +## Party Coordination Integration + +This video player works alongside the party coordination features: +- SS-FOUND messages won't interfere with video overlay +- Video continues playing while in dungeons +- Perfect for watching during repetitive farming sessions + +Enjoy your video overlay! 🎥 diff --git a/src/main/java/io/github/quantizr/dungeonrooms/DungeonRooms.java b/src/main/java/io/github/quantizr/dungeonrooms/DungeonRooms.java index 6062c8c..d7487d9 100644 --- a/src/main/java/io/github/quantizr/dungeonrooms/DungeonRooms.java +++ b/src/main/java/io/github/quantizr/dungeonrooms/DungeonRooms.java @@ -24,6 +24,7 @@ import io.github.quantizr.dungeonrooms.commands.RoomCommand; import io.github.quantizr.dungeonrooms.dungeons.catacombs.Waypoints; import io.github.quantizr.dungeonrooms.gui.WaypointsGUI; +import io.github.quantizr.dungeonrooms.gui.VideoPlayerGUI; import io.github.quantizr.dungeonrooms.handlers.ConfigHandler; import io.github.quantizr.dungeonrooms.handlers.OpenLink; import io.github.quantizr.dungeonrooms.handlers.PacketHandler; @@ -31,6 +32,7 @@ import io.github.quantizr.dungeonrooms.dungeons.catacombs.DungeonManager; import io.github.quantizr.dungeonrooms.dungeons.catacombs.RoomDetection; import io.github.quantizr.dungeonrooms.utils.Utils; +import io.github.quantizr.dungeonrooms.video.VideoEventHandler; import net.minecraft.client.Minecraft; import net.minecraft.client.entity.EntityPlayerSP; import net.minecraft.client.gui.ScaledResolution; @@ -84,7 +86,7 @@ public class DungeonRooms public static HashMap> ROOM_DATA = new HashMap<>(); public static boolean usingSBPSecrets = false; - public static KeyBinding[] keyBindings = new KeyBinding[3]; + public static KeyBinding[] keyBindings = new KeyBinding[4]; public static String imageHotkeyOpen = "gui"; static int tickAmount = 1; @@ -127,6 +129,7 @@ public void init(FMLInitializationEvent event) { MinecraftForge.EVENT_BUS.register(new DungeonManager()); MinecraftForge.EVENT_BUS.register(new RoomDetection()); MinecraftForge.EVENT_BUS.register(new Waypoints()); + MinecraftForge.EVENT_BUS.register(new VideoEventHandler()); //reload config ConfigHandler.reloadConfig(); @@ -135,6 +138,7 @@ public void init(FMLInitializationEvent event) { keyBindings[0] = new KeyBinding("Open Room Images in DSG/SBP", Keyboard.KEY_O, "Dungeon Rooms Mod"); keyBindings[1] = new KeyBinding("Open Waypoint Config Menu", Keyboard.KEY_P, "Dungeon Rooms Mod"); keyBindings[2] = new KeyBinding("Show Waypoints in Practice Mode", Keyboard.KEY_I, "Dungeon Rooms Mod"); + keyBindings[3] = new KeyBinding("Open Video Player", Keyboard.KEY_U, "Dungeon Rooms Mod"); for (KeyBinding keyBinding : keyBindings) { ClientRegistry.registerKeyBinding(keyBinding); } @@ -303,6 +307,14 @@ public void onKey(InputEvent.KeyInputEvent event) { + "Dungeon Rooms: Waypoints must be enabled for Practice Mode to work.")); } } + if (keyBindings[3].isPressed()) { + try { + mc.addScheduledTask(() -> mc.displayGuiScreen(new VideoPlayerGUI())); + } catch (NoClassDefFoundError e) { + player.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + + "Dungeon Rooms: Video player unavailable - JavaFX not found")); + } + } } @SubscribeEvent diff --git a/src/main/java/io/github/quantizr/dungeonrooms/gui/VideoPlayerGUI.java b/src/main/java/io/github/quantizr/dungeonrooms/gui/VideoPlayerGUI.java new file mode 100644 index 0000000..9baca7c --- /dev/null +++ b/src/main/java/io/github/quantizr/dungeonrooms/gui/VideoPlayerGUI.java @@ -0,0 +1,176 @@ +/* + * Dungeon Rooms Mod - Secret Waypoints for Hypixel Skyblock Dungeons + * Copyright 2021 Quantizr(_risk) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package io.github.quantizr.dungeonrooms.gui; + +import io.github.quantizr.dungeonrooms.video.VideoPlayer; +import io.github.quantizr.dungeonrooms.video.VideoRenderer; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.GuiTextField; + +import javax.swing.*; +import javax.swing.filechooser.FileNameExtensionFilter; +import java.awt.*; +import java.io.File; +import java.io.IOException; + +public class VideoPlayerGUI extends GuiScreen { + private GuiTextField videoPathField; + private GuiButton loadButton; + private GuiButton browseButton; + private GuiButton playPauseButton; + private GuiButton closeButton; + + private String selectedVideoPath = ""; + + @Override + public void initGui() { + super.initGui(); + + // Check if JavaFX is available + if (!VideoPlayer.isJavaFXAvailable()) { + return; // GUI will show error message in drawScreen + } + + int centerX = this.width / 2; + int startY = this.height / 4; + + // Video path text field + videoPathField = new GuiTextField(0, this.fontRendererObj, centerX - 150, startY, 300, 20); + videoPathField.setMaxStringLength(500); + videoPathField.setText(selectedVideoPath); + + // Browse button + browseButton = new GuiButton(1, centerX - 150, startY + 30, 70, 20, "Browse..."); + this.buttonList.add(browseButton); + + // Load button + loadButton = new GuiButton(2, centerX - 70, startY + 30, 70, 20, "Load Video"); + this.buttonList.add(loadButton); + + // Play/Pause button + playPauseButton = new GuiButton(3, centerX + 10, startY + 30, 70, 20, + VideoPlayer.getInstance().isPlaying() ? "Pause" : "Play"); + this.buttonList.add(playPauseButton); + + // Close button + closeButton = new GuiButton(4, centerX - 50, startY + 80, 100, 20, "Close"); + this.buttonList.add(closeButton); + } + + @Override + protected void actionPerformed(GuiButton button) throws IOException { + if (button.id == 1) { // Browse + openFileBrowser(); + } else if (button.id == 2) { // Load Video + String path = videoPathField.getText().trim(); + if (!path.isEmpty()) { + VideoPlayer.getInstance().loadVideo(path); + mc.thePlayer.addChatMessage( + new net.minecraft.util.ChatComponentText("§6[Dungeon Rooms] §aLoading video...") + ); + } + } else if (button.id == 3) { // Play/Pause + VideoPlayer.getInstance().togglePlayPause(); + playPauseButton.displayString = VideoPlayer.getInstance().isPlaying() ? "Pause" : "Play"; + } else if (button.id == 4) { // Close + mc.displayGuiScreen(null); + } + } + + private void openFileBrowser() { + // Run file chooser on a separate thread to avoid blocking + new Thread(() -> { + try { + JFileChooser fileChooser = new JFileChooser(); + fileChooser.setDialogTitle("Select Video File"); + fileChooser.setFileFilter(new FileNameExtensionFilter( + "Video Files", "mp4", "avi", "mkv", "mov", "flv", "wmv")); + + int result = fileChooser.showOpenDialog(null); + if (result == JFileChooser.APPROVE_OPTION) { + File selectedFile = fileChooser.getSelectedFile(); + selectedVideoPath = selectedFile.getAbsolutePath(); + videoPathField.setText(selectedVideoPath); + } + } catch (Exception e) { + e.printStackTrace(); + } + }).start(); + } + + @Override + protected void keyTyped(char typedChar, int keyCode) throws IOException { + super.keyTyped(typedChar, keyCode); + videoPathField.textboxKeyTyped(typedChar, keyCode); + } + + @Override + protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOException { + super.mouseClicked(mouseX, mouseY, mouseButton); + videoPathField.mouseClicked(mouseX, mouseY, mouseButton); + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + this.drawDefaultBackground(); + + // Title + this.drawCenteredString(this.fontRendererObj, "§6§lDungeon Rooms - Video Player", + this.width / 2, this.height / 4 - 40, 0xFFFFFF); + + // Check if JavaFX is available + if (!VideoPlayer.isJavaFXAvailable()) { + this.drawCenteredString(this.fontRendererObj, "§c§lJavaFX Not Available", + this.width / 2, this.height / 2 - 20, 0xFF5555); + this.drawCenteredString(this.fontRendererObj, "§eThe video player requires JavaFX which is not available", + this.width / 2, this.height / 2, 0xFFFFFF); + this.drawCenteredString(this.fontRendererObj, "§ein your current Java installation.", + this.width / 2, this.height / 2 + 15, 0xFFFFFF); + this.drawCenteredString(this.fontRendererObj, "§7Press ESC to close", + this.width / 2, this.height / 2 + 40, 0xAAAAAA); + return; + } + + // Instructions + this.drawCenteredString(this.fontRendererObj, "§eControls: V=Toggle | Space=Play/Pause | Arrows=Seek/Volume | +/-=Opacity", + this.width / 2, this.height / 4 - 20, 0xFFFFFF); + + // Video path label + this.drawString(this.fontRendererObj, "Video Path:", this.width / 2 - 150, + this.height / 4 - 12, 0xFFFFFF); + + // Status + String status = "Status: " + (VideoRenderer.getInstance().isEnabled() ? "§aEnabled" : "§cDisabled"); + if (VideoPlayer.getInstance().isInitialized()) { + status += " | " + (VideoPlayer.getInstance().isPlaying() ? "§aPlaying" : "§ePaused"); + } + this.drawCenteredString(this.fontRendererObj, status, + this.width / 2, this.height / 4 + 60, 0xFFFFFF); + + videoPathField.drawTextBox(); + super.drawScreen(mouseX, mouseY, partialTicks); + } + + @Override + public boolean doesGuiPauseGame() { + return false; + } +} diff --git a/src/main/java/io/github/quantizr/dungeonrooms/video/VideoEventHandler.java b/src/main/java/io/github/quantizr/dungeonrooms/video/VideoEventHandler.java new file mode 100644 index 0000000..dbd94ac --- /dev/null +++ b/src/main/java/io/github/quantizr/dungeonrooms/video/VideoEventHandler.java @@ -0,0 +1,123 @@ +/* + * Dungeon Rooms Mod - Secret Waypoints for Hypixel Skyblock Dungeons + * Copyright 2021 Quantizr(_risk) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package io.github.quantizr.dungeonrooms.video; + +import net.minecraft.client.Minecraft; +import net.minecraftforge.client.event.RenderGameOverlayEvent; +import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.InputEvent; +import org.lwjgl.input.Keyboard; + +public class VideoEventHandler { + + private static boolean videoFocused = false; + + @SubscribeEvent + public void onRenderOverlay(RenderGameOverlayEvent.Post event) { + if (event.type != RenderGameOverlayEvent.ElementType.ALL) { + return; + } + + try { + if (!VideoPlayer.isJavaFXAvailable()) { + return; + } + VideoRenderer.getInstance().renderVideoOverlay(); + } catch (NoClassDefFoundError e) { + // JavaFX classes not available - silently ignore + } + } + + @SubscribeEvent + public void onKeyInput(InputEvent.KeyInputEvent event) { + if (!Keyboard.getEventKeyState()) { + return; + } + + try { + int key = Keyboard.getEventKey(); + + // Toggle video overlay with 'V' key + if (key == Keyboard.KEY_V) { + if (!VideoPlayer.isJavaFXAvailable()) { + Minecraft.getMinecraft().thePlayer.addChatMessage( + new net.minecraft.util.ChatComponentText("§6[Dungeon Rooms] §cVideo player unavailable - JavaFX not found") + ); + return; + } + + VideoRenderer renderer = VideoRenderer.getInstance(); + renderer.toggleEnabled(); + + if (renderer.isEnabled()) { + VideoPlayer.getInstance().play(); + Minecraft.getMinecraft().thePlayer.addChatMessage( + new net.minecraft.util.ChatComponentText("§6[Dungeon Rooms] §aVideo overlay enabled") + ); + } else { + VideoPlayer.getInstance().pause(); + Minecraft.getMinecraft().thePlayer.addChatMessage( + new net.minecraft.util.ChatComponentText("§6[Dungeon Rooms] §cVideo overlay disabled") + ); + } + } + + // Only process video controls if JavaFX is available and overlay is enabled + if (!VideoPlayer.isJavaFXAvailable() || !VideoRenderer.getInstance().isEnabled()) { + return; + } + + switch (key) { + case Keyboard.KEY_SPACE: + VideoPlayer.getInstance().togglePlayPause(); + break; + case Keyboard.KEY_RIGHT: + VideoPlayer.getInstance().seek(10); // Skip forward 10 seconds + break; + case Keyboard.KEY_LEFT: + VideoPlayer.getInstance().seek(-10); // Skip backward 10 seconds + break; + case Keyboard.KEY_UP: + VideoPlayer.getInstance().adjustVolume(0.1); + break; + case Keyboard.KEY_DOWN: + VideoPlayer.getInstance().adjustVolume(-0.1); + break; + case Keyboard.KEY_EQUALS: // + key + case Keyboard.KEY_ADD: + VideoRenderer.getInstance().adjustOpacity(0.1f); + Minecraft.getMinecraft().thePlayer.addChatMessage( + new net.minecraft.util.ChatComponentText("§6[Dungeon Rooms] §eOpacity: " + + String.format("%.1f", VideoRenderer.getInstance().getOpacity())) + ); + break; + case Keyboard.KEY_MINUS: + case Keyboard.KEY_SUBTRACT: + VideoRenderer.getInstance().adjustOpacity(-0.1f); + Minecraft.getMinecraft().thePlayer.addChatMessage( + new net.minecraft.util.ChatComponentText("§6[Dungeon Rooms] §eOpacity: " + + String.format("%.1f", VideoRenderer.getInstance().getOpacity())) + ); + break; + } + } catch (NoClassDefFoundError e) { + // JavaFX classes not available - silently ignore + } + } +} diff --git a/src/main/java/io/github/quantizr/dungeonrooms/video/VideoPlayer.java b/src/main/java/io/github/quantizr/dungeonrooms/video/VideoPlayer.java new file mode 100644 index 0000000..6861f65 --- /dev/null +++ b/src/main/java/io/github/quantizr/dungeonrooms/video/VideoPlayer.java @@ -0,0 +1,217 @@ +/* + * Dungeon Rooms Mod - Secret Waypoints for Hypixel Skyblock Dungeons + * Copyright 2021 Quantizr(_risk) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package io.github.quantizr.dungeonrooms.video; + +import javafx.application.Platform; +import javafx.embed.swing.JFXPanel; +import javafx.scene.Scene; +import javafx.scene.layout.StackPane; +import javafx.scene.media.Media; +import javafx.scene.media.MediaPlayer; +import javafx.scene.media.MediaView; +import javafx.util.Duration; + +import java.io.File; + +public class VideoPlayer { + private static VideoPlayer instance; + private static boolean javafxAvailable = false; + + private MediaPlayer mediaPlayer; + private MediaView mediaView; + private JFXPanel fxPanel; + private boolean initialized = false; + private String currentVideoPath; + + static { + // Check if JavaFX is available + try { + Class.forName("javafx.application.Platform"); + javafxAvailable = true; + } catch (ClassNotFoundException e) { + System.err.println("JavaFX is not available - Video player will be disabled"); + javafxAvailable = false; + } + } + + public static boolean isJavaFXAvailable() { + return javafxAvailable; + } + + public static VideoPlayer getInstance() { + if (instance == null) { + instance = new VideoPlayer(); + } + return instance; + } + + private VideoPlayer() { + if (!javafxAvailable) { + return; + } + try { + // Initialize JavaFX + new JFXPanel(); // This initializes the JavaFX toolkit + } catch (Exception e) { + System.err.println("Failed to initialize JavaFX: " + e.getMessage()); + javafxAvailable = false; + } + } + + public void loadVideo(String filePath) { + if (!javafxAvailable) { + return; + } + File videoFile = new File(filePath); + if (!videoFile.exists()) { + System.err.println("Video file not found: " + filePath); + return; + } + + currentVideoPath = filePath; + + Platform.runLater(() -> { + try { + // Dispose of old media player if exists + if (mediaPlayer != null) { + mediaPlayer.stop(); + mediaPlayer.dispose(); + } + + Media media = new Media(videoFile.toURI().toString()); + mediaPlayer = new MediaPlayer(media); + + // Configure media player + mediaPlayer.setAutoPlay(false); + mediaPlayer.setCycleCount(MediaPlayer.INDEFINITE); // Loop video + mediaPlayer.setVolume(0.5); + + // Create media view if not exists + if (mediaView == null) { + mediaView = new MediaView(mediaPlayer); + mediaView.setPreserveRatio(true); + + StackPane root = new StackPane(); + root.getChildren().add(mediaView); + + Scene scene = new Scene(root); + + if (fxPanel == null) { + fxPanel = new JFXPanel(); + } + fxPanel.setScene(scene); + } else { + mediaView.setMediaPlayer(mediaPlayer); + } + + initialized = true; + System.out.println("Video loaded: " + filePath); + } catch (Exception e) { + System.err.println("Error loading video: " + e.getMessage()); + e.printStackTrace(); + } + }); + } + + public void play() { + if (!javafxAvailable || mediaPlayer == null || !initialized) { + return; + } + Platform.runLater(() -> mediaPlayer.play()); + } + + public void pause() { + if (!javafxAvailable || mediaPlayer == null || !initialized) { + return; + } + Platform.runLater(() -> mediaPlayer.pause()); + } + + public void togglePlayPause() { + if (!javafxAvailable || mediaPlayer == null || !initialized) { + return; + } + Platform.runLater(() -> { + if (mediaPlayer.getStatus() == MediaPlayer.Status.PLAYING) { + mediaPlayer.pause(); + } else { + mediaPlayer.play(); + } + }); + } + + public void seek(double seconds) { + if (!javafxAvailable || mediaPlayer == null || !initialized) { + return; + } + Platform.runLater(() -> { + Duration current = mediaPlayer.getCurrentTime(); + Duration newTime = current.add(Duration.seconds(seconds)); + mediaPlayer.seek(newTime); + }); + } + + public void setVolume(double volume) { + if (!javafxAvailable || mediaPlayer == null || !initialized) { + return; + } + Platform.runLater(() -> mediaPlayer.setVolume(Math.max(0.0, Math.min(1.0, volume)))); + } + + public void adjustVolume(double delta) { + if (!javafxAvailable || mediaPlayer == null || !initialized) { + return; + } + Platform.runLater(() -> { + double newVolume = mediaPlayer.getVolume() + delta; + mediaPlayer.setVolume(Math.max(0.0, Math.min(1.0, newVolume))); + }); + } + + public boolean isInitialized() { + return javafxAvailable && initialized; + } + + public boolean isPlaying() { + if (!javafxAvailable || mediaPlayer == null || !initialized) { + return false; + } + return mediaPlayer.getStatus() == MediaPlayer.Status.PLAYING; + } + + public JFXPanel getFxPanel() { + return fxPanel; + } + + public MediaView getMediaView() { + return mediaView; + } + + public void dispose() { + if (!javafxAvailable || mediaPlayer == null) { + return; + } + Platform.runLater(() -> { + mediaPlayer.stop(); + mediaPlayer.dispose(); + mediaPlayer = null; + }); + initialized = false; + } +} diff --git a/src/main/java/io/github/quantizr/dungeonrooms/video/VideoRenderer.java b/src/main/java/io/github/quantizr/dungeonrooms/video/VideoRenderer.java new file mode 100644 index 0000000..5a9db7e --- /dev/null +++ b/src/main/java/io/github/quantizr/dungeonrooms/video/VideoRenderer.java @@ -0,0 +1,171 @@ +/* + * Dungeon Rooms Mod - Secret Waypoints for Hypixel Skyblock Dungeons + * Copyright 2021 Quantizr(_risk) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package io.github.quantizr.dungeonrooms.video; + +import javafx.embed.swing.SwingFXUtils; +import javafx.scene.SnapshotParameters; +import javafx.scene.image.WritableImage; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.GlStateManager; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.WorldRenderer; +import net.minecraft.client.renderer.texture.DynamicTexture; +import net.minecraft.client.renderer.vertex.DefaultVertexFormats; +import org.lwjgl.opengl.GL11; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; + +public class VideoRenderer { + private static VideoRenderer instance; + private DynamicTexture videoTexture; + private WritableImage fxImage; + private BufferedImage bufferedImage; + + private float opacity = 0.6f; + private int width = 854; // Default 480p width + private int height = 480; // Default 480p height + + private boolean enabled = false; + + public static VideoRenderer getInstance() { + if (instance == null) { + instance = new VideoRenderer(); + } + return instance; + } + + private VideoRenderer() { + // Initialize + } + + public void renderVideoOverlay() { + if (!enabled || !VideoPlayer.isJavaFXAvailable()) { + return; + } + + VideoPlayer player = VideoPlayer.getInstance(); + if (!player.isInitialized()) { + return; + } + + try { + // Capture frame from JavaFX MediaView + captureFrame(); + + if (videoTexture != null) { + Minecraft mc = Minecraft.getMinecraft(); + ScaledResolution sr = new ScaledResolution(mc); + + int screenWidth = sr.getScaledWidth(); + int screenHeight = sr.getScaledHeight(); + + // Center the video + int x = (screenWidth - width) / 2; + int y = (screenHeight - height) / 2; + + // Set up OpenGL for overlay rendering + GlStateManager.pushMatrix(); + GlStateManager.enableBlend(); + GlStateManager.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GlStateManager.color(1.0f, 1.0f, 1.0f, opacity); + + // Bind and draw texture + mc.getTextureManager().bindTexture(mc.getTextureManager().getDynamicTextureLocation("video_overlay", videoTexture)); + + Tessellator tessellator = Tessellator.getInstance(); + WorldRenderer worldrenderer = tessellator.getWorldRenderer(); + worldrenderer.begin(GL11.GL_QUADS, DefaultVertexFormats.POSITION_TEX); + worldrenderer.pos(x, y + height, 0.0D).tex(0.0D, 1.0D).endVertex(); + worldrenderer.pos(x + width, y + height, 0.0D).tex(1.0D, 1.0D).endVertex(); + worldrenderer.pos(x + width, y, 0.0D).tex(1.0D, 0.0D).endVertex(); + worldrenderer.pos(x, y, 0.0D).tex(0.0D, 0.0D).endVertex(); + tessellator.draw(); + + GlStateManager.disableBlend(); + GlStateManager.popMatrix(); + } + } catch (Exception e) { + // Silently fail to avoid spam + } + } + + private void captureFrame() { + try { + if (VideoPlayer.getInstance().getMediaView() != null) { + javafx.application.Platform.runLater(() -> { + try { + if (fxImage == null) { + fxImage = new WritableImage(width, height); + } + + SnapshotParameters params = new SnapshotParameters(); + VideoPlayer.getInstance().getMediaView().snapshot(params, fxImage); + + // Convert to BufferedImage + bufferedImage = SwingFXUtils.fromFXImage(fxImage, bufferedImage); + + // Update texture + if (videoTexture == null) { + videoTexture = new DynamicTexture(bufferedImage); + } else { + videoTexture.updateDynamicTexture(); + } + } catch (Exception e) { + // Ignore + } + }); + } + } catch (Exception e) { + // Ignore + } + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnabled() { + return enabled; + } + + public void toggleEnabled() { + this.enabled = !this.enabled; + } + + public void setOpacity(float opacity) { + this.opacity = Math.max(0.1f, Math.min(1.0f, opacity)); + } + + public float getOpacity() { + return opacity; + } + + public void adjustOpacity(float delta) { + setOpacity(opacity + delta); + } + + public void setSize(int width, int height) { + this.width = width; + this.height = height; + fxImage = null; // Reset image to force recreation + } +}