package complexion.client;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
import org.lwjgl.LWJGLException;
import org.lwjgl.input.Keyboard;
import org.lwjgl.input.Mouse;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.GL11;
import complexion.common.Config;
import complexion.common.Console;
import complexion.common.Utils;
import complexion.network.message.AtomDelta;
import complexion.network.message.AtomUpdate;
import complexion.network.message.CreateDialog;
import complexion.network.message.DialogSync;
import complexion.network.message.FullAtomUpdate;
import complexion.network.message.InputData;
import complexion.network.message.LoginAccepted;
import complexion.network.message.SendEvent;
import com.esotericsoftware.minlog.Log;
import de.matthiasmann.twl.GUI;
import de.matthiasmann.twl.renderer.lwjgl.LWJGLRenderer;
import de.matthiasmann.twl.theme.ThemeManager;
/**
* Class representing the entire client application, and global
* client state.
*/
public class Client {
/** Maintains a list of all dialogs currently open.
*/
ConcurrentMap<Integer,Dialog> dialogsByUID = new ConcurrentHashMap<Integer,Dialog>();
/** Permanently passing around client instances is very bothersome.
Since in one application, we will have only one client, use a
global instance instead.
**/
public static Client current;
/** TWL topevel GUI instance this client uses for rendering its GUI widgets. **/
GUI gui;
/**
* Client program initialization and loop.
*/
public static void main(String[] args)
{
current = new Client();
// See https://code.google.com/p/minlog/#Log_level
//Log.set(Log.LEVEL_DEBUG);
//new Console();
// Initialize the client window.
try {
current.renderer = new Renderer(1.5);
} catch (LWJGLException e) {
Client.notifyError("Error setting up program window. Exiting.", e);
System.exit(1); // Exit with 1 to signify that an error occured
}
// Build the GUI
LWJGLRenderer guiRenderer;
try {
guiRenderer = new LWJGLRenderer();
current.gui = new GUI(guiRenderer);
// TODO: load the root level theme from a nicer file(in res/, no weird ../../.. paths)
ThemeManager theme = ThemeManager.createThemeManager(
Client.class.getResource("test.xml"), guiRenderer);
current.gui.applyTheme(theme);
} catch (LWJGLException e) {
Client.notifyError("Error setting up GUI instance. Exiting.", e);
System.exit(1);
} catch (IOException e) {
Client.notifyError("Error setting up GUI instance. Exiting.", e);
System.exit(1);
}
// Try to log into a preconfigured server.
// This should become a user dialog later.
try {
current.connection = new ServerConnection(current, InetAddress.getByName("localhost"), 1024);
System.out.println(current.connection.login("head", "password"));
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// Intercept and process AtomDelta's
while(!Display.isCloseRequested())
{
if(current.atomDeltas.size() > 0)
{
Utils.sleep(Config.tickLag / current.atomDeltas.size());
} else
{
Utils.sleep(1);
}
Object msg;
// Check for new messages from the server.
while((msg = current.serverMessages.poll()) != null)
{
current.processMessage(msg);
}
// If we have a tick initialized, that means we're connected,
// so start processing incoming AtomDelta's
if(current.tick != -1)
{
// We will only be processing a single "tick" here. Before consequent ticks are processed,
// control will be given back to the client, which will lead to the updates being rendered, as
// well as a small delay(Config.tickLag) being put in place.
// By doing this, we can make sure there's always a more or less constant delay between ticks,
// which will smoothen out movement, animation and other changes to the map.
// Without this "controlled delay" in processing updates, if the client were to experience
// minor lag spikes (of say 0.2 seconds), what he'd see is a mob moving 0 tiles due to the lag,
// then 2 tiles at once when two packages arrive at once, then 0 tiles, then 2 tiles at once,
// i.e. the "time" would be distorted from the client's point of view
// Get a delta from the queue
AtomDelta delta = current.atomDeltas.poll();
if(delta != null)
{
// whether we're going to process this update
boolean update_relevant = true;
// Check if the update's tick is valid
if(delta.tick <= current.tick)
{
// Update too old, ignore
update_relevant = false;
}
else if(delta.tick > current.tick + 1)
{
// The tick is not the next tick. That's bad, it means we missed something.
// When this occurs, that is an actual bug(as we do not want to miss any updates), so do not
// remove this error, fix your buggy code instead.
System.err.println("Next tick from server is " + delta.tick + ", but client is only at " + current.tick);
}
if(update_relevant)
{
// Process the individual updates
for(AtomUpdate update : delta.updates)
{
current.processAtomUpdate(update);
}
// Skip ahead to the tick we just processed
current.tick = delta.tick;
}
}
}
// Process mouse click events
// TODO: Add scroll events from LWJGLInput.java
while(Mouse.next()) {
// First check whether TWL can handle the event
boolean handledByTWL = current.gui.handleMouse(
Mouse.getEventX(), current.gui.getHeight() - Mouse.getEventY() - 1,
Mouse.getEventButton(), Mouse.getEventButtonState());
// If TWL can't handle the click, and the event was a mouse button press,
// check whether the event is an atom click
if(!handledByTWL && Mouse.getEventButtonState())
{
current.onClick(Mouse.getEventX(),Mouse.getEventY(),Mouse.getEventButton());
}
}
// Check if any keys were pressed.
while(Keyboard.next() && Display.isVisible())
{
int key = Keyboard.getEventKey();
boolean state = Keyboard.getEventKeyState();
if(state == true)
{
// Only send an input message when the button was released
InputData data = new InputData(key);
current.connection.send(data);
}
}
// Handle dialog messages
for(Dialog dialog : Client.current.dialogsByUID.values())
{
Object message = dialog.messageQueue.poll();
while(message != null)
{
// Invoke the Dialog's handler for processing messages
dialog.processMessage(message);
message = dialog.messageQueue.poll();
}
}
// Clear the screen and depth buffer
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
// Re-render the widget
current.renderer.draw();
// Draw the GUI
// TODO: handle mouse and keyboard events where appropriate
current.gui.updateTime();
current.gui.draw();
Display.update();
}
current.renderer.destroy();
}
/**
* Notify the user of an error. This function should always work, even when there's no
* valid LWJGL context.
* @param user_error User-friendly message describing what went wrong.
* @param e Internal stack-trace, useful for debugging.
*/
public static void notifyError(String user_error, Exception e)
{
System.out.println(user_error);
}
/**
*
* Method called when the user clicks
* Calculates the tile position
* @param mouse_x // The screen pixel x location of the click
* @param mouse_y // The screen pixel y location of the click
* @param key // Left Button(0) or Right Button(1)
*/
private void onClick(int mouse_x,int mouse_y,int key)
{
int tile_x = (int)Math.floor(mouse_x/Config.tileWidth); // Give us the tile y cordinate
int tile_y = (int)Math.floor(mouse_y/Config.tileHeight);// Give us the tile x cordinate
int offset_x =mouse_x-(tile_x*Config.tileWidth) ;// calculate what pixel inside the tile
int offset_y = mouse_y-(tile_y*Config.tileWidth); // calculate what pixel inside the tile
Atom clicked_atom = null;
// I have to iterate from the end due that its sorted by layer
for(int x=renderer.atoms.size()-1;x>=0;x--)
{
Atom A = renderer.atoms.get(x); // Get the atom
if(A.tile_x == tile_x && A.tile_y == tile_y) // Check if atom is in the tile
{
if(!A.isTransparent(offset_x,offset_y)) // Check if said pixel is transparent.
{
clicked_atom = A;
break;
}
}
}
// TODO: Debug code remove.
if(clicked_atom != null)
{
System.err.println("Clicked "+clicked_atom.sprite_state);
}
// TODO: send event to server
}
/**
* Process a single AtomUpdate type object to add it to the map/add a new object.
* @param data The AtomUpdate to process.
*/
private void processAtomUpdate(AtomUpdate data)
{
// Check if the atom already exists
boolean exists = atomsByUID.containsKey(data.UID);
// Check the type of the AtomUpdate
if(data instanceof FullAtomUpdate)
{
FullAtomUpdate full = (FullAtomUpdate) data;
if(exists)
{
// Atom already exists, update it
Atom old = atomsByUID.get(full.UID);
// Load the entire data of the atom update into the existing atom
full.updateClientAtom(old);
}
else
{
// The atom doesn't exist yet, create it
Atom atom = new Atom();
full.updateClientAtom(atom);
// Add the created atom to our atom cache, and also
// to the renderer.
atomsByUID.put(data.UID, atom);
renderer.addAtom(atom);
}
}
renderer.sortLayers();
}
/** Process a message from the server. **/
private void processMessage(Object message)
{
if(message instanceof AtomDelta)
{
// If it's an AtomDelta, put it into client.atomDeltas and
// allow the main thread to process it. We'll be sorting it
// into its specified tick.
AtomDelta delta = (AtomDelta) message;
this.atomDeltas.add(delta);
}
else if(message instanceof CreateDialog)
{
// If it's a CreateDialog, try to create the specified dialog.
CreateDialog create = (CreateDialog) message;
System.out.println(create.classID);
try {
// We're creating the class from a string, this is a bit hacky.
// First check what class was specified
@SuppressWarnings("rawtypes")
Class cl = Class.forName(create.classID);
// Now make sure the class is actually a Dialog class
@SuppressWarnings("unchecked")
Class<Dialog> dialogClass = cl.asSubclass(Dialog.class);
// Now initialize the dialog
Dialog dialog = dialogClass.newInstance();
dialog.UID = create.UID;
dialog.initialize(create.args);
if(dialog.root != null)
{
Client.current.gui.getRootPane().add(dialog.root);
}
Client.current.dialogsByUID.put(dialog.UID, dialog);
} catch (ClassNotFoundException e) {
System.err.println("Server asked to create non-existing dialog "+create.classID);
return;
} catch (InstantiationException e) {
System.err.println("Unable to instantiate given Dialog class "+create.classID);
return;
} catch (IllegalAccessException e) {
System.err.println("Unable to instantiate given Dialog class "+create.classID);
return;
} catch(ClassCastException e) {
System.err.println("Server attempted to create illegal Dialog class "+create.classID);
return;
}
}
else if(message instanceof DialogSync)
{
// If it's a DialogSync, forward the message to the correct Dialog instance
DialogSync sync = (DialogSync) message;
Dialog dialog = current.dialogsByUID.get(sync.UID);
if(dialog == null)
{
System.err.println("Received DialogSync for Dialog UID that doesn't exist.");
return;
}
dialog.messageQueue.add(sync.message);
}
}
/** Get the width of the LWJGL display **/
public int getDisplayWidth()
{
return Display.getWidth();
}
/** Get the height of the LWJGL display **/
public int getDisplayHeight()
{
return Display.getHeight();
}
private Renderer renderer;
/** Instance responsible for exchanging messages with the server. **/
ServerConnection connection;
/// Maps Atom UID's to the respective atoms
private Map<Integer,Atom> atomsByUID = new HashMap<Integer,Atom>();
/** A private Queue of AtomDeltas which have been incoming.
* Sorted by the order in which they arrived.
*/
Queue<AtomDelta> atomDeltas =
new LinkedList<AtomDelta>();
/** A list of messages received by the server in the order they arrived. **/
ConcurrentLinkedQueue<Object> serverMessages =
new ConcurrentLinkedQueue<Object>();
/** A list of events that are currently active, or have been sent by
* the server and will be active shortly.
*/
List<SendEvent> activeEvents = new ArrayList<SendEvent>();
/// Represents the current tick the client is processing from the server.
/// A value of -1 means that no tick has been processed yet.
int tick = -1;
public void setTick(int new_tick)
{
tick = new_tick;
}
}