wrath.client.Game.java Source code

Java tutorial

Introduction

Here is the source code for wrath.client.Game.java

Source

/**
 *  Wrath Engine 
 *  Copyright (C) 2015  Trent Spears
 *
 *  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 <http://www.gnu.org/licenses/>.
 */
package wrath.client;

import wrath.client.enums.WindowState;
import wrath.client.enums.RenderMode;
import wrath.client.input.InputManager;
import wrath.client.events.GameEventHandler;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.imageio.ImageIO;
import org.lwjgl.BufferUtils;
import org.lwjgl.Version;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWErrorCallback;
import org.lwjgl.glfw.GLFWFramebufferSizeCallback;
import org.lwjgl.glfw.GLFWVidMode;
import org.lwjgl.openal.AL;
import org.lwjgl.openal.ALContext;
import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL13;
import org.lwjgl.system.MemoryUtil;
import org.lwjgl.util.vector.Matrix4f;
import wrath.client.enums.ImageFormat;
import wrath.client.events.InputEventHandler;
import wrath.client.graphics.Camera;
import wrath.client.graphics.Color;
import wrath.client.graphics.EntityRenderer;
import wrath.client.graphics.Model;
import wrath.client.graphics.ShaderProgram;
import wrath.client.graphics.TextRenderer;
import wrath.client.graphics.TileRenderer;
import wrath.common.Closeable;
import wrath.common.Reloadable;
import wrath.common.entities.Player;
import wrath.common.javaloader.JarLoader;
import wrath.common.scheduler.Scheduler;
import wrath.common.scripts.ScriptManager;
import wrath.util.Config;
import wrath.util.Logger;

/**
 * The entry point and base of the game. Make a class extending this and overriding at least render() method.
 * @author Trent Spears
 */
public class Game {
    private static Game GAME_INSTANCE = null;

    /**
     * Gets the current, primary {@link wrath.client.Game} instance.
     * @return Returns the current, primary {@link wrath.client.Game} instance.
     */
    public static Game getCurrentInstance() {
        return GAME_INSTANCE;
    }

    private final RenderMode MODE;
    private final String TITLE;
    private final double TPS;
    private final String VERSION;

    private final Config gameConfig = new Config(new File("etc/configs/game.cfg"));
    private final Logger gameLogger = new Logger(new File("etc/logs/game.log"));
    private final Scheduler gameScheduler = new Scheduler();

    private GLFWErrorCallback errStr;
    private GLFWFramebufferSizeCallback winSizeStr;

    private ALContext audiocontext;
    private boolean isRunning = false;

    private final EventManager evManager;
    private final InputManager inpManager;
    private final RenderManager renManager;
    private final WindowManager winManager;

    private final RefreshManager refresher;
    private final TrashCollector trashCollector;

    private final Player player;
    private final Camera playerCamera;

    /**
     * Constructor.
     * Describes all the essential and unmodifiable variables of the Game.
     * @param gameTitle Title of the Game.
     * @param version Version of the Game.
     * @param ticksPerSecond The amount of times the printlnic of the game should update in one second. Recommended 30-60.
     * @param renderMode Describes how to game should be rendered (2D or 3D).
     */
    public Game(String gameTitle, String version, double ticksPerSecond, RenderMode renderMode) {
        MODE = renderMode;
        TITLE = gameTitle;
        VERSION = version;
        TPS = ticksPerSecond;
        duringConstructor();
        this.refresher = new RefreshManager();
        this.trashCollector = new TrashCollector();
        this.player = new Player();
        this.playerCamera = new Camera(player);
        this.evManager = new EventManager();
        this.inpManager = new InputManager();
        this.renManager = new RenderManager();
        this.winManager = new WindowManager();

        File nativeDir = new File("assets/native");
        if (!nativeDir.exists()) {
            ClientUtils.throwInternalError("Missing assets folder! Try re-downloading!", true);
            stopImpl();
        }
    }

    /**
    * Adds a {@link wrath.common.Reloadable} object to be run after the window opens.
    * Note that the window opening is NOT the start of the program.
    * @param obj The {@link wrath.common.Reloadable} object to run after the window opens.
    */
    public void addToRefreshList(Reloadable obj) {
        refresher.list.add(obj);
    }

    /**
    * Adds a {@link wrath.common.Closeable} object to be run after the window closes.
    * Note that the window closing is NOT the end of the program.
    * @param obj The {@link wrath.common.Closeable} object to run after the window closes.
    */
    public void addToTrashCleanup(Closeable obj) {
        trashCollector.list.add(obj);
    }

    private void duringConstructor() {
        GAME_INSTANCE = this;
    }

    /**
     * Gets the {@link wrath.util.Config} object of the game.
     * @return Returns the configuration object of the game.
     */
    public Config getConfig() {
        return gameConfig;
    }

    /**
     * Gets the {@link wrath.client.Game.EventManager} class that manages all event handlers.
     * This class is used to control, access and change Event Handlers from the {@link wrath.client.events} package.
     * @return Returns the {@link wrath.client.Game.EventManager} class that manages all event handlers.
     */
    public EventManager getEventManager() {
        return evManager;
    }

    /**
     * Gets the {@link wrath.client.input.InputManager} linked to this {@link wrath.client.Game} instance.
     * @return Returns the {@link wrath.client.input.InputManager} linked to this {@link wrath.client.Game} instance.
     */
    public InputManager getInputManager() {
        return inpManager;
    }

    /**
     * Gets the standard info {@link wrath.util.Logger} for the game.
     * @return Returns the standard info {@link wrath.util.Logger} for the game.
     */
    public Logger getLogger() {
        return gameLogger;
    }

    /**
     * Gets the {@link wrath.common.entities.Player} object representing the player!
     * @return Returns the {@link wrath.common.entities.Player} object representing the player!
     */
    public Player getPlayer() {
        return player;
    }

    /**
     * Gets the {@link wrath.client.graphics.Camera} associated with the player.
     * @return Returns the {@link wrath.client.graphics.Camera} associated with the player.
     */
    public Camera getPlayerCamera() {
        return playerCamera;
    }

    /**
     * Gets the renderer (as specified by the {@link wrath.client.Game.RenderManager} class) for this game.
     * @return Returns the renderer (as specified by the {@link wrath.client.Game.RenderManager} class) for this game.
     */
    public RenderManager getRenderer() {
        return renManager;
    }

    /**
     * Gets whether the game should be rendered in 2D or 3D.
     * @return Returns the enumerator-style representation of the game's rendering mode.
     */
    public RenderMode getRenderMode() {
        return MODE;
    }

    /**
     * Gets the standardized {@link wrath.common.scheduler.Scheduler} for the game.
     * @return Returns the scheduler for the game.
     */
    public Scheduler getScheduler() {
        return gameScheduler;
    }

    /**
     * Gets the title/name of the Game.
     * @return Returns the title of the Game.
     */
    public String getTitle() {
        return TITLE;
    }

    /**
     * Gets the amount of times the game's printlnic will update in one second.
     * Recommended to not be over 64 or under 10.
     * If the TPS is set over 60 and VSync is on, the ticks will be forced to 60 TPS.
     * Unfortunately, there are not any good ways to overcome said bug, though I am looking into potential solutions.
     * @return Returns the Ticks-per-second of the game's printlnic.
     */
    public double getTPS() {
        return TPS;
    }

    /**
     * Gets the {@link java.lang.String} representation of the Game's Version.
     * @return Returns the version of the game in a {@link java.lang.String} format.
     */
    public String getVersion() {
        return VERSION;
    }

    /**
     * Gets the {@link wrath.client.Game.WindowManager} linked to this {@link wrath.client.Game} instance.
     * @return Returns the {@link wrath.client.Game.WindowManager} linked to this {@link wrath.client.Game} instance.
     */
    public WindowManager getWindowManager() {
        return winManager;
    }

    /**
     * Returns whether or not the game is currently running.
     * @return Returns true if the game is running, otherwise false.
     */
    public boolean isRunnning() {
        return isRunning;
    }

    /**
     * Loads a java plugin from the specified file.
     * @param jarFile The {@link java.io.File} to load the plugin from.
     * @return Returns the object formed from the Java plugin.
     */
    public Object loadJavaPlugin(File jarFile) {
        Object obj = JarLoader.loadObject(jarFile);
        this.getEventManager().getGameEventHandler().onLoadJavaPlugin(obj);
        return obj;
    }

    /**
     * Private loop (main game loop).
     */
    private void loop() {
        // FPS counter.
        int afpsCount = 0;
        int fpsCount = 0;

        //Input Checking
        int inpCount = 0;
        double checksPerSec = gameConfig.getDouble("PersistentInputChecksPerSecond", 0.0);
        if (checksPerSec > TPS || checksPerSec < 1)
            checksPerSec = TPS;
        final double INPUT_CHECK_TICKS = TPS / checksPerSec;

        //Timings
        long last = System.nanoTime();
        final double conv = 1000000000.0 / TPS;
        double delta = 0.0;
        long now;

        while (isRunning
                && (!winManager.windowOpen || GLFW.glfwWindowShouldClose(winManager.window) != GL11.GL_TRUE)) {
            now = System.nanoTime();
            delta += (now - last) / conv;
            last = now;

            //Tick occurs
            while (delta >= 1) {
                onTickPreprocessor();

                //Persistent input management
                if (INPUT_CHECK_TICKS == 1 || inpCount >= INPUT_CHECK_TICKS) {
                    inpManager.onPersistentInput();
                    inpCount -= INPUT_CHECK_TICKS;
                } else
                    inpCount++;

                //FPS Counter
                if (winManager.windowOpen)
                    if (fpsCount >= TPS) {
                        afpsCount++;
                        renManager.fps = renManager.fpsBuf;
                        renManager.avgFps = renManager.totalFramesRendered / afpsCount;
                        renManager.fpsBuf = 0;
                        fpsCount -= TPS;
                    } else
                        fpsCount++;

                delta--;
            }

            renManager.render();
        }

        stop();
        stopImpl();
    }

    /**
     * Private method that takes care of all background processes before onTick() is called.
     */
    private void onTickPreprocessor() {
        gameScheduler.onTick();
        evManager.getGameEventHandler().onTick();
    }

    /**
     * Removes a {@link wrath.common.Reloadable} object from the refresh list.
     * @param obj The {@link wrath.common.Reloadable} object to remove from refresh list.
     */
    public void removeFromRefreshList(Reloadable obj) {
        refresher.list.remove(obj);
    }

    /**
     * Removes a {@link wrath.common.Closeable} object from the cleanup list.
     * @param obj The {@link wrath.common.Closeable} object to remove from cleanup list.
     */
    public void removeFromTrashCleanup(Closeable obj) {
        trashCollector.list.remove(obj);
    }

    /**
     * Override-able method that is called as much as possible to issue rendering commands.
     */
    protected void render() {
    }

    /**
     * Method that is used to load the game and all of it's resources.
     */
    public void start() {
        start(new String[0]);
    }

    /**
     * Method that is used to load the game and all of it's resources.
     * @param args Arguments, usually from the main method (entry point).
     */
    public void start(String[] args) {
        gameLogger.println(
                "Launching '" + TITLE + "' Client v." + VERSION + "  with LWJGL v." + Version.getVersion() + "!");

        //Initialize GLFW and OpenGL
        GLFW.glfwSetErrorCallback((errStr = new GLFWErrorCallback() {
            @Override
            public void invoke(int error, long description) {
                System.err.println("GLFW hit ERROR ID '" + error + "' with message '" + description + "'!");
            }
        }));

        if (GLFW.glfwInit() != GL11.GL_TRUE) {
            System.err.println("Could not initialize GLFW! Unknown Error!");
            ClientUtils.throwInternalError("Failed to initialize GLFW!", false);
            stopImpl();
        }

        //Interpret command-line arguments.
        for (String a : args) {
            String[] b = a.split("=", 2);
            if (b.length <= 1)
                continue;

            gameConfig.setProperty(b[0], b[1]);
            gameLogger.println("Set property '" + b[0] + "' to value '" + b[1] + "'!");
        }

        //Auto-loads Java Plugins from specified directory.
        if (gameConfig.getBoolean("AutoLoadJavaPlugins", true)) {
            Object[] list = JarLoader.loadPluginsDirectory(
                    new File(gameConfig.getString("AutoLoadJavaPluginsDirectory", "etc/plugins")));
            for (Object obj : list)
                evManager.getGameEventHandler().onLoadJavaPlugin(obj);
            if (list.length != 0)
                gameLogger.println("Loaded " + list.length + " plugins from the directory '"
                        + gameConfig.getString("AutoLoadJavaPluginsDirectory", "etc/plugins") + "'!");
        }

        isRunning = true;
        winManager.openWindow();
        evManager.getGameEventHandler().onGameOpen();
        inpManager.loadKeys();
        loop();
    }

    /**
     * Method that flags the game to stop.
     */
    public void stop() {
        if (!isRunning)
            return;
        evManager.getGameEventHandler().onGameClose();
        isRunning = false;
    }

    /**
     * Method to stop the game and close all of it's resources.
     */
    private void stopImpl() {
        try {
            winManager.closeWindow();
            inpManager.destroyCursor();
            GLFW.glfwTerminate();

            gameConfig.save();
            inpManager.saveKeys();
            gameLogger.println("Average FPS throughout session: " + renManager.avgFps);
            gameLogger.println("Time of Session: "
                    + (double) ((double) (System.nanoTime() - EntryPoint.UNIX_START_TIMESTAMP) / 1000 / 1000 / 1000)
                    + " seconds.");
            gameLogger.println("Stopping '" + TITLE + "' Client v." + VERSION + "!");
            if (gameLogger != null && !gameLogger.isClosed())
                gameLogger.close();
            errStr.release();
        } catch (Exception e) {
        }

        ScriptManager.closeScripting();

        System.exit(0);
    }

    /**
     * Class to manage all event handlers from the {@link wrath.client.events} package.
     */
    public class EventManager {
        /**
         * Constructor.
         * Protected so multiple instances aren't made pointlessly.
         */
        private EventManager() {
        }

        private final ArrayList<GameEventHandler> gameHandlers = new ArrayList<>();
        private final ArrayList<InputEventHandler> inpHandlers = new ArrayList<>();

        private final RootGameEventHandler ghan = new RootGameEventHandler();
        private final RootInputEventHandler ihan = new RootInputEventHandler();

        /**
         * Adds a {@link wrath.client.events.GameEventHandler} to associate with this Game.
         * @param handler The {@link wrath.client.events.GameEventHandler} to add to the list of handlers that handles all of this Game's events.
         */
        public void addGameEventHandler(GameEventHandler handler) {
            gameHandlers.add(handler);
        }

        /**
         * Adds a {@link wrath.client.events.InputEventHandler} to associate with this Game's Input Manager.
         * @param handler The {@link wrath.client.events.InputEventHandler} to add to the list of handlers that handles all of this Game's Input events.
         */
        public void addInputEventHandler(InputEventHandler handler) {
            inpHandlers.add(handler);
        }

        /**
         * Gets the root {@link wrath.client.events.GameEventHandler} linked to this Game.
         * @return Returns the root {@link wrath.client.events.GameEventHandler} linked to this Game.
         */
        public GameEventHandler getGameEventHandler() {
            return ghan;
        }

        /**
         * Gets the root {@link wrath.client.events.InputEventHandler}s linked to this Game's Input Manager.
         * @return Returns the root {@link wrath.client.events.GameEventHandler} linked to this Game's Input Manager.
         */
        public InputEventHandler getInputEventHandler() {
            return ihan;
        }
    }

    /**
     * Class to define Graphical User Interface (GUI) of the game.
     * This *will* include method to control pop-ups, sub-windows, etc.
     */
    public class GUI {
        /**
         * Constructor.
         * Protected so multiple instances aren't made pointlessly.
         */
        private GUI() {
        }

        /**
         * Method to render the GUI defined by the class.
         */
        private void renderGUI() {
            //This is just the outline of what is to come.
        }
    }

    private class RefreshManager {
        private final ArrayList<Reloadable> list = new ArrayList<>();

        public void run() {
            list.stream().forEach((r) -> {
                r.reload();
            });
        }
    }

    public class RenderManager {
        public static final float FAR_PLANE = 1000f;
        public static final float NEAR_PLANE = 0.1f;

        private int avgFps = 0;
        private Color color = Color.WHITE;
        private float fov = gameConfig.getFloat("FOV", 70f);
        private int fps = 0;
        private int fpsBuf = 0;
        private final GUI front = new GUI();
        private int maxFps = getConfig().getInt("MaxFps", 0);
        private Matrix4f projMatrix = new Matrix4f();
        private boolean renderFps = false;
        private TextRenderer text = null;
        private int totalFramesRendered = 0;

        private boolean shouldRender = true;
        private long next = 0;

        private final HashMap<Model, List<EntityRenderer>> entityRenderMap = new HashMap<>();
        private final HashMap<Model, List<TileRenderer>> terrainRenderMap = new HashMap<>();

        private RenderManager() {
        }

        /**
         * Efficiently renders an Entity.
         * @param ren The {@link wrath.client.graphics.EntityRenderer} to render.
         */
        public void addEntityRenderingJob(EntityRenderer ren) {
            if (entityRenderMap.containsKey(ren.getModel()))
                entityRenderMap.get(ren.getModel()).add(ren);
            else {
                ArrayList<EntityRenderer> rl = new ArrayList<>();
                rl.add(ren);
                entityRenderMap.put(ren.getModel(), rl);
            }
        }

        /**
         * Efficiently renders a Tile of Terrain.
         * @param ren The {@link wrath.client.graphics.TileRenderer} to render.
         */
        public void addTerrainRenderingJob(TileRenderer ren) {
            if (terrainRenderMap.containsKey(ren.getTileModel()))
                terrainRenderMap.get(ren.getTileModel()).add(ren);
            else {
                ArrayList<TileRenderer> rl = new ArrayList<>();
                rl.add(ren);
                terrainRenderMap.put(ren.getTileModel(), rl);
            }
        }

        /**
         * Gets the average FPS of the game while it has been running.
         * @return Returns the average FPS of the game while it has been running.
         */
        public double getAverageFPS() {
            return avgFps;
        }

        /**
         * Gets the defined 3D Field-of-View.
         * @return Returns the defined 3D Field-of-View.
         */
        public float getFOV() {
            return fov;
        }

        /**
        * Gets the last recorded Frames-Per-Second count.
        * @return Returns the last FPS count.
        */
        public double getFPS() {
            return fps;
        }

        /**
         * Gets the {@link wrath.client.Game.GUI} linked to this Window.
         * @return Returns the {@link wrath.client.Game.GUI} linked to this Window.
         */
        public GUI getGUI() {
            return front;
        }

        /**
         * Gets the maximum amount of times the game will render to the screen in one second.
         * @return Returns the maximum amount of times the game will render to the screen in one second.
         */
        public int getMaxFPS() {
            return maxFps;
        }

        /**
         * Gets the {@link org.lwjgl.util.vector.Matrix4f} object of the 3D projection matrix.
         * @return Returns the {@link org.lwjgl.util.vector.Matrix4f} object of the 3D projection matrix.
         */
        public Matrix4f getProjectionMatrix() {
            return projMatrix;
        }

        /**
         * Gets the standard {@link wrath.client.graphics.Color} that will be used to render unless specified otherwise by OpenGL code.
         * @return Returns the standard {@link wrath.client.graphics.Color}.
         */
        public Color getStandardColor() {
            return color;
        }

        /**
        * Gets the default global text renderer.
        * @return The current global {@link wrath.client.graphics.TextRenderer}.
        */
        public TextRenderer getTextRenderer() {
            return text;
        }

        /**
         * Gets the amount of frames the game has rendered since it launched.
         * @return Returns the amount of frames the game has rendered since it launched.
         */
        public int getTotalFramesRendered() {
            return totalFramesRendered;
        }

        /**
         * If true, the FPS will be rendered at the top-left of the screen.
         * @return Returns true if the FPS will be rendered in text at the top-left of the screen.
         */
        public boolean isRenderingFPS() {
            return renderFps;
        }

        /**
         * Method called by the loop to render everything needed.
         */
        private void render() {
            if (maxFps > 0) {
                if (next == 0 || System.nanoTime() >= next) {
                    if (next == 0)
                        next = System.nanoTime();
                    next = next + (long) Math.round(((float) 1.0 / maxFps * 1000000000.0));
                    shouldRender = true;
                } else
                    shouldRender = false;
            }

            if (winManager.windowOpen) {
                if (shouldRender) {
                    GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
                    color.bindColor();

                    terrainRenderMap.entrySet().stream().map((entry) -> {
                        Model m = (Model) entry.getKey();
                        m.renderSetup();
                        for (TileRenderer r : (List<TileRenderer>) entry.getValue()) {
                            r.update();
                            m.render(true);
                        }
                        return m;
                    }).forEach((m) -> {
                        m.renderStop();
                    });
                    terrainRenderMap.clear();

                    entityRenderMap.entrySet().stream().forEach((entry) -> {
                        Model m = (Model) entry.getKey();
                        if (!(m == null)) {
                            m.renderSetup();
                            for (EntityRenderer r : (List<EntityRenderer>) entry.getValue()) {
                                r.update();
                                m.render(true);
                            }
                            m.renderStop();
                        }
                    });
                    entityRenderMap.clear();

                    GAME_INSTANCE.render();
                    color.bindColor();
                    front.renderGUI();
                    if (renderFps)
                        text.renderString(fps + "", -1f, 1f, 0.5f, new Color(0.57f, 2.37f, 0.4f));
                    GL11.glFlush();
                    GLFW.glfwSwapBuffers(winManager.window);

                    fpsBuf++;
                    totalFramesRendered++;
                }

                GLFW.glfwPollEvents();
            }
        }

        /**
         * Changes the 3D Field-of-View.
         * @param fov The 3D Field-of-View angle.
         */
        public void setFOV(float fov) {
            this.fov = fov;
            gameConfig.setProperty("FOV", fov);
            winManager.closeWindow();
            winManager.openWindow();
        }

        /**
         * Changes the max FPS the game is allowed to render at.
         * When 0, there is no limit.
         * @param max The maximum number of frames that should be rendered in a second.
         */
        public void setMaxFPS(int max) {
            getConfig().setProperty("MaxFps", max);
            maxFps = max;
        }

        /**
         * Changes the state of whether or not to render the FPS.
         * @param render If true, the FPS will be rendered in text at the top-left of the screen.
         */
        public void setRenderFPS(boolean render) {
            this.renderFps = render;
        }

        /**
         * Sets the standard {@link wrath.client.graphics.Color} that will be used to render unless specified otherwise by OpenGL code.
         * @param color The standard {@link wrath.client.graphics.Color} to set.
         */
        public void setStandardColor(Color color) {
            this.color = color;
        }

        /**
         * Sets the game's global text renderer.
         * @param text The {@link wrath.client.graphics.TextRenderer} to manage text rendering.
         */
        public void setTextRenderer(TextRenderer text) {
            this.text = text;
        }
    }

    private class RootGameEventHandler implements GameEventHandler {
        @Override
        public void onGameClose() {
            evManager.gameHandlers.stream().forEach((handler) -> {
                handler.onGameClose();
            });
        }

        @Override
        public void onGameOpen() {
            evManager.gameHandlers.stream().forEach((handler) -> {
                handler.onGameOpen();
            });
        }

        @Override
        public void onLoadJavaPlugin(Object loadedObject) {
            evManager.gameHandlers.stream().forEach((handler) -> {
                handler.onLoadJavaPlugin(loadedObject);
            });
        }

        @Override
        public void onTick() {
            evManager.gameHandlers.stream().forEach((handler) -> {
                handler.onTick();
            });
        }

        @Override
        public void onWindowOpen() {
            evManager.gameHandlers.stream().forEach((handler) -> {
                handler.onWindowOpen();
            });
        }

        @Override
        public void onResolutionChange(int oldWidth, int oldHeight, int newWidth, int newHeight) {
            evManager.gameHandlers.stream().forEach((handler) -> {
                handler.onResolutionChange(oldWidth, oldHeight, newWidth, newHeight);
            });
        }

    }

    private class RootInputEventHandler implements InputEventHandler {
        @Override
        public void onCharInput(char c) {
            evManager.inpHandlers.stream().forEach((handler) -> {
                handler.onCharInput(c);
            });
        }

        @Override
        public void onCursorMove(double x, double y) {
            evManager.inpHandlers.stream().forEach((handler) -> {
                handler.onCursorMove(x, y);
            });
        }

        @Override
        public void onScroll(double xoffset, double yoffset) {
            evManager.inpHandlers.stream().forEach((handler) -> {
                handler.onScroll(xoffset, yoffset);
            });
        }

    }

    /**
     * Class to manage every 'Closeable' interface in the game.
     */
    private class TrashCollector {
        protected final ArrayList<Closeable> list = new ArrayList<>();

        private TrashCollector() {
        }

        private void run() {
            ArrayList<Closeable> cpy = new ArrayList<>();
            cpy.addAll(list);
            cpy.stream().forEach((c) -> {
                c.close();
            });
        }
    }

    /**
     * Class to manage anything to do with the Game Window.
     */
    public class WindowManager {
        private boolean firstOpen = true;

        private long window;
        private boolean windowOpen = false;
        private WindowState windowState = null;

        private int width = 800;
        private int height = 600;

        /**
         * Constructor.
         * Protected so multiple instances aren't made pointlessly.
         */
        private WindowManager() {
        }

        /**
        * Centers the window in the middle of the designated primary monitor.
        * Does not work in Fullscreen or Fulscreen_Windowed modes.
        */
        public void centerWindow() {
            if (windowState == WindowState.FULLSCREEN || windowState == WindowState.FULLSCREEN_WINDOWED)
                return;

            GLFWVidMode vidmode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor());
            GLFW.glfwSetWindowPos(window, (vidmode.width() / 2) - (width / 2),
                    (vidmode.height() / 2) - (height / 2));
        }

        /**
        * Destroys and deallocates all GLFW/window resources.
        */
        public void closeWindow() {
            if (!windowOpen)
                return;
            windowOpen = false;

            gameLogger.println("Closing window [" + width + "x" + height + "]");

            winSizeStr.release();
            trashCollector.run();
            AL.destroy(audiocontext);
            GLFW.glfwDestroyWindow(window);

            gameConfig.save();
        }

        /**
         * Returns the height of the window.
         * @return Returns the height (in pixels) of the window.
         */
        public int getHeight() {
            return height;
        }

        /**
         * Gets the GLFW Window ID.
         * @return Returns the {@link org.lwjgl.glfw.GLFW} window ID.
         */
        public long getWindowID() {
            return window;
        }

        /**
         * Returns the width of the window.
         * @return Returns the width (in pixels) of the window.
         */
        public int getWidth() {
            return width;
        }

        /**
         * Gets the current state of the window as of
         * {@link wrath.client.enums.WindowState}.
         * @return Returns the current state of the window.
         */
        public WindowState getWindowState() {
            return windowState;
        }

        /**
         * Tells whether or not the window is open.
         * @return Returns true if the window is open, otherwise false.
         */
        public boolean isWindowOpen() {
            return windowOpen;
        }

        /**
         * Force minimizes the window.
         */
        public void minimizeWindow() {
            if (!windowOpen)
                return;
            GLFW.glfwIconifyWindow(window);
        }

        /**
         * Method to start the display. Made independent from start() so window
         * options can be adjusted without restarting game.
         */
        public void openWindow() {
            if (windowOpen)
                return;

            GLFW.glfwDefaultWindowHints();
            GLFW.glfwWindowHint(GLFW.GLFW_DOUBLE_BUFFER,
                    ClientUtils.getOpenGLBoolean(gameConfig.getBoolean("DoubleBuffered", true)));
            GLFW.glfwWindowHint(GLFW.GLFW_STEREO,
                    ClientUtils.getOpenGLBoolean(gameConfig.getBoolean("RenderStereostopic", false)));
            GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GL11.GL_FALSE);
            GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE,
                    ClientUtils.getOpenGLBoolean(gameConfig.getBoolean("WindowResizable", true)));
            GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT,
                    ClientUtils.getOpenGLBoolean(gameConfig.getBoolean("APIForwardCompatMode", false)));
            GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_DEBUG_CONTEXT,
                    ClientUtils.getOpenGLBoolean(gameConfig.getBoolean("DebugMode", false)));
            GLFW.glfwWindowHint(GLFW.GLFW_SAMPLES, gameConfig.getInt("AntiAliasingSamples", 8));
            GLFW.glfwWindowHint(GLFW.GLFW_REFRESH_RATE, gameConfig.getInt("DisplayRefreshRate", 0));

            windowState = WindowState
                    .valueOf(gameConfig.getString("WindowState", "fullscreen_windowed").toUpperCase());

            if (windowState == WindowState.FULLSCREEN) {
                GLFWVidMode videomode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor());
                if (!gameConfig.getBoolean("FullscreenUsesResolution", false)) {
                    gameConfig.setProperty("Width", videomode.width());
                    gameConfig.setProperty("Height", videomode.height());
                    width = videomode.width();
                    height = videomode.height();
                }

                window = GLFW.glfwCreateWindow(videomode.width(), videomode.height(), TITLE,
                        GLFW.glfwGetPrimaryMonitor(), MemoryUtil.NULL);
            } else if (windowState == WindowState.FULLSCREEN_WINDOWED) {
                GLFWVidMode videomode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor());
                width = videomode.width();
                height = videomode.height();

                GLFW.glfwWindowHint(GLFW.GLFW_DECORATED, GL11.GL_FALSE);
                window = GLFW.glfwCreateWindow(width, height, TITLE, MemoryUtil.NULL, MemoryUtil.NULL);
            } else if (windowState == WindowState.WINDOWED) {
                width = gameConfig.getInt("Width", 800);
                height = gameConfig.getInt("Height", 600);
                window = GLFW.glfwCreateWindow(width, height, TITLE, MemoryUtil.NULL, MemoryUtil.NULL);
            } else if (windowState == WindowState.WINDOWED_UNDECORATED) {
                GLFW.glfwWindowHint(GLFW.GLFW_DECORATED, GL11.GL_FALSE);

                width = gameConfig.getInt("Width", 800);
                height = gameConfig.getInt("Height", 600);
                window = GLFW.glfwCreateWindow(width, height, TITLE, MemoryUtil.NULL, MemoryUtil.NULL);
            }

            if (window == MemoryUtil.NULL) {
                System.err.println("Could not initialize window! Window Info[" + width + "x" + height + "]");
                ClientUtils.throwInternalError("Window failed to initialize!", false);
                stopImpl();
            }

            gameLogger.println("Opened window [" + width + "x" + height + "] in "
                    + windowState.toString().toUpperCase() + " mode.");

            inpManager.openInput();

            GLFW.glfwMakeContextCurrent(window);
            if (gameConfig.getBoolean("DisplayVsync", false))
                GLFW.glfwSwapInterval(1);
            else
                GLFW.glfwSwapInterval(0);
            GL.createCapabilities();

            audiocontext = ALContext.create();
            audiocontext.makeCurrent();

            GLFW.glfwSetFramebufferSizeCallback(window, (winSizeStr = new GLFWFramebufferSizeCallback() {
                @Override
                public void invoke(long window, int width, int height) {
                    if (width <= 0 || height <= 0)
                        return;

                    int ow = winManager.width;
                    int oh = winManager.height;
                    winManager.width = width;
                    winManager.height = height;

                    gameConfig.setProperty("Width", width);
                    gameConfig.setProperty("Height", height);
                    GL11.glViewport(0, 0, width, height);
                    evManager.getGameEventHandler().onResolutionChange(ow, oh, width, height);
                }
            }));

            GL11.glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
            GL11.glViewport(0, 0, width, height);
            if (gameConfig.getBoolean("FullscreenUsesResolution", false)
                    && gameConfig.getString("WindowState").equalsIgnoreCase("Fullscreen")) {
                width = gameConfig.getInt("Width", 800);
                height = gameConfig.getInt("Height", 600);
                winManager.setResolution(width, height);
            }
            GL11.glMatrixMode(GL11.GL_PROJECTION);
            GL11.glLoadIdentity();
            GL11.glMatrixMode(GL11.GL_MODELVIEW);
            GL11.glLoadIdentity();
            if (gameConfig.getInt("AntiAliasingSamples", 8) > 0)
                GL11.glEnable(GL13.GL_MULTISAMPLE);
            GL11.glEnable(GL11.GL_CULL_FACE);
            GL11.glCullFace(GL11.GL_BACK);

            if (renManager.text == null)
                renManager.text = new TextRenderer(new File("assets/fonts/arial.png"), 0.75f);

            if (MODE == RenderMode.Mode3D) {
                renManager.projMatrix = ClientUtils.createProjectionMatrix(renManager.fov);
                GL11.glEnable(GL11.GL_DEPTH_TEST);
                GL11.glDepthFunc(GL11.GL_LESS);
            }
            ShaderProgram.DEFAULT_SHADER = ShaderProgram.loadShaderProgram(
                    new File("assets/shaders/defaultshader.vert"), new File("assets/shaders/defaultshader.frag"));
            ShaderProgram.DEFAULT_TERRAIN_SHADER = ShaderProgram.loadShaderProgram(
                    new File("assets/shaders/defaultterrainshader.vert"),
                    new File("assets/shaders/defaultterrainshader.frag"));

            if (firstOpen)
                firstOpen = false;
            else
                refresher.run();

            windowOpen = true;
            evManager.getGameEventHandler().onWindowOpen();
            GLFW.glfwShowWindow(window);
        }

        /**
         * Takes a screen-shot and saves it to the file specified as a PNG.
         * @param saveToName The name of the file to save the screen-shot to (excluding file extension).
         */
        public void screenShot(String saveToName) {
            screenShot(saveToName, ImageFormat.PNG);
        }

        /**
         * Takes a screen-shot and saves it to the file specified.
         * @param saveToName The name of the file to save the screen-shot to (excluding file extension).
         * @param format The format to save the image as.
         */
        public void screenShot(String saveToName, ImageFormat format) {
            GL11.glReadBuffer(GL11.GL_FRONT);
            ByteBuffer buffer = BufferUtils.createByteBuffer(width * height * 4);
            GL11.glReadPixels(0, 0, width, height, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, buffer);

            Thread t = new Thread(() -> {
                File screenshotDir = new File("etc/screenshots");
                if (!screenshotDir.exists())
                    screenshotDir.mkdirs();
                File saveTo = new File("etc/screenshots/" + saveToName + "." + format.name().toLowerCase());
                BufferedImage image = ClientUtils.getByteBufferToImage(buffer, width, height);
                try {
                    ImageIO.write(image, format.name(), saveTo);
                    gameLogger.println("Saved screenshot '" + saveTo.getName() + "'!");
                } catch (IOException e) {
                    System.err.println(
                            "Could not save Screenshot to '" + saveTo.getName() + "'! I/O Error has occured!");
                }
            });

            t.start();
        }

        /**
        * Changes the size of the window.
        * @param width New width of the window, measured in pixels.
        * @param height New height of the window, measures in pixels.
        */
        public void setResolution(int width, int height) {
            this.width = width;
            this.height = height;
            GLFW.glfwSetWindowSize(window, width, height);
            GL11.glViewport(0, 0, width, height);

            gameConfig.setProperty("Width", width);
            gameConfig.setProperty("Height", height);
        }

        /**
        * Changes the state of the window.
        * This method will require the window to restart, and this will be done via {@link wrath.client.Game.WindowManager#closeWindow() } and {@link wrath.client.Game.WindowManager#openWindow() }.
        * @param state The state to set the window to.
        */
        public void setWindowState(WindowState state) {
            gameConfig.setProperty("WindowState", state.toString().toUpperCase());
            if (windowOpen) {
                closeWindow();
                openWindow();
            }
        }
    }
}