package complexion.server;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ConcurrentMap;
import org.lwjgl.input.Keyboard;
import complexion.common.Directions;
import complexion.network.message.AtomDelta;
import complexion.network.message.AtomUpdate;
import complexion.network.message.AtomVerbs;
import complexion.network.message.DialogSync;
import complexion.network.message.FullAtomUpdate;
import complexion.network.message.InputData;
import complexion.network.message.VerbResponse;
import complexion.network.message.VerbSignature;
import complexion.test.TestAtom;
/**
* This class represents a single client connected to the server.
* The class can be used to send messages to the server etc.
*/
public class Client
{
/** Maintains a list of all dialogs currently open.
*/
ConcurrentMap<Integer,DialogHandle> dialogsByUID = new ConcurrentHashMap<Integer,DialogHandle>();
/// Identifies the client in the (as of yet non-existent)
/// login system
String account_name;
// The mob that the client uses and input is relayed too. aka the "thing" the client is controlling
private Mob holder;
/// The TCP connection this client uses.
ClientConnection connection;
// TODO: test variable, remove
DialogHandle testDialog;
/** A sort of constructor called after the Client has been set up properly. **/
public void initialize()
{
Mob test_mover = new Mob();
test_mover.setSprite("mask.dmi");
test_mover.setSpriteState("muzzle");
test_mover.setLayer(10);
test_mover.Move(Server.current.getTile(1, 1, 0));
setHolder(test_mover);
testDialog = new DialogHandle(this,"complexion.test.TestDialog",null);
DialogHandle dialog2 = new DialogHandle(this,"randomgarbleclass",null);
DialogHandle dialog3 = new DialogHandle(this,"complexion.test.KryoTest",null);
TestAtom a = new TestAtom();
this.createVerbDialog(a);
testDialog.sendMessage("Test");
}
/** Handler invoked regularly(every tick) to process things. **/
public void Tick()
{
//testDialog.sendMessage("TickMessage!");
Object o = testDialog.pollMessage();
if(o != null) System.out.println(o);
// ====== PROCESS NETWORK MESSAGES ======
while(networkMessages.size() > 0)
{
// No need to check whether message retrievel was successful,
// due to the fact that other threads will only add, not remove
// messages from the queue
Object message = networkMessages.poll();
// Process network events.
System.out.println("ClientConnection: "+message);
if(message instanceof InputData)
{
InputData data = (InputData) message;
String has = "pressed";
System.out.println(getAccountName() + " has " + has + data.key);
ProcessInput(data.key);
}
if(message instanceof DialogSync)
{
// If it's a DialogSync, forward the message to the correct Dialog instance
DialogSync sync = (DialogSync) message;
DialogHandle dialog = dialogsByUID.get(sync.UID);
if(dialog == null)
{
System.err.println("Received DialogSync for Dialog UID that doesn't exist.");
continue;
}
dialog.messageQueue.add(sync.message);
}
if(message instanceof VerbResponse)
{
VerbResponse verb = (VerbResponse) message;
Atom target = atomCache.get(verb.UID);
// If the atom doesn't exist anymore, that's a problem.
if(target == null)
{
System.err.println("Received VerbResponse for atom that doesn't exist.");
continue;
}
target.callVerb(verb.verbName, verb.arguments.toArray());
}
}
}
public String getAccountName() {
return account_name;
}
/**
* @return The current atom determining the client's view range
*/
public Atom getEye() {
return eye;
}
/** Update the client's viewport, this may cause the client's view to
* "jump" elsewhere
* @param The new atom to associate the client's view range with
*/
public void setEye(Atom eye) {
this.eye = eye;
}
/** Remove the updated flags from all atoms in view of the client.
* This should be called on each client at the end of the processing
* tick.
*/
void clearAtomsInView()
{
int eye_x = 0;
int eye_y = 0;
if(this.eye != null)
{
eye_x = eye.getX();
eye_y = eye.getY();
}
// Scan over all the atoms and see if they're outdated
for(int x=eye_x - view_range; x<=eye_x + view_range; x++)
{
for(int y=eye_y - view_range; y<=eye_y + view_range; y++)
{
// Extract the turf at the given tile
Tile turf = Server.current.getTile(x, y, 0);
// Make sure the turf actually exists
if(turf != null)
{
// clear the outdated flags for the tile and its contents
turf.outdated = 0;
for(Atom content : turf.contents)
{
content.outdated = 0;
}
}
}
}
}
/** Send an update for all visible atoms to the remote client.
* @param server The server on which the world is running.
*/
void synchronizeAtoms()
{
Server server = Server.current;
AtomDelta delta = new AtomDelta();
// Store on which tick this message was sent.
delta.tick = server.getTick();
// Extract the viewport center from this.eye
if(this.eye != null)
{
delta.eye_x = this.eye.getX();
delta.eye_y = this.eye.getY();
}
else
{
// TODO: rather than setting the position to 0, notify the client somehow
// that they aren't on the map at all anymore
delta.eye_x = 0; delta.eye_y = 0;
}
// Send how far the client can see
delta.viewrange = this.view_range;
// Start building the list of atoms to update
delta.updates = new ArrayList<AtomUpdate>();
// TODO: rather than resend everything when anything changed, try to somehow figure out
// which part of the viewrange is new, and which is old
if(delta.eye_x != last_x || delta.eye_y != last_y || resendEverything)
{
// TODO: comment this out when a method has been added to detect it when atoms have changed
resendEverything = false;
// For now, simply resend everything
// Iterate over all the atoms in the given range
// TODO: Add a function to Server that extracts a range for us
for(int x=delta.eye_x - view_range; x<=delta.eye_x + view_range; x++)
{
for(int y=delta.eye_y - view_range; y<=delta.eye_y + view_range; y++)
{
// Extract the turf at the given tile
Tile turf = server.getTile(x, y, 0);
// Check if the tile is entirely new, or still in the old viewrange
if(last_x - view_range <= x && x <= last_x + view_range &&
last_y - view_range <= y && y <= last_y + view_range)
{
// The tile was also on the previous viewport, it's enough to update it
addTileIfOutdated(turf, delta);
}
else
{
// The tile wasn't on the previous viewport, resend completely
addTileToDelta(turf, delta);
}
}
}
last_x = delta.eye_x;
last_y = delta.eye_y;
}
else
{
// We're not resending everything, so let's find out if any of our atoms changed at all.
// Scan over all the atoms and see if they're outdated
for(int x=delta.eye_x - view_range; x<=delta.eye_x + view_range; x++)
{
for(int y=delta.eye_y - view_range; y<=delta.eye_y + view_range; y++)
{
// Extract the turf at the given tile
Tile turf = server.getTile(x, y, 0);
addTileIfOutdated(turf, delta);
}
}
}
// Send the atom updates to the remote client.
if(!delta.updates.isEmpty())
connection.send(delta);
}
/**
* Processes input received from clients
* @param key : The internal number of a keyboard key
*/
public void ProcessInput(int key)
{
if(Keyboard.KEY_W == key)
{
if(holder != null)
holder.Step(Directions.NORTH);
}
if(Keyboard.KEY_D == key)
{
if(holder != null)
holder.Step(Directions.EAST);
}
if(Keyboard.KEY_A == key)
{
if(holder != null)
holder.Step(Directions.WEST);
}
if(Keyboard.KEY_S == key)
{
if(holder != null)
holder.Step(Directions.SOUTH);
}
}
/**
* @return the holder
*/
public Mob getHolder()
{
return holder;
}
/**
* @param holder the holder to set
*/
public void setHolder(Mob holder)
{
if(holder != null && holder.getClient() != null)
return;
this.holder = holder;
this.holder.setClient(this);
}
/** Creates a verb dialog for the specified target. Does nothing if the target
* has zero verbs that this client's holder can interact with.
*
* @param target The atom to interact with, holding the list of verbs we might use.
*/
public void createVerbDialog(Atom target)
{
// Cache the UID so we remember it later.
atomCache.put(target.getUID(), target);
AtomVerbs verbs = target.getVerbs(this.holder);
DialogHandle.createSimpleDialog(this, "complexion.client.DialogVerb", verbs);
}
private void addTileToDelta(Tile tile, AtomDelta delta)
{
// Make sure the turf actually exists
if(tile != null)
{
delta.updates.add(new FullAtomUpdate(tile));
for(Atom content : tile.contents)
{
delta.updates.add(new FullAtomUpdate(content));
}
}
}
private void addTileIfOutdated(Tile tile, AtomDelta delta)
{
// Make sure the turf actually exists
if(tile != null)
{
// TODO: discriminate between the different outdated types
if(tile.outdated != 0)
{
delta.updates.add(new FullAtomUpdate(tile));
}
for(Atom content : tile.contents)
{
if(content.outdated != 0)
{
delta.updates.add(new FullAtomUpdate(content));
}
}
}
}
/// An atom on the map that this client is associated with.
/// This will determine the client's current view range on the map.
private Atom eye;
/// Describes how far the client can see from her eye in each direction.
private int view_range = 8;
/// The last viewport(center and size) that was sent to the client
private int last_x, last_y, last_z, last_view_range;
/// Force the synchronization handler to resend all atoms.
/// This flag will be automatically cleared once everything has been sent.
/// Set to true by default, because on the first tick everything should be resent.
private boolean resendEverything = true;
/// A queue of incoming network messages, sorted by arrival
ConcurrentLinkedQueue<Object> networkMessages =
new ConcurrentLinkedQueue<Object>();
/** Local Atom cache, we'll use this to find which atom a verb was invoked on. **/
Map<Integer,Atom> atomCache = new HashMap<Integer,Atom>();
}