public interface SendableObject {
    public long getSerial();
    public String getName();
    public int getGraphic();
    public int getHue();
    public Point3D getLocation();
} public class FullItemsContainerPacket extends SLPacket {
    public static final short ID = 0x71;
    private List<SendableItem> items;
    private long containerSerial;

    private FullItemsContainerPacket() {
        items = new ArrayList<SendableItem>();
    }

    public static FullItemsContainerPacket read(ByteBuffer b, int len) {
        FullItemsContainerPacket res = new FullItemsContainerPacket();
        int itemCount = readUWord(b);
        for(int i = 0; i < itemCount; i++) {
            ItemStub itm = new ItemStub();
            itm.setSerial(readUDWord(b));
            itm.setGraphic(readUWord(b));
            readUByte(b);
            itm.setAmount(readUWord(b));
            int x = readUWord(b);
            int y = readUWord(b);
            itm.setLocation(new Point3D(x, y));
            res.containerSerial = readUDWord(b); // TODO: restricted to one container per packet now
            itm.setHue(readUWord(b));
            res.items.add(itm);
        }
        return res;
    }

    public FullItemsContainerPacket(Collection<? extends SendableItem> items, SendableItem container) {
        initWrite(ID, 4 + 2 + items.size() * 19);
        addUWord(items.size());
        for(SendableItem item : items) {
            addUDWord(item.getSerial());
            addUWord(item.getGraphic());
            addUByte((short) 0); // seems to be added to item graphic if container is a container
            addUWord(item.getAmount());
            addUWord(item.getLocation().getX());
            addUWord(item.getLocation().getY());
            addUDWord(container.getSerial());
            addUWord(item.getHue());
        }
    }

    public List<SendableItem> getItems() {
        return Collections.unmodifiableList(items);
    }

    public long getContainerSerial() {
        return containerSerial;
    }

    @Override
    public short getID() {
        return ID;
    }
} public class InitPlayerPacket extends SLPacket {
    public static final short ID = 0x36;
    private long serial, seed;

    public InitPlayerPacket(SendableMobile player, long seed) {
        initWrite(ID, 0x1A);

        addUDWord(player.getSerial());
        addUDWord(seed);
        // unknown stuff
        addUWord(0);
        addUWord(0);
        addUWord(0);
        addUWord(0);
        addUByte((short) 0);
        addUByte((short) 0);
        addUDWord(0);
    }

    private InitPlayerPacket() {
    }

    public static InitPlayerPacket read(ByteBuffer buffer, int length) {
        InitPlayerPacket res = new InitPlayerPacket();
        res.serial = readUDWord(buffer);
        res.seed = readUDWord(buffer);
        readUWord(buffer);
        readUWord(buffer);
        readUWord(buffer);
        readUWord(buffer);
        readUByte(buffer);
        readUByte(buffer);
        readUDWord(buffer);
        return res;
    }

    public long getSerial() {
        return serial;
    }

    public long getSeed() {
        return seed;
    }

    @Override
    public short getID() {
        return ID;
    }
} public class OpenGumpPacket extends SLPacket {
    public static final short ID =  0x42;
    private long serial;
    private int gumpID;

    private OpenGumpPacket() {
    }

    public static OpenGumpPacket read(ByteBuffer b, int len) {
        OpenGumpPacket res = new OpenGumpPacket();
        res.serial = readUDWord(b);
        res.gumpID = readUWord(b);
        return res;
    }

    public OpenGumpPacket(SendableObject obj, int gumpID) {
        initWrite(ID, 0x0A);
        addUDWord(obj.getSerial());
        addUWord(gumpID);
    }

    public long getSerial() {
        return serial;
    }

    public int getGumpID() {
        return gumpID;
    }

    @Override
    public short getID() {
        return ID;
    }

} public abstract class SLPacket {
    private static final short SL_PACKET_MAGIC = 0xFE;
    private ByteBuffer sendBuffer;

    protected void initWrite(short id, int len) {
        sendBuffer = ByteBuffer.allocate(len);
        sendBuffer.order(ByteOrder.BIG_ENDIAN);
        addUByte(SL_PACKET_MAGIC);
        addUByte(id);
        addUWord(len);
    }

    public abstract short getID();

    public void addSByte(byte b) {
        sendBuffer.put(b);
    }

    public void addUByte(short b) {
        sendBuffer.put((byte) b);
    }

    public void addSWord(short w) {
        sendBuffer.putShort(w);
    }

    public void addUWord(int w) {
        sendBuffer.putShort((short) w);
    }

    public void addSDWord(int d) {
        sendBuffer.putInt(d);
    }

    public void addUDWord(long d) {
        sendBuffer.putInt((int) d);
    }

    public void addString(String s, int len) {
        for(int i = 0; i < len; i++) {
            if(s != null && i < s.length()) {
                char c = s.charAt(i);
                addUByte((short) (c & 0xFF));
            } else {
                addUByte((short) 0);
            }
        }
    }

    public void addString(String s) {
        addString(s, s.length() + 1);
    }

    public static byte readSByte(ByteBuffer b) {
        return b.get();
    }

    public static short readUByte(ByteBuffer b) {
        return (short) (b.get() & 0xFF);
    }

    public static short readSWord(ByteBuffer b) {
        return b.getShort();
    }

    public static int readUWord(ByteBuffer b) {
        return b.getShort() & 0xFFFF;
    }

    public static int readSDWord(ByteBuffer b) {
        return b.getInt();
    }

    public static long readUDWord(ByteBuffer b) {
        return b.getInt() & 0xFFFFFFFF;
    }

    public static String readString(ByteBuffer b, int len) {
        StringBuilder res = new StringBuilder(len);
        boolean gotNull = false;
        for(int i = 0; i < len; i++) {
            char chr = (char) b.get();
            if(chr != '\0' && !gotNull) {
                res.append(chr);
            } else {
                gotNull = true;
            }
        }
        return res.toString();
    }

    public static String readString(ByteBuffer b) {
        StringBuilder res = new StringBuilder();
        char chr;
        do {
            chr = (char) b.get();
            if(chr != '\0') {
                res.append(chr);
            }
        } while(chr != '\0');
        return res.toString();
    }

    // write packet to buffer
    public void writeTo(ByteBuffer dest) throws IOException {
        sendBuffer.flip();
        dest.put(sendBuffer);
        if(sendBuffer.remaining() > 0) {
            throw new IOException("send buffer overflow");
        }
    }

    public static SLPacket readPacket(ByteBuffer buffer) throws IOException {
        int gotBytes = buffer.remaining();
        if(gotBytes < 4) {
            // not enough bytes to read packet header
            return null;
        }

        short magic = readUByte(buffer);
        if(magic != SL_PACKET_MAGIC) {
            // not an UOSL packet or packet error -> need to kick client
            throw new IOException("invalid packet magic: " + magic);
        }
        short id = readUByte(buffer);
        int length = readUWord(buffer);
        if(length > gotBytes) {
            // not enough bytes to read entire packet
            return null;
        }
        int dataLength = length - 4;

        switch(id) {
        case LoginPacket.ID:            return LoginPacket.read(buffer, dataLength);
        case LoginErrorPacket.ID:       return LoginErrorPacket.read(buffer, dataLength);
        case InitPlayerPacket.ID:       return InitPlayerPacket.read(buffer, dataLength);
        case StatsUpdatePacket.ID:      return StatsUpdatePacket.read(buffer, dataLength);
        case LocationPacket.ID:         return LocationPacket.read(buffer, dataLength);
        case EquipPacket.ID:            return EquipPacket.read(buffer, dataLength);
        case GlobalLightLevelPacket.ID: return GlobalLightLevelPacket.read(buffer, dataLength);
        case SendTextPacket.ID:         return SendTextPacket.read(buffer, dataLength);
        case SendObjectPacket.ID:       return SendObjectPacket.read(buffer, dataLength);
        case RemoveObjectPacket.ID:     return RemoveObjectPacket.read(buffer, dataLength);
        case AllowMovePacket.ID:        return AllowMovePacket.read(buffer, dataLength);
        case DenyMovePacket.ID:         return DenyMovePacket.read(buffer, dataLength);
        case SoundPacket.ID:            return SoundPacket.read(buffer, dataLength);
        case OpenGumpPacket.ID:         return OpenGumpPacket.read(buffer, dataLength);
        case FullItemsContainerPacket.ID: return FullItemsContainerPacket.read(buffer, dataLength);
        case ItemInContainerPacket.ID:  return ItemInContainerPacket.read(buffer, dataLength);
        case DragPacket.ID:             return DragPacket.read(buffer, dataLength);
        case DropPacket.ID:             return DropPacket.read(buffer, dataLength);
        case SingleClickPacket.ID:      return SingleClickPacket.read(buffer, dataLength);
        case ActionPacket.ID:           return ActionPacket.read(buffer, dataLength);
        case EquipReqPacket.ID:         return EquipReqPacket.read(buffer, dataLength);
        case MoveRequestPacket.ID:      return MoveRequestPacket.read(buffer, dataLength);
        case DoubleClickPacket.ID:      return DoubleClickPacket.read(buffer, dataLength);
        case SpeechRequestPacket.ID:    return SpeechRequestPacket.read(buffer, dataLength);
        case RequestPacket.ID:          return RequestPacket.read(buffer, dataLength);
        case ShopPacket.ID:             return ShopPacket.read(buffer, dataLength);
        case AttackPacket.ID:           return AttackPacket.read(buffer, dataLength);
        case BoardAddPostPacket.ID:     return BoardAddPostPacket.read(buffer, dataLength);
        case GroupPacket.ID:            return GroupPacket.read(buffer, dataLength);
        default:                        return UnknownPacket.read(buffer, id, dataLength);
        }
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        if(sendBuffer != null) {
            builder.append(String.format("Packet %02X -> ", getID()));
            for(int i = 0; i < sendBuffer.capacity(); i++) {
                builder.append(String.format("%02X", sendBuffer.get(i)));
            }
        } else {
            builder.append(getClass().getSimpleName());
        }
        return builder.toString();
    }

    public static SLPacket fromHexString(short id, String data) {
        return new ASCIIPacket(id, data);
    }
 } public class Connection {
    private static final Logger log = Logger.getLogger("slclient.connection");
    private static final int DEFAULT_PORT = 2590;
    private static final int BUFFER_SIZE = 65536;
    private final Selector selector;
    private final SocketChannel socketChan;
    private final Thread connectionThread;
    private final ByteBuffer recvBuffer, sendBuffer;
    private final Object selectMutex;
    private boolean enableWrite, disableWrite;
    private final ConnectionHandler handler;
    private String host;
    private int port;

    public interface ConnectionHandler {
        void onConnected();
        void onIncomingPacket(SLPacket packet);
        void onRemoteDisconnect();
        void onNetworkError(String reason);
    }


    public Connection(ConnectionHandler handler, String destination) throws IOException {
        this.handler = handler;
        this.selectMutex = new Object();

        String[] info = destination.split(":");
        if(info.length == 1) {
            host = info[0];
            port = DEFAULT_PORT;
        } else if(info.length == 2) {
            host = info[0];
            port = Integer.parseInt(info[1]);
        } else {
            throw new IllegalArgumentException("Invalid format");
        }

        recvBuffer = ByteBuffer.allocate(BUFFER_SIZE);
        recvBuffer.order(ByteOrder.BIG_ENDIAN);
        sendBuffer = ByteBuffer.allocate(BUFFER_SIZE);
        sendBuffer.order(ByteOrder.BIG_ENDIAN);

        selector = Selector.open();
        socketChan = SocketChannel.open();
        socketChan.configureBlocking(false);
        socketChan.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ);
        connectionThread = new Thread(() -> loop());
        connectionThread.setName("Network");
        connectionThread.setDaemon(true);
        connectionThread.start();

        connect();
    }

    private void connect() throws IOException {
        try {
            log.info("Connecting to " + host + ", port " + port);
            InetSocketAddress addr = new InetSocketAddress(host, port);

            if(socketChan.connect(addr)) {
                // immediate success
                log.fine("Immediate success when connecting");
                handler.onConnected();
            }
        } catch(Exception e) {
            log.warning("Couldn't connect: " + e.getMessage());
            disconnect();
            throw e;
        }
    }

    public void disconnect() {
        try {
            log.fine("Disconnecting connection");
            selector.wakeup();
            connectionThread.interrupt();
            socketChan.close();
        } catch (IOException e) {
            // network error on shutdown is OK, so don't propagate
            log.fine("Exception on channel close: " + e.getMessage());
        }
    }

    private void loop() {
        log.fine("Running network loop");
        while(!Thread.interrupted()) {
            try {
                selector.select();
                for(SelectionKey key : selector.selectedKeys()) {
                    if(!key.isValid()) {
                        log.warning("Invalid key in select");
                        handler.onNetworkError("Network Error: select");
                        key.cancel();
                        continue;
                    }

                    if(key.isConnectable()) {
                        onConnect();
                    } else if(key.isReadable()) {
                        onReadable();
                    } else if(key.isWritable()) {
                        onWritable();
                    }
                }

                selector.selectedKeys().clear();

                synchronized(selectMutex) {
                    SelectionKey key = socketChan.keyFor(selector);
                    if(key == null || !key.isValid()) {
                        log.warning("Key became invalid when setting interestOps");
                        handler.onRemoteDisconnect();
                        break;
                    }

                    if(disableWrite && !enableWrite) {
                        disableWrite = false;
                        key.interestOps(SelectionKey.OP_READ);
                    }
                    if(enableWrite) {
                        enableWrite = false;
                        key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
                    }
                }
            } catch (IOException e) {
                handler.onNetworkError(e.getMessage());
                break;
            }
        }
        log.fine("Left network loop");
    }

    private void onConnect() {
        try {
            socketChan.finishConnect();
        } catch(Exception e) {
            log.warning("Connect failed: " + e.getMessage());
            handler.onNetworkError(e.getMessage());
            return;
        }
        handler.onConnected();
    }

    public void sendPacket(SLPacket packet) {
        boolean needEnable = false;

        if(sendBuffer.position() == 0) {
            // there was nothing to send before, enable select notification for write-ready
            needEnable = true;
        }
        try {
            // append packet to sendBuffer
            packet.writeTo(sendBuffer);
        } catch (IOException e) {
            log.log(Level.SEVERE, "Exception when writing packet: " + e.getMessage(), e);
            handler.onRemoteDisconnect();
            needEnable = false;
        }
        if(needEnable) {
            synchronized(selectMutex) {
                enableWrite = true;
                selector.wakeup();
            }
        }
    }

    private void onWritable() throws IOException {
        log.finest("Sending out write buffer");
        boolean needDisable = false;
        sendBuffer.flip();
        socketChan.write(sendBuffer);
        sendBuffer.compact();
        if(sendBuffer.position() == 0) {
            // buffer empty again -> disable write notification
            needDisable = true;
        }
        if(needDisable) {
            synchronized(selectMutex) {
                disableWrite = true;
                selector.wakeup();
            }
        }
    }

    private void onReadable() {
        List<SLPacket> packets = new ArrayList<>(5);

        try {
            int bytesRead = socketChan.read(recvBuffer);
            if(bytesRead == -1) {
                // normal shutdown of the client
                log.fine("Client shutdown while reading");
                handler.onRemoteDisconnect();
                return;
            }

            SLPacket lastPacket = null;
            do {
                recvBuffer.flip();
                lastPacket = SLPacket.readPacket(recvBuffer);
                if(lastPacket != null) {
                    // got a full packet, add to queue
                    packets.add(lastPacket);
                    recvBuffer.compact();
                } else {
                    // didn't get a full packet, try again next time
                    recvBuffer.position(recvBuffer.limit());
                    recvBuffer.limit(recvBuffer.capacity());
                }
            } while(lastPacket != null);
        } catch (IOException e) {
            handler.onNetworkError("Read error: " + e.getMessage());
            return;
        }

        // add all received packets to handler
        for(SLPacket packet : packets) {
            try {
                handler.onIncomingPacket(packet);
            } catch(Exception e) {
                log.log(Level.SEVERE, "Error when handling incoming packet: " + e.getMessage(), e);
            }
        }
    }
} public class GameState {
    public enum State {DISCONNECTED, CONNECTED, LOGGED_IN};

    private static final Logger log = Logger.getLogger("slclient.game");
    private final ObservableValue<State> state;
    private Player player;
    private String loginName, loginPassword;

    private Connection connection;
    private int updateRange = 15;

    private final ObjectRegistry objectsInRange;

    // Movement
    private final int MOVE_DELAY = 150;
    private short nextMoveSequence, lastAckedMoveSequence;
    private long lastMoveTime;

    public GameState() {
        state = new ObservableValue<>(State.DISCONNECTED);
        objectsInRange = new ObjectRegistry();
        log.fine("Game state initialized");
    }

    public void setLoginDetails(String name, String password) {
        this.loginName = name;
        this.loginPassword = password;
    }

    public State getState() {
        return state.getValue();
    }

    public void addStateListener(BiConsumer<State, State> listener) {
        state.addObserver(listener);
    }

    public void removeStateListener(BiConsumer<State, State> listener) {
        state.removeObserver(listener);
    }

    public void onConnect(Connection connection) {
        this.connection = connection;
        state.setValue(State.CONNECTED);
    }

    public void onDisconnect() {
        this.connection = null;
        state.setValue(State.DISCONNECTED);
    }

    public void tryLogin() {
        LoginPacket login = new LoginPacket();
        login.setName(loginName);
        login.setPassword(loginPassword);
        login.setSeed(LoginPacket.LOGIN_BY_NAME);
        login.setSerial(LoginPacket.LOGIN_BY_NAME);
        login.prepareSend();
        connection.sendPacket(login);
    }

    public void onLoginSuccess(long serial, int graphic, Point3D location, Direction facing) {
        player = new Player(serial, graphic);
        player.setLocation(location);
        player.setFacing(facing);
        objectsInRange.registerObject(player);
        state.setValue(State.LOGGED_IN);
    }

    public int getUpdateRange() {
        return updateRange;
    }

    public void setUpdateRange(int updateRange) {
        this.updateRange = updateRange;
        if(player != null) {
            // treat as location change to remove old and show new objects
            onPlayerLocationChange(player.getLocation(), player.getLocation());
        }
    }

    // there is potential for optimization here
    public Stream<SLObject> getObjectsAt(Point2D point) {
        return Stream.concat(
                objectsInRange.getObjectsAt(point),
                SLData.get().getStatics().getStaticsStream(point)
                    .map((sta) -> SLItem.fromStatic(sta)));
    }

    // need better object hierarchy here
    private List<SLStatic> getObjectsAsStaticAt(Point2D point) {
        return getObjectsAt(point)
            .filter((o) -> o instanceof SLItem)
            .map((itm) -> new SLStatic(itm.getSerial(), itm.getGraphic(), itm.getLocation(), 0))
            .collect(Collectors.toList());
    }

    public void playerMoveRequest(Direction dir) {
        if(lastMoveTime + MOVE_DELAY > getTimeMillis()) {
            // too fast
            return;
        }

        if(nextMoveSequence - lastAckedMoveSequence > 3) {
            log.finer("Disallowing move due to missing acks, want seq: " + nextMoveSequence + ", last ack: " + lastAckedMoveSequence);
            return;
        }

        if(player.getFacing() != dir) {
            // only turning
            MoveRequestPacket packet = new MoveRequestPacket(dir, nextMoveSequence++, false);
            connection.sendPacket(packet);
            player.setFacing(dir);
            lastMoveTime = getTimeMillis();
        } else {
            // actual move
            Point3D oldLoc = player.getLocation();
            Point3D newLoc = SLData.get().getElevatedPoint(oldLoc, dir, (point) -> getObjectsAsStaticAt(point));
            if(newLoc != null) {
                MoveRequestPacket packet = new MoveRequestPacket(dir, nextMoveSequence++, false);
                connection.sendPacket(packet);
                player.setLocation(newLoc);
                lastMoveTime = getTimeMillis();
            }
        }
        if(nextMoveSequence > 255) {
            nextMoveSequence = 0;
        }
    }

    public void playerTextInput(String text) {
        SpeechRequestPacket packet = new SpeechRequestPacket(text, 0x0000FF, SpeechRequestPacket.MODE_BARK);
        connection.sendPacket(packet);
    }

    public void allowMove(short sequence) {
        lastAckedMoveSequence = sequence;
    }

    public void denyMove(short deniedSequence, Point3D location, Direction facing) {
        nextMoveSequence = 0;
        lastAckedMoveSequence = 0;
        player.setLocation(location);
        player.setFacing(facing);
    }

    private void onPlayerLocationChange(Point3D oldLoc, Point3D newLoc) {
        removeOutOfRangeObjects();
    }

    private void removeOutOfRangeObjects() {
        // remove objects that are no longer visible
        objectsInRange.removeFarther(player.getLocation(), updateRange);
    }

    public void updateOrInitObject(SendableObject object, Direction facing, int amount) {
        // valid in object: serial, graphic, location, hue
        SLObject updatedObj = objectsInRange.getObjectBySerial(object.getSerial());
        if(updatedObj == null) {
            // init new object
            if(object.getSerial() >= Items.SERIAL_FIRST) {
                updatedObj = new SLItem(object.getSerial(), object.getGraphic());
            } else {
                updatedObj = new SLMobile(object.getSerial(), object.getGraphic());
            }
            objectsInRange.registerObject(updatedObj);
        }
        updatedObj.setGraphic(object.getGraphic());
        updatedObj.setLocation(object.getLocation());
        updatedObj.setHue(object.getHue());
        if(updatedObj instanceof SLItem) {
            ((SLItem) updatedObj).setAmount(amount);
        } else if(updatedObj instanceof SLMobile) {
            ((SLMobile) updatedObj).setFacing(facing);
        }
        removeOutOfRangeObjects();
    }

    public void queryMobileInformation(SLMobile mob) {
        SingleClickPacket packet = new SingleClickPacket(mob.getSerial());
        connection.sendPacket(packet);
    }

    public void doubleClick(SLObject obj) {
        DoubleClickPacket packet = new DoubleClickPacket(obj.getSerial());
        connection.sendPacket(packet);
    }

    public void equipItem(SendableMobile mobInfo, SendableItem itemInfo) {
        SLObject obj = objectsInRange.getObjectBySerial(mobInfo.getSerial());
        // if the item is visible or anything is known about it, forget it now
        objectsInRange.removeObject(itemInfo.getSerial());

        if(obj == null || !(obj instanceof SLMobile)) {
            log.fine("Equip received for unknown serial " + String.format("%08X", mobInfo.getSerial()));
            return;
        }
        SLMobile mob = (SLMobile) obj;
        SLItem itm = new SLItem(itemInfo.getSerial(), itemInfo.getGraphic());
        itm.setLayer(itemInfo.getLayer());
        itm.setHue(itemInfo.getHue());
        mob.equip(itm);
        objectsInRange.registerObject(itm);
    }

    public void addItemToContainer(SendableItem itemInfo, SLItem container) {
        // forget about the item as it has a new state now
        objectsInRange.removeObject(itemInfo.getSerial());

        SLItem itm = new SLItem(itemInfo.getSerial(), itemInfo.getGraphic());
        itm.setHue(itemInfo.getHue());
        itm.setAmount(itemInfo.getAmount());
        itm.setLocation(itemInfo.getLocation());
        container.addToContainer(itm);
        objectsInRange.registerObject(itm);
    }

    public boolean hasPlayer() {
        return player != null;
    }

    public void setPlayerGraphic(int graphic) {
        player.setGraphic(graphic);
    }

    public void setPlayerLocation(Point3D location) {
        Point3D oldLoc = player.getLocation();
        player.setLocation(location);
        onPlayerLocationChange(oldLoc, location);
    }

    public void setPlayerFacing(Direction facing) {
        player.setFacing(facing);
    }

    public long getPlayerSerial() {
        return player.getSerial();
    }

    public Point3D getPlayerLocation() {
        return player.getLocation();
    }

    public long getTimeMillis() {
        return (Sys.getTime() * 1000) / Sys.getTimerResolution();
    }

    public void removeObject(long serial) {
        objectsInRange.removeObject(serial);
    }

    public SLObject getObjectBySerial(long serial) {
        return objectsInRange.getObjectBySerial(serial);
    }

    public boolean isPlayerInWarMode() {
        return player.isInWarMode();
    }

    public void toggleWarmode() {
        player.setWarMode(!player.isInWarMode());
    }
} public enum State {DISCONNECTED, CONNECTED, LOGGED_IN}; public class SLItem extends SLObject implements SendableItem {
    private short layer, facingOverride;
    private int amount;
    private StaticTile tileInfo;
    private boolean isStatic;
    private SLObject parent;
    private final Set<SLItem> containerContents;

    public SLItem(long serial, int graphic) {
        super(serial, graphic);
        isStatic = false;
        containerContents = new HashSet<SLItem>();
    }

    public static SLItem fromStatic(SLStatic stat) {
        SLItem res = new SLItem(stat.getSerial(), stat.getStaticID());
        res.setLocation(stat.getLocation());
        res.setHue(stat.getHue());
        res.setLayer(res.tileInfo.layer);
        res.setName(res.tileInfo.name);
        res.setFacingOverride((short) 0);
        res.setAmount(1);
        res.isStatic = true;
        return res;
    }

    @Override
    public void setGraphic(int graphic) {
        super.setGraphic(graphic);
        tileInfo = SLData.get().getTiles().getStaticTile(graphic);
        if(tileInfo != null && tileInfo.name != null) {
            setName(tileInfo.name);
        }
    }

    public void setAmount(int amount) {
        this.amount = amount;
    }

    @Override
    public int getAmount() {
        return amount;
    }

    public void setLayer(short layer) {
        this.layer = layer;
    }

    @Override
    public short getLayer() {
        return layer;
    }

    public void setFacingOverride(short override) {
        this.facingOverride = override;
    }

    @Override
    public short getFacingOverride() {
        return facingOverride;
    }

    public StaticTile getTileInfo() {
        return tileInfo;
    }

    public void setWorn(SLMobile byWhom) {
        this.parent = byWhom;
    }

    @Override
    public void unregister() {
        if(parent instanceof SLItem) {
            ((SLItem) parent).removeFromContainer(this);
        } else if(parent instanceof SLMobile) {
            ((SLMobile) parent).unequip(this);
        }
        super.unregister();
    }

    public void setContainer(SLItem container) {
        this.parent = container;
    }

    public boolean isStatic() {
        return isStatic;
    }

    public boolean isOnGround() {
        return parent == null;
    }

    public boolean isWorn() {
        return parent instanceof SLMobile;
    }

    public boolean isInContainer() {
        return parent instanceof SLItem;
    }

    public void addToContainer(SLItem itm) {
        containerContents.add(itm);
        itm.setContainer(this);
    }

    public void removeFromContainer(SLItem itm) {
        containerContents.remove(itm);
        itm.setContainer(null);
    }

    public Set<SLItem> getContainerContents() {
        return Collections.unmodifiableSet(containerContents);
    }
} public abstract class SLObject implements SendableObject {
    protected long serial;
    protected Point3D location;
    protected int graphic, hue;
    protected String name;
    protected boolean registered;

    public SLObject(long serial, int graphic) {
        setSerial(serial);
        setGraphic(graphic);
        registered = false;
    }

    public void register() {
        registered = true;
    }

    public void unregister() {
        registered = false;
    }

    public long getSerial() {
        return serial;
    }

    public void setSerial(long serial) {
        this.serial = serial;
    }

    public Point3D getLocation() {
        return location;
    }

    public void setLocation(Point3D location) {
        this.location = location;
    }

    public int getGraphic() {
        return graphic;
    }

    public void setGraphic(int graphic) {
        this.graphic = graphic;
    }

    public int getHue() {
        return hue;
    }

    public void setHue(int hue) {
        this.hue = hue;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
} public interface ConnectionHandler {
    void onConnected();
    void onIncomingPacket(SLPacket packet);
    void onRemoteDisconnect();
    void onNetworkError(String reason);
}
import java.io.IOException;
import java.nio.channels.UnresolvedAddressException;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.solhost.folko.uosl.libuosl.network.SendableItem;
import org.solhost.folko.uosl.libuosl.network.SendableMobile;
import org.solhost.folko.uosl.libuosl.network.SendableObject;
import org.solhost.folko.uosl.libuosl.network.packets.AllowMovePacket;
import org.solhost.folko.uosl.libuosl.network.packets.DenyMovePacket;
import org.solhost.folko.uosl.libuosl.network.packets.EquipPacket;
import org.solhost.folko.uosl.libuosl.network.packets.FullItemsContainerPacket;
import org.solhost.folko.uosl.libuosl.network.packets.InitPlayerPacket;
import org.solhost.folko.uosl.libuosl.network.packets.ItemInContainerPacket;
import org.solhost.folko.uosl.libuosl.network.packets.LocationPacket;
import org.solhost.folko.uosl.libuosl.network.packets.LoginErrorPacket;
import org.solhost.folko.uosl.libuosl.network.packets.OpenGumpPacket;
import org.solhost.folko.uosl.libuosl.network.packets.RemoveObjectPacket;
import org.solhost.folko.uosl.libuosl.network.packets.SLPacket;
import org.solhost.folko.uosl.libuosl.network.packets.SendObjectPacket;
import org.solhost.folko.uosl.libuosl.network.packets.SendTextPacket;
import org.solhost.folko.uosl.libuosl.network.packets.SoundPacket;
import org.solhost.folko.uosl.slclient.models.Connection;
import org.solhost.folko.uosl.slclient.models.GameState;
import org.solhost.folko.uosl.slclient.models.GameState.State;
import org.solhost.folko.uosl.slclient.models.SLItem;
import org.solhost.folko.uosl.slclient.models.SLObject;
import org.solhost.folko.uosl.slclient.models.Connection.ConnectionHandler;
package org.solhost.folko.uosl.slclient.controllers;



public class NetworkController implements ConnectionHandler {
    private static final Logger log = Logger.getLogger("slclient.network");
    private final MainController mainController;
    private final GameState game;
    private final Queue<SLPacket> pendingPackets;
    private Connection connection;
    private long loginSerial;

    public NetworkController(MainController mainController) {
        this.mainController = mainController;
        this.pendingPackets = new ConcurrentLinkedQueue<SLPacket>();
        this.game = mainController.getGameState();
    }

    public void tryConnect(String host) {
        try {
            connection = new Connection(this, host);
        } catch (NumberFormatException e) {
            mainController.onNetworkError("Invalid port");
        } catch (UnresolvedAddressException e) {
            mainController.onNetworkError("Unknown host");
        } catch (IllegalArgumentException e) {
            mainController.onNetworkError("Invalid host");
        } catch (IOException e) {
            mainController.onNetworkError(e.getMessage());
        } catch (Exception e) {
            mainController.onNetworkError(e.getMessage());
        }
    }

    public void stopNetwork() {
        if(connection != null) {
            log.fine("Stopping network");
            connection.disconnect();
            game.onDisconnect();
            connection = null;
        }
    }

    @Override
    public void onConnected() {
        game.onConnect(connection);
    }

    @Override
    public void onNetworkError(String reason) {
        log.warning("Network error: " + reason);
        mainController.onNetworkError(reason);
        game.onDisconnect();
    }

    @Override
    public void onRemoteDisconnect() {
        log.warning("Remote disconnected");
        game.onDisconnect();
    }

    private void onLoginFail(LoginErrorPacket packet) {
        String message;
        switch(packet.getReason()) {
        case LoginErrorPacket.REASON_PASSWORD:
            message = "Invalid Password";
            break;
        case LoginErrorPacket.REASON_CHAR_NOT_FOUND:
            message = "Character not found";
            break;
        default:
            message = "Unknown reason";
            break;
        }
        mainController.onLoginFail(message);
    }

    private void onInitPlayer(InitPlayerPacket packet) {
        // set player serial
        loginSerial = packet.getSerial();
    }

    private void onLocationChange(LocationPacket packet) {
        SendableMobile src = packet.getMobile();
        if(game.getState() != State.LOGGED_IN) {