diff --git a/common/src/main/java/gjum/minecraft/civ/snitchmod/common/JalistAutoPaginator.java b/common/src/main/java/gjum/minecraft/civ/snitchmod/common/JalistAutoPaginator.java new file mode 100644 index 0000000..1e6265d --- /dev/null +++ b/common/src/main/java/gjum/minecraft/civ/snitchmod/common/JalistAutoPaginator.java @@ -0,0 +1,234 @@ +package gjum.minecraft.civ.snitchmod.common; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.world.inventory.ClickType; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; + +import java.util.HashSet; +import java.util.Set; + +public class JalistAutoPaginator { + private static final Minecraft mc = Minecraft.getInstance(); + private static JalistAutoPaginator instance; + + private boolean isActive = false; + private boolean waitingForNextPage = false; + private int pagesProcessed = 0; + private int totalSnitchesFound = 0; + private int snitchesWillCullSoon = 0; // Count snitches that will cull in next 48h + private int snitchesWillDormantSoon = 0; // Count snitches that will go dormant in next 48h + private Set groupsFound = new HashSet<>(); + private long startTime = 0; + + // JAList GUI detection + private static final String JALIST_TITLE = "JukeAlert snitches"; + private static final int NEXT_PAGE_SLOT = 53; // Bottom right slot in a 54-slot inventory + + public static JalistAutoPaginator getInstance() { + if (instance == null) { + instance = new JalistAutoPaginator(); + } + return instance; + } + + public void startAutoPagination() { + if (isActive) { + logToChat("Auto-pagination already running!"); + return; + } + + if (!isJalistOpen()) { + logToChat("JAList must be open to start auto-pagination! Press J key while in JAList."); + return; + } + + isActive = true; + waitingForNextPage = false; + pagesProcessed = 0; + totalSnitchesFound = 0; + snitchesWillCullSoon = 0; + snitchesWillDormantSoon = 0; + groupsFound.clear(); + startTime = System.currentTimeMillis(); + + logToChat("Starting JAList auto-pagination... This will read all pages automatically."); + + // Start the first page click after a short delay + new Thread(() -> { + try { + Thread.sleep(50); // Wait 50ms before starting + if (isActive) { + mc.execute(this::clickNextPage); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }).start(); + } + + public void stopAutoPagination() { + if (!isActive) return; + + isActive = false; + waitingForNextPage = false; + + long duration = (System.currentTimeMillis() - startTime) / 1000; + logToChat(String.format( + "JAList scan complete! Found %d snitches across %d groups in %d pages (%ds)", + totalSnitchesFound, groupsFound.size(), pagesProcessed, duration + )); + + // Add warnings about snitches that will cull or go dormant soon + if (snitchesWillCullSoon > 0 || snitchesWillDormantSoon > 0) { + StringBuilder warning = new StringBuilder("⚠ Warning: You have "); + + if (snitchesWillCullSoon > 0 && snitchesWillDormantSoon > 0) { + warning.append(String.format("%d snitches that will be culled and %d that will go dormant in the next 48h!", snitchesWillCullSoon, snitchesWillDormantSoon)); + } else if (snitchesWillCullSoon > 0) { + warning.append(String.format("%d snitches that will be culled in the next 48h!", snitchesWillCullSoon)); + } else { + warning.append(String.format("%d snitches that will go dormant in the next 48h!", snitchesWillDormantSoon)); + } + + warning.append(" Use https://civinfo.net/snitches/map to see where they are!"); + logToChat(warning.toString()); + } + } + + public void onJalistPageLoaded(int snitchCount) { + if (!isActive) return; + + pagesProcessed++; + totalSnitchesFound += snitchCount; + waitingForNextPage = false; + + // Show progress every 10 pages instead of 5 + if (pagesProcessed % 10 == 0) { + logToChat(String.format("Progress: %d pages (%d snitches so far)", pagesProcessed, totalSnitchesFound)); + } + + // Schedule next page click with very minimal delay for maximum speed + new Thread(() -> { + try { + Thread.sleep(10); // Wait only 10ms between pages for maximum speed + if (isActive) { + mc.execute(this::clickNextPage); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }).start(); + } + + public void addSnitchEntry(String group, long dormantTs, long cullTs) { + if (group != null && !group.trim().isEmpty()) { + groupsFound.add(group); + } + + long currentTime = System.currentTimeMillis(); + long fortyEightHoursFromNow = currentTime + (48L * 60L * 60L * 1000L); // 48 hours in milliseconds + + if (cullTs != 0 && cullTs <= fortyEightHoursFromNow) { + snitchesWillCullSoon++; + } + + if (dormantTs != 0 && dormantTs <= fortyEightHoursFromNow) { + snitchesWillDormantSoon++; + } + } + + private void clickNextPage() { + if (!isActive || waitingForNextPage) { + return; + } + + var screen = mc.screen; + if (!(screen instanceof AbstractContainerScreen containerScreen)) { + stopAutoPagination(); + return; + } + + if (!isJalistOpen()) { + stopAutoPagination(); + return; + } + + // Get the next page button (arrow in slot 53) + var container = containerScreen.getMenu(); + + if (container.slots.size() <= NEXT_PAGE_SLOT) { + stopAutoPagination(); + return; + } + + ItemStack nextPageItem = container.getSlot(NEXT_PAGE_SLOT).getItem(); + + // Check if there's a next page (should be an arrow item) + if (nextPageItem.isEmpty() || !isArrowItem(nextPageItem)) { + stopAutoPagination(); + return; + } + + // Click the next page button + waitingForNextPage = true; + + // Simulate a left click on the slot + if (mc.gameMode != null) { + mc.gameMode.handleInventoryMouseClick( + container.containerId, + NEXT_PAGE_SLOT, + 0, + ClickType.PICKUP, + mc.player + ); + } + } + + private boolean isJalistOpen() { + var screen = mc.screen; + if (!(screen instanceof AbstractContainerScreen containerScreen)) { + return false; + } + + var title = containerScreen.getTitle().getString(); + return title.contains("JukeAlert") || title.contains("snitches") || title.toLowerCase().contains("your snitches"); + } + + private boolean isArrowItem(ItemStack item) { + // Check for common arrow items used for pagination + return item.is(Items.ARROW) || + item.is(Items.SPECTRAL_ARROW) || + item.getDisplayName().getString().toLowerCase().contains("next"); + } + + public boolean isActive() { + return isActive; + } + + public boolean isWaitingForNextPage() { + return waitingForNextPage; + } + + public static void onTick() { + // No longer needed for this approach + } + + public static void onChatMessage(String message) { + // No longer needed for this approach + } + + public static boolean isRunning() { + return getInstance().isActive(); + } + + public static int getCurrentPage() { + return getInstance().pagesProcessed; + } + + private void logToChat(String message) { + mc.gui.getChat().addMessage(Component.literal("[JAList Auto] " + message)); + } +} \ No newline at end of file diff --git a/common/src/main/java/gjum/minecraft/civ/snitchmod/common/Renderer.java b/common/src/main/java/gjum/minecraft/civ/snitchmod/common/Renderer.java index 2fba966..224749d 100644 --- a/common/src/main/java/gjum/minecraft/civ/snitchmod/common/Renderer.java +++ b/common/src/main/java/gjum/minecraft/civ/snitchmod/common/Renderer.java @@ -546,20 +546,43 @@ private static void renderBoxGuides(AABB box, Color color, float a, float lineWi * middle center of text is at `pos` before moving it down the screen by `offset` */ private static void renderTextFacingCamera(Component text, Vec3 pos, float offset, float scale, int colorAlphaHex) { + // Create a new pose stack for proper 3D positioning + PoseStack poseStack = new PoseStack(); + + // Translate to the world position + poseStack.translate(pos.x, pos.y, pos.z); + + // Make text face the camera + poseStack.mulPose(mc.gameRenderer.getMainCamera().rotation()); + + // Calculate scale based on distance + scale *= 0.005f * (mc.player.position().distanceTo(pos) / 2.4); + scale = Math.clamp(scale, 0.015f, 0.15f); + + // Apply scaling (negative Y to flip text right-side up) + poseStack.scale(scale, -scale, scale); + + // Calculate text positioning float w = mc.font.width(text); float x = -w / 2f; float y = -(.5f - offset) * (mc.font.lineHeight + 2); // +2 for background padding, -1 for default line spacing boolean shadow = false; - scale *= 0.005f * (mc.player.position().distanceTo(pos) / 2.4); - scale = Math.clamp(scale, 0.015f, 0.15f); - Matrix4f matrix = new Matrix4f(eventPoseStack.last().pose()); - matrix.scale(scale, -scale, 1); // third component determines background distance + + // Get the final transformation matrix + Matrix4f matrix = poseStack.last().pose(); + + // Background settings - make it more transparent to see text better float bgOpacity = Minecraft.getInstance().options.getBackgroundOpacity(0.25f); int bgColor = (int) (bgOpacity * 255.0f) << 24; - int flags = 0; - // XXX somehow, the letters farthest from the crosshair render behind the background - try (RenderBufferGuard guard = RenderBufferGuard.open(false, false, false)) { - mc.font.drawInBatch(text, x, y, colorAlphaHex, shadow, matrix, guard.bufferSource, Font.DisplayMode.SEE_THROUGH, bgColor, flags); + + // Ensure text has full alpha if not already set + if ((colorAlphaHex & 0xFF000000) == 0) { + colorAlphaHex |= 0xFF000000; // Add full alpha if missing + } + + // Use immediate mode rendering with proper depth handling + try (RenderBufferGuard guard = RenderBufferGuard.open(false, true, false)) { + mc.font.drawInBatch(text, x, y, colorAlphaHex, shadow, matrix, guard.bufferSource, Font.DisplayMode.NORMAL, bgColor, 15728880); } /*var poseStack = new PoseStack(); diff --git a/common/src/main/java/gjum/minecraft/civ/snitchmod/common/SnitchMod.java b/common/src/main/java/gjum/minecraft/civ/snitchmod/common/SnitchMod.java index 48c68e0..e8c2724 100644 --- a/common/src/main/java/gjum/minecraft/civ/snitchmod/common/SnitchMod.java +++ b/common/src/main/java/gjum/minecraft/civ/snitchmod/common/SnitchMod.java @@ -57,6 +57,13 @@ public abstract class SnitchMod { "category.snitchmod" ); + protected static final KeyMapping jalistAutoKey = new KeyMapping( + "key.snitchmod.jalistAuto", + InputConstants.Type.KEYSYM, + GLFW.GLFW_KEY_J, + "category.snitchmod" + ); + protected static final KeyMapping toggleSnitchGoneStatusKey = new KeyMapping( "key.snitchmod.toggleSnitchGoneStatusKey", InputConstants.Type.KEYSYM, @@ -145,6 +152,12 @@ public void handleTick() { logToChat(Component.literal("Reloaded the database")); } + while (jalistAutoKey.consumeClick()) { + // Start JAList auto-pagination - works even in GUI + System.out.println("[SnitchMod] J key pressed! Current screen: " + (mc.screen != null ? mc.screen.getClass().getSimpleName() : "null")); + JalistAutoPaginator.getInstance().startAutoPagination(); + } + while (toggleSnitchGoneStatusKey.consumeClick()) { if (!getMod().rangeOverlayVisible) { break; @@ -301,6 +314,10 @@ public void handleWindowItems(List stacks) { JalistEntry jalistEntry = JalistEntry.fromStack(stack, store.server); if (jalistEntry != null) { jalistEntries.add(jalistEntry); + // Notify auto-paginator with actual timestamp data + if (JalistAutoPaginator.getInstance().isActive()) { + JalistAutoPaginator.getInstance().addSnitchEntry(jalistEntry.group, jalistEntry.dormantTs, jalistEntry.cullTs); + } } } catch (Throwable e) { System.err.println("Failed parsing jalist stack " + i + " " + stack); @@ -310,7 +327,12 @@ public void handleWindowItems(List stacks) { } store.updateSnitchesFromJalist(jalistEntries); if (jalistEntries.size() > 0) { - logToChat(Component.literal("Found " + jalistEntries.size() + " snitches on JAList page")); + // Notify auto-paginator that this page was processed + if (JalistAutoPaginator.getInstance().isActive()) { + JalistAutoPaginator.getInstance().onJalistPageLoaded(jalistEntries.size()); + } else { + logToChat(Component.literal("Found " + jalistEntries.size() + " snitches on JAList page")); + } } } diff --git a/fabric/src/main/java/gjum/minecraft/civ/snitchmod/fabric/FabricSnitchMod.java b/fabric/src/main/java/gjum/minecraft/civ/snitchmod/fabric/FabricSnitchMod.java index fa0c484..e2c8a35 100644 --- a/fabric/src/main/java/gjum/minecraft/civ/snitchmod/fabric/FabricSnitchMod.java +++ b/fabric/src/main/java/gjum/minecraft/civ/snitchmod/fabric/FabricSnitchMod.java @@ -1,10 +1,14 @@ package gjum.minecraft.civ.snitchmod.fabric; +import gjum.minecraft.civ.snitchmod.common.JalistAutoPaginator; import gjum.minecraft.civ.snitchmod.common.SnitchMod; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents; import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; import net.fabricmc.fabric.api.client.rendering.v1.WorldRenderEvents; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import org.lwjgl.glfw.GLFW; public class FabricSnitchMod extends SnitchMod implements ClientModInitializer { @Override @@ -14,10 +18,26 @@ public void onInitializeClient() { KeyBindingHelper.registerKeyBinding(togglePlacementKey); KeyBindingHelper.registerKeyBinding(previewSnitchFieldKey); KeyBindingHelper.registerKeyBinding(toggleSnitchGoneStatusKey); + KeyBindingHelper.registerKeyBinding(jalistAutoKey); ClientTickEvents.START_CLIENT_TICK.register(client -> { try { handleTick(); + + // Check for J key press while in JAList GUI + var mc = Minecraft.getInstance(); + if (mc.screen instanceof AbstractContainerScreen containerScreen) { + String title = containerScreen.getTitle().getString(); + if ((title.toLowerCase().contains("snitches") || title.contains("JukeAlert")) + && GLFW.glfwGetKey(mc.getWindow().getWindow(), GLFW.GLFW_KEY_J) == GLFW.GLFW_PRESS) { + + // Prevent spam clicking by checking if auto-paginator is not already active + if (!JalistAutoPaginator.getInstance().isActive()) { + System.out.println("[FabricSnitchMod] J key detected in JAList!"); + JalistAutoPaginator.getInstance().startAutoPagination(); + } + } + } } catch (Exception e) { e.printStackTrace(); }