/*
* This file is part of the Haven & Hearth game client.
* Copyright (C) 2009 Fredrik Tolf <fredrik@dolda2000.com>, and
* Björn Johannessen <johannessen.bjorn@gmail.com>
*
* Redistribution and/or modification of this file is subject to the
* terms of the GNU Lesser General Public License, version 3, as
* published by the Free Software Foundation.
*
* 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.
*
* Other parts of this source tree adhere to other copying
* rights. Please see the file `COPYING' in the root directory of the
* source tree for details.
*
* A copy the GNU Lesser General Public License is distributed along
* with the source tree of which this file is a part in the file
* `doc/LPGL-3'. If it is missing for any reason, please see the Free
* Software Foundation's website at <http://www.fsf.org/>, or write
* to the Free Software Foundation, Inc., 59 Temple Place, Suite 330,
* Boston, MA 02111-1307 USA
*/
package haven;
import haven.MapView.BorderCam;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.TreeMap;
public class Session {
public UI ui;
public static final int PVER = 2;
public static final int MSG_SESS = 0;
public static final int MSG_REL = 1;
public static final int MSG_ACK = 2;
public static final int MSG_BEAT = 3;
public static final int MSG_MAPREQ = 4;
public static final int MSG_MAPDATA = 5;
public static final int MSG_OBJDATA = 6;
public static final int MSG_OBJACK = 7;
public static final int MSG_CLOSE = 8;
public static final int OD_REM = 0;
public static final int OD_MOVE = 1;
public static final int OD_RES = 2;
public static final int OD_LINBEG = 3;
public static final int OD_LINSTEP = 4;
public static final int OD_SPEECH = 5;
public static final int OD_LAYERS = 6;
public static final int OD_DRAWOFF = 7;
public static final int OD_LUMIN = 8;
public static final int OD_AVATAR = 9;
public static final int OD_FOLLOW = 10;
public static final int OD_HOMING = 11;
public static final int OD_OVERLAY = 12;
/* public static final int OD_AUTH = 13; -- Removed */
public static final int OD_HEALTH = 14;
public static final int OD_BUDDY = 15;
public static final int OD_END = 255;
public static final int SESSERR_AUTH = 1;
public static final int SESSERR_BUSY = 2;
public static final int SESSERR_CONN = 3;
public static final int SESSERR_PVER = 4;
public static final int SESSERR_EXPR = 5;
static final int ackthresh = 30;
DatagramSocket sk;
InetAddress server;
Thread rworker, sworker, ticker;
public int connfailed = 0;
public String state = "conn";
int tseq = 0, rseq = 0;
int ackseq;
long acktime = -1;
LinkedList<Message> uimsgs = new LinkedList<Message>();
Map<Integer, Message> waiting = new TreeMap<Integer, Message>();
LinkedList<Message> pending = new LinkedList<Message>();
Map<Integer, ObjAck> objacks = new TreeMap<Integer, ObjAck>();
String username;
public String charname;
byte[] cookie;
final Map<Integer, Indir<Resource>> rescache = new TreeMap<Integer, Indir<Resource>>();
public final Glob glob;
@SuppressWarnings("serial")
public class MessageException extends RuntimeException {
public Message msg;
public MessageException(String text, Message msg) {
super(text);
this.msg = msg;
}
}
public Indir<Resource> getres(final int id) {
synchronized (rescache) {
Indir<Resource> ret = rescache.get(id);
if (ret != null)
return (ret);
ret = new Indir<Resource>() {
public int resid = id;
Resource res;
public Resource get() {
if (res == null)
return (null);
if (res.loading) {
res.boostprio(0);
return (null);
}
return (res);
}
public void set(Resource r) {
res = r;
}
public int compareTo(Indir<Resource> x) {
return ((this.getClass().cast(x)).resid - resid);
}
public String toString() {
if (res == null) {
return ("<res:" + resid + ">");
} else {
if (res.loading)
return ("<!" + res + ">");
else
return ("<" + res + ">");
}
}
};
rescache.put(id, ret);
return (ret);
}
}
private class ObjAck {
int id;
int frame;
long recv;
long sent;
public ObjAck(int id, int frame, long recv) {
this.id = id;
this.frame = frame;
this.recv = recv;
this.sent = 0;
}
}
private class Ticker extends HackThread {
public Ticker() {
super("Server time ticker");
setDaemon(true);
}
public void run() {
try {
while (true) {
long now, then;
then = System.currentTimeMillis();
glob.oc.tick();
now = System.currentTimeMillis();
if (now - then < 70)
Thread.sleep(70 - (now - then));
}
} catch (InterruptedException e) {
}
}
}
private class RWorker extends HackThread {
boolean alive;
public RWorker() {
super("Session reader");
setDaemon(true);
}
private void gotack(int seq) {
synchronized (pending) {
for (ListIterator<Message> i = pending.listIterator(); i.hasNext();) {
Message msg = i.next();
if (msg.seq <= seq)
i.remove();
}
}
}
private void getobjdata(Message msg) {
OCache oc = glob.oc;
while (msg.off < msg.blob.length) {
int fl = msg.uint8();
int id = msg.int32();
int frame = msg.int32();
if ((fl & 1) != 0) {
oc.remove(id, frame - 1);
}
synchronized (oc) {
while (true) {
int type = msg.uint8();
if (type == OD_REM) {
oc.remove(id, frame);
} else if (type == OD_MOVE) {
Coord c = msg.coord();
oc.move(id, frame, c);
} else if (type == OD_RES) {
int resid = msg.uint16();
Message sdt;
if ((resid & 0x8000) != 0) {
resid &= ~0x8000;
sdt = msg.derive(0, msg.uint8());
} else {
sdt = new Message(0);
}
oc.cres(id, frame, getres(resid), sdt);
} else if (type == OD_LINBEG) {
Coord s = msg.coord();
Coord t = msg.coord();
int c = msg.int32();
oc.linbeg(id, frame, s, t, c);
} else if (type == OD_LINSTEP) {
int l = msg.int32();
oc.linstep(id, frame, l);
} else if (type == OD_SPEECH) {
Coord off = msg.coord();
String text = msg.string();
oc.speak(id, frame, off, text);
} else if ((type == OD_LAYERS) || (type == OD_AVATAR)) {
Indir<Resource> baseres = null;
if (type == OD_LAYERS)
baseres = getres(msg.uint16());
List<Indir<Resource>> layers = new LinkedList<Indir<Resource>>();
while (true) {
int layer = msg.uint16();
if (layer == 65535)
break;
layers.add(getres(layer));
}
if (type == OD_LAYERS)
oc.layers(id, frame, baseres, layers);
else
oc.avatar(id, frame, layers);
} else if (type == OD_DRAWOFF) {
Coord off = msg.coord();
oc.drawoff(id, frame, off);
} else if (type == OD_LUMIN) {
oc.lumin(id, frame, msg.coord(), msg.uint16(), msg.uint8());
} else if (type == OD_FOLLOW) {
int oid = msg.int32();
Coord off = Coord.z;
int szo = 0;
if (oid != -1) {
szo = msg.int8();
off = msg.coord();
}
oc.follow(id, frame, oid, off, szo);
} else if (type == OD_HOMING) {
int oid = msg.int32();
if (oid == -1) {
oc.homostop(id, frame);
} else if (oid == -2) {
Coord tgtc = msg.coord();
int v = msg.uint16();
oc.homocoord(id, frame, tgtc, v);
} else {
Coord tgtc = msg.coord();
int v = msg.uint16();
oc.homing(id, frame, oid, tgtc, v);
}
} else if (type == OD_OVERLAY) {
int olid = msg.int32();
boolean prs = (olid & 1) != 0;
olid >>= 1;
int resid = msg.uint16();
Indir<Resource> res;
Message sdt;
if (resid == 65535) {
res = null;
sdt = null;
} else {
if ((resid & 0x8000) != 0) {
resid &= ~0x8000;
sdt = msg.derive(0, msg.uint8());
} else {
sdt = new Message(0);
}
res = getres(resid);
}
oc.overlay(id, frame, olid, prs, res, sdt);
} else if (type == OD_HEALTH) {
int hp = msg.uint8();
oc.health(id, frame, hp);
} else if (type == OD_BUDDY) {
String name = msg.string();
int group = msg.uint8();
int btype = msg.uint8();
oc.buddy(id, frame, name, group, btype);
} else if (type == OD_END) {
break;
} else {
throw (new MessageException("Unknown objdelta type: " + type, msg));
}
}
Gob g = oc.getgob(id, frame);
if (g != null)
g.frame = frame;
}
synchronized (objacks) {
if (objacks.containsKey(id)) {
ObjAck a = objacks.get(id);
a.frame = frame;
a.recv = System.currentTimeMillis();
} else {
objacks.put(id, new ObjAck(id, frame, System.currentTimeMillis()));
}
}
}
synchronized (sworker) {
sworker.notifyAll();
}
}
private void handlerel(Message msg) {
if (msg.type == Message.RMSG_NEWWDG) {
synchronized (uimsgs) {
uimsgs.add(msg);
}
} else if (msg.type == Message.RMSG_WDGMSG) {
synchronized (uimsgs) {
uimsgs.add(msg);
}
} else if (msg.type == Message.RMSG_DSTWDG) {
synchronized (uimsgs) {
uimsgs.add(msg);
}
} else if (msg.type == Message.RMSG_MAPIV) {
glob.map.invalblob(msg, ui);
} else if (msg.type == Message.RMSG_GLOBLOB) {
glob.blob(msg);
} else if (msg.type == Message.RMSG_PAGINAE) {
glob.paginae(msg);
} else if (msg.type == Message.RMSG_RESID) {
int resid = msg.uint16();
String resname = msg.string();
int resver = msg.uint16();
synchronized (rescache) {
getres(resid).set(Resource.load(resname, resver, -5));
}
} else if (msg.type == Message.RMSG_PARTY) {
glob.party.msg(msg);
} else if (msg.type == Message.RMSG_SFX) {
Indir<Resource> res = getres(msg.uint16());
if (!Config.isSoundOn)
return;// Sound effects disabled
double vol = ((double) msg.uint16()) / 256.0;
double spd = ((double) msg.uint16()) / 256.0;
Audio.play(res);
} else if (msg.type == Message.RMSG_CATTR) {
glob.cattr(msg);
} else if (msg.type == Message.RMSG_MUSIC) {
String resnm = msg.string();
int resver = msg.uint16();
boolean loop = !msg.eom() && (msg.uint8() != 0);
if (Music.enabled) {
if (resnm.equals(""))
Music.play(null, false);
else
Music.play(Resource.load(resnm, resver), loop);
}
} else if (msg.type == Message.RMSG_TILES) {
glob.map.tilemap(msg);
} else if (msg.type == Message.RMSG_BUFF) {
glob.buffmsg(msg);
} else {
throw (new MessageException("Unknown rmsg type: " + msg.type, msg));
}
}
private void getrel(int seq, Message msg) {
if (seq == rseq) {
synchronized (uimsgs) {
handlerel(msg);
while (true) {
rseq = (rseq + 1) % 65536;
if (!waiting.containsKey(rseq))
break;
handlerel(waiting.get(rseq));
waiting.remove(rseq);
}
}
sendack(rseq - 1);
synchronized (Session.this) {
Session.this.notifyAll();
}
} else if (seq > rseq) {
waiting.put(seq, msg);
}
}
public void run() {
try {
alive = true;
try {
sk.setSoTimeout(1000);
} catch (SocketException e) {
throw (new RuntimeException(e));
}
while (alive) {
DatagramPacket p = new DatagramPacket(new byte[65536], 65536);
try {
sk.receive(p);
} catch (java.nio.channels.ClosedByInterruptException e) {
/*
* Except apparently Sun's J2SE doesn't throw this when
* interrupted :P
*/
break;
} catch (SocketTimeoutException e) {
continue;
} catch (IOException e) {
throw (new RuntimeException(e));
}
if (!p.getAddress().equals(server))
continue;
Message msg = new Message(p.getData()[0], p.getData(), 1, p.getLength() - 1);
if (msg.type == MSG_SESS) {
if (state == "conn") {
int error = msg.uint8();
synchronized (Session.this) {
if (error == 0) {
state = "";
} else {
connfailed = error;
Session.this.close();
}
Session.this.notifyAll();
}
}
}
if (state != "conn") {
if (msg.type == MSG_SESS) {
} else if (msg.type == MSG_REL) {
int seq = msg.uint16();
while (!msg.eom()) {
int type = msg.uint8();
int len;
if ((type & 0x80) != 0) {
type &= 0x7f;
len = msg.uint16();
} else {
len = msg.blob.length - msg.off;
}
getrel(seq, new Message(type, msg.blob, msg.off, len));
msg.off += len;
seq++;
}
} else if (msg.type == MSG_ACK) {
gotack(msg.uint16());
} else if (msg.type == MSG_MAPDATA) {
glob.map.mapdata(msg);
} else if (msg.type == MSG_OBJDATA) {
getobjdata(msg);
} else if (msg.type == MSG_CLOSE) {
synchronized (Session.this) {
state = "fin";
Session.this.notifyAll();
}
Session.this.close();
} else {
// throw(new
// MessageException("Unknown message type: " +
// msg.type, msg));
System.out.println("Unknown message type: " + msg.type + "\nMSG: " + msg.toString());
}
}
}
} finally {
synchronized (Session.this) {
state = "dead";
Session.this.notifyAll();
}
}
}
public void interrupt() {
alive = false;
super.interrupt();
}
}
private class SWorker extends HackThread {
public SWorker() {
super("Session writer");
setDaemon(true);
}
public void run() {
try {
long to, last = 0, retries = 0;
while (true) {
long now = System.currentTimeMillis();
if (state == "conn") {
if (now - last > 2000) {
if (++retries > 5) {
synchronized (Session.this) {
connfailed = SESSERR_CONN;
Session.this.notifyAll();
return;
}
}
Message msg = new Message(MSG_SESS);
msg.adduint16(1);
msg.addstring("Haven");
msg.adduint16(PVER);
msg.addstring(username);
msg.addbytes(cookie);
sendmsg(msg);
last = now;
}
Thread.sleep(100);
} else {
to = 5000;
synchronized (pending) {
if (pending.size() > 0)
to = 60;
}
synchronized (objacks) {
if ((objacks.size() > 0) && (to > 120))
to = 200;
}
synchronized (this) {
if (acktime > 0)
to = acktime + ackthresh - now;
if (to > 0)
this.wait(to);
}
now = System.currentTimeMillis();
boolean beat = true;
/*
* if((closing != -1) && (now - closing > 500)) {
* Message cm = new Message(MSG_CLOSE); sendmsg(cm);
* closing = now; if(++ctries > 5)
* getThreadGroup().interrupt(); }
*/
synchronized (pending) {
if (pending.size() > 0) {
for (Message msg : pending) {
int txtime;
if (msg.retx == 0)
txtime = 0;
else if (msg.retx == 1)
txtime = 80;
else if (msg.retx < 4)
txtime = 200;
else if (msg.retx < 10)
txtime = 620;
else
txtime = 2000;
if (now - msg.last > txtime) { /* XXX */
msg.last = now;
msg.retx++;
Message rmsg = new Message(MSG_REL);
rmsg.adduint16(msg.seq);
rmsg.adduint8(msg.type);
rmsg.addbytes(msg.blob);
sendmsg(rmsg);
}
}
beat = false;
}
}
synchronized (objacks) {
Message msg = null;
for (Iterator<ObjAck> i = objacks.values().iterator(); i.hasNext();) {
ObjAck a = i.next();
boolean send = false, del = false;
if (now - a.sent > 200)
send = true;
if (now - a.recv > 120)
send = del = true;
if (send) {
if (msg == null)
msg = new Message(MSG_OBJACK);
msg.addint32(a.id);
msg.addint32(a.frame);
a.sent = now;
}
if (del)
i.remove();
}
if (msg != null) {
sendmsg(msg);
beat = false;
}
}
synchronized (this) {
if ((acktime > 0) && (now - acktime >= ackthresh)) {
byte[] msg = { MSG_ACK, 0, 0 };
Utils.uint16e(ackseq, msg, 1);
sendmsg(msg);
acktime = -1;
beat = false;
}
}
if (beat) {
if (now - last > 5000) {
sendmsg(new byte[] { MSG_BEAT });
last = now;
}
}
}
}
} catch (InterruptedException e) {
for (int i = 0; i < 5; i++) {
sendmsg(new Message(MSG_CLOSE));
long f = System.currentTimeMillis();
while (true) {
synchronized (Session.this) {
if ((state == "conn") || (state == "fin") || (state == "dead"))
break;
state = "close";
long now = System.currentTimeMillis();
if (now - f > 500)
break;
try {
Session.this.wait(500 - (now - f));
} catch (InterruptedException e2) {
}
}
}
}
} finally {
ticker.interrupt();
rworker.interrupt();
}
}
}
public Session(InetAddress server, String username, byte[] cookie) {
this.server = server;
this.username = username;
this.cookie = cookie;
glob = new Glob(this);
try {
sk = new DatagramSocket();
} catch (SocketException e) {
throw (new RuntimeException(e));
}
rworker = new RWorker();
rworker.start();
sworker = new SWorker();
sworker.start();
ticker = new Ticker();
ticker.start();
}
private void sendack(int seq) {
synchronized (sworker) {
if (acktime < 0)
acktime = System.currentTimeMillis();
ackseq = seq;
sworker.notifyAll();
}
}
public void close() {
sworker.interrupt();
ticker.interrupt(); // new
rworker.interrupt(); // new
}
public synchronized boolean alive() {
return (state != "dead");
}
public void queuemsg(Message msg) {
msg.seq = tseq;
tseq = (tseq + 1) % 65536;
synchronized (pending) {
pending.add(msg);
}
synchronized (sworker) {
sworker.notify();
}
}
public Message getuimsg() {
synchronized (uimsgs) {
if (uimsgs.size() == 0)
return (null);
return (uimsgs.remove());
}
}
public void sendmsg(Message msg) {
byte[] buf = new byte[msg.blob.length + 1];
buf[0] = (byte) msg.type;
System.arraycopy(msg.blob, 0, buf, 1, msg.blob.length);
sendmsg(buf);
}
public void sendmsg(byte[] msg) {
try {
sk.send(new DatagramPacket(msg, msg.length, server, 1870));
} catch (IOException e) {
}
}
}