/* * 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.ItemInfo.Owner; import java.net.*; import java.util.*; import java.io.*; import java.lang.ref.*; public class Session implements Owner { public static final int PVER = 36; 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_COMPOSE = 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_CMPPOSE = 16; public static final int OD_CMPMOD = 17; public static final int OD_CMPEQU = 18; public static final int OD_ICON = 19; 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; SocketAddress server; Thread rworker, sworker, ticker; Object[] args; 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<Long, ObjAck> objacks = new TreeMap<Long, ObjAck>(); String username; byte[] cookie; final Map<Integer, CachedRes> rescache = new TreeMap<Integer, CachedRes>(); public final Glob glob; public byte[] sesskey; @SuppressWarnings("serial") public class MessageException extends RuntimeException { public Message msg; public MessageException(String text, Message msg) { super(text); this.msg = msg; } } public static class LoadingIndir extends Loading { public final int resid; private final CachedRes res; private LoadingIndir(CachedRes res) { this.res = res; this.resid = res.resid; } public void waitfor() throws InterruptedException { synchronized(res) { while(res.resnm == null) res.wait(); } } public boolean canwait() {return(true);} } private static class CachedRes { private final int resid; private String resnm = null; private int resver; private Reference<Indir<Resource>> ind; private CachedRes(int id) { resid = id; } private Indir<Resource> get() { Indir<Resource> ind = (this.ind == null)?null:(this.ind.get()); if(ind == null) { ind = new Indir<Resource>() { private Resource res; public Resource get() { if(resnm == null) throw(new LoadingIndir(CachedRes.this)); if(res == null) res = Resource.load(resnm, resver, 0); if(res.loading) throw(new Resource.Loading(res)); return(res); } public String toString() { if(res == null) { return("<res:" + resid + ">"); } else { if(res.loading) return("<!" + res + ">"); else return("<" + res + ">"); } } }; this.ind = new WeakReference<Indir<Resource>>(ind); } return(ind); } public void set(String nm, int ver) { synchronized(this) { this.resnm = nm; this.resver = ver; notifyAll(); } Resource.load(nm, ver, -5); } } private CachedRes cachedres(int id) { synchronized(rescache) { CachedRes ret = rescache.get(id); if(ret != null) return(ret); ret = new CachedRes(id); rescache.put(id, ret); return(ret); } } public Indir<Resource> getres(int id) { return(cachedres(id).get()); } public int getresid(String name){ synchronized(rescache) { for(CachedRes cres : rescache.values()){ if(name.equals(cres.resnm)){ return cres.resid; } } } return 0; } private class ObjAck { long id; int frame; long recv; long sent; public ObjAck(long 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(); long id = msg.uint32(); int frame = msg.int32(); synchronized(oc) { if((fl & 1) != 0) oc.remove(id, frame - 1); Gob gob = oc.getgob(id, frame); if(gob != null) { gob.frame = frame; gob.virtual = ((fl & 2) != 0); } while(true) { int type = msg.uint8(); if(type == OD_REM) { oc.remove(id, frame); } else if(type == OD_MOVE) { Coord c = msg.coord(); int ia = msg.uint16(); if(gob != null) oc.move(gob, c, (ia / 65536.0) * Math.PI * 2); } 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); } if(gob != null) oc.cres(gob, getres(resid), sdt); } else if(type == OD_LINBEG) { Coord s = msg.coord(); Coord t = msg.coord(); int c = msg.int32(); if(gob != null) oc.linbeg(gob, s, t, c); } else if(type == OD_LINSTEP) { int l = msg.int32(); if(gob != null) oc.linstep(gob, l); } else if(type == OD_SPEECH) { float zo = msg.int16() / 100.0f; String text = msg.string(); if(gob != null && !ChatUI.hasTags(text)) oc.speak(gob, zo, text); } else if(type == OD_COMPOSE) { Indir<Resource> base = getres(msg.uint16()); if(gob != null) oc.composite(gob, base); } else if(type == OD_CMPPOSE) { List<ResData> poses = null, tposes = null; int pfl = msg.uint8(); int seq = msg.uint8(); boolean interp = (pfl & 1) != 0; if((pfl & 2) != 0) { poses = new LinkedList<ResData>(); while(true) { int resid = msg.uint16(); if(resid == 65535) break; Message sdt = Message.nil; if((resid & 0x8000) != 0) { resid &= ~0x8000; sdt = msg.derive(0, msg.uint8()); } poses.add(new ResData(getres(resid), sdt)); } } float ttime = 0; if((pfl & 4) != 0) { tposes = new LinkedList<ResData>(); while(true) { int resid = msg.uint16(); if(resid == 65535) break; Message sdt = Message.nil; if((resid & 0x8000) != 0) { resid &= ~0x8000; sdt = msg.derive(0, msg.uint8()); } tposes.add(new ResData(getres(resid), sdt)); } ttime = (msg.uint8() / 10.0f); } if(gob != null) oc.cmppose(gob, seq, poses, tposes, interp, ttime); } else if(type == OD_CMPMOD) { List<Composited.MD> mod = new LinkedList<Composited.MD>(); while(true) { int modid = msg.uint16(); if(modid == 65535) break; Indir<Resource> modr = getres(modid); List<Indir<Resource>> tex = new LinkedList<Indir<Resource>>(); while(true) { int resid = msg.uint16(); if(resid == 65535) break; tex.add(getres(resid)); } mod.add(new Composited.MD(modr, tex)); } if(gob != null) oc.cmpmod(gob, mod); } else if(type == OD_CMPEQU) { List<Composited.ED> equ = new LinkedList<Composited.ED>(); while(true) { int h = msg.uint8(); if(h == 255) break; int ef = h & 0x80; int et = h & 0x7f; String at = msg.string(); Indir<Resource> res; int resid = msg.uint16(); if(resid == 65535) res = null; else res = getres(resid); Coord3f off; if((ef & 128) != 0) { int x = msg.int16(), y = msg.int16(), z = msg.int16(); off = new Coord3f(x / 1000.0f, y / 1000.0f, z / 1000.0f); } else { off = Coord3f.o; } equ.add(new Composited.ED(et, at, res, off)); } if(gob != null) oc.cmpequ(gob, equ); } else if(type == OD_DRAWOFF) { Coord off = msg.coord(); if(gob != null) oc.drawoff(gob, off); } else if(type == OD_LUMIN) { Coord off = msg.coord(); int sz = msg.uint16(); int str = msg.uint8(); if(gob != null) oc.lumin(gob, off, sz, str); } else if(type == OD_AVATAR) { List<Indir<Resource>> layers = new LinkedList<Indir<Resource>>(); while(true) { int layer = msg.uint16(); if(layer == 65535) break; layers.add(getres(layer)); } if(gob != null) oc.avatar(gob, layers); } else if(type == OD_FOLLOW) { long oid = msg.uint32(); Indir<Resource> xfres = null; String xfname = null; if(oid != 0xffffffffl) { xfres = getres(msg.uint16()); xfname = msg.string(); } if(gob != null) oc.follow(gob, oid, xfres, xfname); } else if(type == OD_HOMING) { long oid = msg.uint32(); if(oid == 0xffffffffl) { if(gob != null) oc.homostop(gob); } else if(oid == 0xfffffffel) { Coord tgtc = msg.coord(); int v = msg.uint16(); if(gob != null) oc.homocoord(gob, tgtc, v); } else { Coord tgtc = msg.coord(); int v = msg.uint16(); if(gob != null) oc.homing(gob, 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); } if(gob != null) oc.overlay(gob, olid, prs, res, sdt); } else if(type == OD_HEALTH) { int hp = msg.uint8(); if(gob != null) oc.health(gob, hp); } else if(type == OD_BUDDY) { String name = msg.string(); if(name.length() > 0) { int group = msg.uint8(); int btype = msg.uint8(); if(gob != null) oc.buddy(gob, name, group, btype); } else { if(gob != null) oc.buddy(gob, null, 0, 0); } } else if(type == OD_ICON) { int resid = msg.uint16(); Indir<Resource> res; if(resid == 65535) { res = null; } else { res = getres(resid); int ifl = msg.uint8(); } if(gob != null) oc.icon(gob, res); } else if(type == OD_END) { break; } else { throw(new MessageException("Unknown objdelta type: " + type, msg)); } } } 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); } 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(); cachedres(resid).set(resname, resver); } else if(msg.type == Message.RMSG_PARTY) { glob.party.msg(msg); } else if(msg.type == Message.RMSG_SFX) { Indir<Resource> res = getres(msg.uint16()); 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(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 if(msg.type == Message.RMSG_SESSKEY) { sesskey = msg.bytes(); } else { throw(new MessageException("Unknown rmsg type: " + msg.type, msg)); } } private void getrel(int seq, Message msg) { if(seq == rseq) { int lastack; synchronized(uimsgs) { handlerel(msg); while(true) { rseq = ((lastack = rseq) + 1) % 65536; if(!waiting.containsKey(rseq)) break; handlerel(waiting.get(rseq)); waiting.remove(rseq); } } sendack(lastack); synchronized(Session.this) { Session.this.notifyAll(); } } else if(Utils.floormod(seq - rseq, 65536) < 32768) { 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.getSocketAddress().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)); } } } } 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(2); msg.addstring("Salem"); msg.adduint16(PVER); msg.addstring(username); msg.adduint16(cookie.length); msg.addbytes(cookie); msg.addlist(args); 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); } else if(msg.blob.length > 1000 - 8) { sendmsg(msg); beat = false; msg = new Message(MSG_OBJACK); } msg.adduint32(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(SocketAddress server, String username, byte[] cookie, Object... args) { this.server = server; this.username = username; this.cookie = cookie; this.args = args; 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(); } 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)); } catch(IOException e) { } } @Override public Glob glob() { return glob; } @Override public List<ItemInfo> info() { return null; } }