package com.gameminers.glasspane.internal; import gminers.glasspane.GlassPane; import gminers.glasspane.GlassPaneMirror; import gminers.glasspane.ease.PaneEaser; import gminers.glasspane.event.KeyTypedEvent; import gminers.glasspane.event.MouseDownEvent; import gminers.glasspane.event.MouseUpEvent; import gminers.glasspane.event.MouseWheelEvent; import gminers.glasspane.event.PaneDisplayEvent; import gminers.glasspane.event.PaneOverrideEvent; import gminers.glasspane.exception.PaneCantContinueError; import java.lang.reflect.Field; import java.nio.ByteBuffer; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import lombok.SneakyThrows; import lombok.extern.log4j.Log4j2; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiIngame; import net.minecraft.client.gui.ScaledResolution; import net.minecraftforge.client.event.GuiOpenEvent; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.fml.common.FMLCommonHandler; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.common.Mod.EventHandler; import net.minecraftforge.fml.common.Mod.Instance; import net.minecraftforge.fml.common.event.FMLInitializationEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; import net.minecraftforge.fml.common.gameevent.TickEvent; import org.apache.logging.log4j.Logger; import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; import com.google.common.collect.Lists; import com.google.common.collect.Maps; /** * An internal class used by Glass Pane to listen to events from Forge.<br/> * As this is an internal class, fields and methods are likely to change without notice and without compatibility layers. Avoid directly * using this class if you can. * * @author Aesen Vismea * */ @Mod(name = "Glass Pane", modid = "GlassPane", version = "1.1.1 `Borosilicate' Beta", dependencies = "required-after:KitchenSink") @Log4j2 public class GlassPaneMod { @Instance("GlassPane") public static GlassPaneMod inst; public final Map<Class<?>, GlassPane> overrides = Maps.newHashMap(); public final Map<Class<?>, List<GlassPane>> overlays = Maps.newHashMap(); public final List<GlassPane> currentOverlays = Lists.newCopyOnWriteArrayList(); public final List<GlassPane> currentStickyOverlays = Lists.newCopyOnWriteArrayList(); public final List<Class<?>> overrideExemptions = Lists.newCopyOnWriteArrayList(); public static Map<Object, PaneEaser> easers = Collections.synchronizedMap(new HashMap<Object, PaneEaser>()); public static boolean invertMouseCoordinates = false; @EventHandler public void init(final FMLInitializationEvent e) { FMLCommonHandler.instance().bus().register(this); MinecraftForge.EVENT_BUS.register(this); overrideExemptions.add(GuiIngame.class); try { mouseReadBuffer = mouseClass.getDeclaredField("readBuffer"); keyboardReadBuffer = keyboardClass.getDeclaredField("readBuffer"); mouseDWheel = mouseClass.getDeclaredField("dwheel"); mouseReadBuffer.setAccessible(true); keyboardReadBuffer.setAccessible(true); mouseDWheel.setAccessible(true); } catch (NoSuchFieldException e1) { e1.printStackTrace(); } catch (SecurityException ex) { if (System.getSecurityManager() == null) { GlassPaneMod.inst .getLog() .error("[GlassPane] [EventSystem] A SecurityException was thrown, but there's no SecurityManager registered..."); } else throw new PaneCantContinueError("Security manager (" + System.getSecurityManager().getClass().getName() + ") prevents proper collection of input events by GlassPane!", ex); } } public Logger getLog() { return log; } /** * Gets called by Forge whenever the current GuiScreen changes. * This method looks at all registered auto overlays and auto overrides, and applies them. * Narrower class specs are given priority - an override for GuiMainMenu will be preferred over one for GuiScreen. * For overlays, narrower class specs are put at the top of the stack, and broader at the bottom. * * If you need control over where your overlay is inserted into the stack, listen for a DisplayEvent on the relevant GlassPane and * directly access currentOverlays. */ @SubscribeEvent public void onGuiShown(final GuiOpenEvent e) { invertMouseCoordinates = false; // first, clear the current overlays. for (GlassPane gp : currentOverlays) { gp.hide(); // we call hide instead of just clearing the list so the panes can do proper cleanup } // save the gui into a var, as we might be replacing it Object orig = e.gui; if (orig == null) { // if it's null, we're going into a game orig = Minecraft.getMinecraft().ingameGUI; } if (orig instanceof GlassPaneMirror) { // if it's a mirror, unwrap it orig = ((GlassPaneMirror) orig).getMirrored(); } // find all applicable overrides final List<Entry<Class<?>, GlassPane>> possibleOverrides = Lists.newArrayList(); boolean exempted = false; for (final Class<?> clazz : overrideExemptions) { if (clazz.isInstance(orig)) { exempted = true; break; } } if (!exempted) { // don't allow overriding exempted UIs for (final Entry<Class<?>, GlassPane> override : overrides.entrySet()) { if (override.getKey().isInstance(orig)) { if (override.getValue().getClass().isInstance(orig)) { continue; // don't get caught in a loop } possibleOverrides.add(override); } } } // create a hierarchy, with Object having the highest index final List<Class<?>> hierarchy = Lists.newArrayList(); Class<?> work = orig.getClass(); while (true) { hierarchy.add(work); if (work.getSuperclass() == null) { break; } work = work.getSuperclass(); } // create a comparator using the hierarchy we created final Comparator<Entry<Class<?>, ?>> classComparator = new Comparator<Entry<Class<?>, ?>>() { @Override public int compare(final Entry<Class<?>, ?> o1, final Entry<Class<?>, ?> o2) { int i1 = hierarchy.indexOf(o1.getKey()); int i2 = hierarchy.indexOf(o2.getKey()); return (i1 < i2) ? -1 : ((i1 == i2) ? 0 : 1); } }; // if we found multiple overrides, do some sort-y stuff if (possibleOverrides.size() > 1) { // sort the overrides so that the first object has the most narrow class specification Collections.sort(possibleOverrides, classComparator); } // and apply index 0, if it exists if (!possibleOverrides.isEmpty()) { e.gui = possibleOverrides.get(0).getValue().getScreenMirror(); final GlassPane pane = possibleOverrides.get(0).getValue(); pane.unsetModality(); pane.fireEvent(PaneDisplayEvent.class, pane); pane.fireEvent(PaneOverrideEvent.class, pane, orig); } // we do the possible overlays list here instead of when we do overrides so that we can properly react to an override being applied Object newGui = e.gui; if (newGui == null) { // if it's null, we're going into a game newGui = Minecraft.getMinecraft().ingameGUI; } if (newGui instanceof GlassPaneMirror) { // if it's a mirror, unwrap it newGui = ((GlassPaneMirror) newGui).getMirrored(); } final List<Entry<Class<?>, List<GlassPane>>> possibleOverlays = Lists.newArrayList(); for (final Entry<Class<?>, List<GlassPane>> overlay : overlays.entrySet()) { if (overlay.getKey().isInstance(orig) || overlay.getKey().isInstance(newGui)) { possibleOverlays.add(overlay); } } final List<GlassPane> applyingOverlays = Lists.newArrayList(); // sort the overlays so that the first object has the most narrow class specification Collections.sort(possibleOverlays, Collections.reverseOrder(classComparator)); // flatten the "map" for (final Entry<Class<?>, List<GlassPane>> overlay : possibleOverlays) { applyingOverlays.addAll(overlay.getValue()); } // apply the overlays if (applyingOverlays.size() >= 1) { for (final GlassPane overpane : applyingOverlays) { overpane.overlay(); // we call overlay instead of adding to the list directly so the pane can do proper set-up } } } private int touchScreenCounter; private Class<Mouse> mouseClass = Mouse.class; private Class<Keyboard> keyboardClass = Keyboard.class; private Field mouseReadBuffer; private Field mouseDWheel; private Field keyboardReadBuffer; /** * Protip: If you want to avoid having to do the same terrible hackery that is done in this method, * register an autoOverlay(Object) GlassPane that just */ @SubscribeEvent @SneakyThrows public void onTick(final TickEvent.ClientTickEvent e) { // tick the easers Minecraft.getMinecraft().mcProfiler.startSection("paneEaser"); for (PaneEaser pe : GlassPaneMod.easers.values().toArray(new PaneEaser[GlassPaneMod.easers.size()])) { Minecraft.getMinecraft().mcProfiler.startSection(Integer.toHexString(pe.hashCode())); try { pe.onTick(e.phase); } catch (Throwable t) { t.printStackTrace(); System.out.println("Exception while ticking easer " + Integer.toHexString(pe.hashCode())); } Minecraft.getMinecraft().mcProfiler.endSection(); } Minecraft.getMinecraft().mcProfiler.endSection(); if (e.phase == TickEvent.Phase.START) { // this is a terrible, terrible hack, but there's no better way to do it // Mouse and Keyboard InputEvents are only called when in a game, not a GUI, which is the opposite of helpful // so we do this awful hack final Minecraft mc = Minecraft.getMinecraft(); final ScaledResolution res = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); final int width = res.getScaledWidth(); final int height = res.getScaledHeight(); // that wasn't the hack - see the reflection below if (Mouse.isCreated()) { final ByteBuffer buf = (ByteBuffer) mouseReadBuffer.get(null); buf.mark(); while (Mouse.next()) { int mX = Mouse.getEventX() * width / mc.displayWidth; int mY = height - Mouse.getEventY() * height / mc.displayHeight - 1; if (invertMouseCoordinates) { mX = width - mX; mY = height - mY; } int button = Mouse.getEventButton(); if (Minecraft.isRunningOnMac && button == 0 && (Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL))) { button = 1; } if (Mouse.getEventButtonState()) { if (mc.gameSettings.touchscreen && touchScreenCounter++ > 0) return; for (final GlassPane pane : combine(currentOverlays, currentStickyOverlays)) { pane.fireEvent(MouseDownEvent.class, pane, mX, mY, button); } } else if (button != -1) { if (mc.gameSettings.touchscreen && --touchScreenCounter > 0) return; if (mc.currentScreen instanceof GlassPaneMirror) { final GlassPane pane = ((GlassPaneMirror) mc.currentScreen).getMirrored(); pane.fireEvent(MouseUpEvent.class, pane, mX, mY, button); } for (final GlassPane pane : combine(currentOverlays, currentStickyOverlays)) { pane.fireEvent(MouseUpEvent.class, pane, mX, mY, button); } } } if (Mouse.hasWheel()) { final int wheel = Mouse.getDWheel(); if (wheel != 0) { final int mX = Mouse.getX() * width / mc.displayWidth; final int mY = height - Mouse.getY() * height / mc.displayHeight - 1; if (mc.currentScreen instanceof GlassPaneMirror) { final GlassPane pane = ((GlassPaneMirror) mc.currentScreen).getMirrored(); pane.fireEvent(MouseWheelEvent.class, pane, mX, mY, wheel); } for (final GlassPane pane : combine(currentOverlays, currentStickyOverlays)) { pane.fireEvent(MouseWheelEvent.class, pane, mX, mY, wheel); } mouseDWheel.set(null, wheel); } } buf.reset(); } if (Keyboard.isCreated()) { final ByteBuffer buf = (ByteBuffer) keyboardReadBuffer.get(null); buf.mark(); while (Keyboard.next()) { if (Keyboard.getEventKeyState()) { final int kCode = Keyboard.getEventKey(); final char kChar = Keyboard.getEventCharacter(); for (final GlassPane pane : combine(currentOverlays, currentStickyOverlays)) { pane.fireEvent(KeyTypedEvent.class, pane, kChar, kCode); } } } buf.reset(); } } else if (e.phase == TickEvent.Phase.END) { // get the minecraft instance final Minecraft mc = Minecraft.getMinecraft(); // get the resolution final ScaledResolution res = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); // store width and height for convenience final int width = res.getScaledWidth(); final int height = res.getScaledHeight(); // reset mouse's dWheel Mouse.getDWheel(); // iterate through the current overlays for (final GlassPane pane : combine(currentOverlays, currentStickyOverlays)) { // if the pane's width is out of sync, sync it if (pane.getWidth() != width) { pane.setWidth(width); } // if the pane's height is out of sync, sync it if (pane.getHeight() != height) { pane.setHeight(height); } // tick the pane pane.tick(); // tick the pane's shadowbox, if present if (pane.getShadowbox() != null) { pane.getShadowbox().tick(); } } } } @SubscribeEvent public void onRender(final TickEvent.RenderTickEvent e) { if (e.phase == TickEvent.Phase.END) { final Minecraft mc = Minecraft.getMinecraft(); final ScaledResolution res = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); // just render all the overlays in insertion order for (final GlassPane pane : combine(currentOverlays, currentStickyOverlays)) { if (Minecraft.getMinecraft().gameSettings.hideGUI && !pane.isRenderedWhenHUDIsOff()) { continue; } mc.entityRenderer.setupOverlayRendering(); // have to do weird maths with the mouse stuff because Minecraft's 0, 0 is top-left, // but GL/LWJGL's 0, 0 is bottom-left, and since Minecraft does resolution scaling int mouseX = Mouse.getX() * res.getScaledWidth() / mc.displayWidth; int mouseY = res.getScaledHeight() - Mouse.getY() * res.getScaledHeight() / mc.displayHeight - 1; if (invertMouseCoordinates) { mouseX = res.getScaledWidth() - mouseX; mouseY = res.getScaledHeight() - mouseY; } pane.render(mouseX, mouseY, e.renderTickTime); } } } private <T> List<T> combine(final List<T> a, final List<T> b) { final List<T> list = Lists.newArrayList(); list.addAll(a); list.addAll(b); return list; } }