public class SLData {
    public static boolean DEBUG_MOVE = false;
    public static final int CHARACHTER_HEIGHT = 10; // height of a character
    private static SLData instance;
    private final String dataPath;
    private SLMap map;
    private SLPalette palette;
    private SLStatics statics;
    private SLSound sound;
    private SLArt art;
    private SLGumps gumps;
    private SLTiles tiles;

    private SLData(String dataPath) {
        this.dataPath = dataPath;
    }

    public synchronized static SLData init(String dataPath) throws IOException {
        if(instance != null) {
            throw new RuntimeException("client data already initialized");
        }
        SLData newInstance = new SLData(dataPath);
        newInstance.load();
        instance = newInstance;
        return newInstance;
    }

    public static SLData get() {
        return instance;
    }

    public void buildCaches() {
        tiles.buildCache();
        map.buildCache();
        statics.buildCache();
    }

    private void load() throws IOException {
        map = new SLMap(dataPath +          "/MAP0.MUL");
        palette = new SLPalette(dataPath +  "/PALETTE.MUL");
        statics = new SLStatics(dataPath +  "/STATICS0.MUL", dataPath + "/STAIDX0.MUL");
        sound = new SLSound(dataPath +      "/SOUND.MUL", dataPath + "/SOUNDIDX.MUL");
        art = new SLArt(dataPath +          "/ART.MUL", dataPath + "/ARTIDX.MUL", dataPath + "/ANIMDATA.MUL");
        tiles = new SLTiles(dataPath +      "/TILEDATA.MUL");
        gumps = new SLGumps(dataPath +      "/GUMPS.MUL");
    }

    // reverse engineered from the client, sub_4061A0
    // this _should_ be written more readable, but it is very important
    // that the semantic doesn't change. Otherwise, client and server would
    // become out of sync and NPCs could walk through statics or bad things like that
    private Point3D getElevatedPointReal(Point3D source, Direction dir, ObjectLister lister) {
        Point2D dest = source.getTranslated(dir);

        int currentZ = source.getZ();
        int currZp9 = currentZ + 9;
        int finalZ = -128;
        int edi = -128;
        boolean staticsAllowWalking = false;
        for(SLStatic stat : lister.getStaticsAndDynamicsAtLocation(dest)) {
            StaticTile tile = tiles.getStaticTile(stat.getStaticID());
            int eax = stat.getLocation().getZ();
            if(eax > currZp9) {
                // starts above us -> ignore
                continue;
            }
            // is lastStat.blocks: break
            eax = 1;
            int ecx = tile.height;
            if(ecx != 0) {
                eax = ecx;
            }
            int ebx = stat.getLocation().getZ();
            int edx = eax + ebx;
            if(edx > edi) {
                if(tile.isStair()) {
                    // 40627B
                    eax = ebx + ecx;
                    if(currZp9 >= eax) {
                        ebx += ecx;
                        edi = edx;
                        staticsAllowWalking = true;
                        finalZ = ebx;
                        continue;
                    }
                }
                if(tile.isSurface()) {
                    // 406284
                    eax = ebx + ecx;
                    if(currentZ >= eax) {
                        ebx += ecx;
                        edi = edx;
                        staticsAllowWalking = true;
                        finalZ = ebx;
                        continue;
                    }
                }
                if(tile.isImpassable()) {
                    staticsAllowWalking = false;
                    edi = edx;
                    continue;
                }
                eax = currentZ;
                eax++;
                if(eax >= edx) {
                    continue;
                }
                eax = currZp9;
                eax += 3;
                if(eax < edx) {
                    continue;
                }
                staticsAllowWalking = false;
                edi = edx;
                continue;
            } else {
                if(edx != edi) {
                    continue;
                } else {
                    if(tile.isImpassable()) {
                        staticsAllowWalking = false;
                    }
                }
            }
        }
        if(edi > -128) {
            // there are statics in our way
            if(staticsAllowWalking) {
                return new Point3D(dest, finalZ);
            } else {
                return null;
            }
        }

        // statics are ok, need to check land

        // maybe todo: if lastStat.blocks: return false;

        LandTile tile = tiles.getLandTile(map.getTextureID(dest));
        if(tile.isImpassable()) {
            return null;
        }

        finalZ = map.getTileElevation(dest);

        int resX = dest.getX();
        int resY = dest.getY();
        int resZ = finalZ;

        // blacklist because of map errors where players can escape the test area
        if((resX == 432 && (resY == 724 || resY == 723 || resY == 722) && resZ == -15) ||
            (resX == 334 && resY == 707)) {
            return null;
        }

        return new Point3D(resX, resY, resZ);
    }

    // when standing at "from" and moving in direction "dir", what's the effective 3D point?
    // returns null if impassable
    public Point3D getElevatedPoint(Point3D source, Direction dir, ObjectLister lister) {
        switch(dir) {
        case NORTH:
        case WEST:
        case SOUTH:
        case EAST:
            // straight movement: only check target
            return getElevatedPointReal(source, dir, lister);
        case NORTH_EAST:
            if(getElevatedPointReal(source, Direction.NORTH, lister) == null) {
                return null;
            } else if(getElevatedPointReal(source, Direction.EAST, lister) == null) {
                return null;
            } else {
                return getElevatedPointReal(source, dir, lister);
            }
        case NORTH_WEST:
            if(getElevatedPointReal(source, Direction.NORTH, lister) == null) {
                return null;
            } else if(getElevatedPointReal(source, Direction.WEST, lister) == null) {
                return null;
            } else {
                return getElevatedPointReal(source, dir, lister);
            }
        case SOUTH_EAST:
            if(getElevatedPointReal(source, Direction.SOUTH, lister) == null) {
                return null;
            } else if(getElevatedPointReal(source, Direction.EAST, lister) == null) {
                return null;
            } else {
                return getElevatedPointReal(source, dir, lister);
            }
        case SOUTH_WEST:
            if(getElevatedPointReal(source, Direction.SOUTH, lister) == null) {
                return null;
            } else if(getElevatedPointReal(source, Direction.WEST, lister) == null) {
                return null;
            } else {
                return getElevatedPointReal(source, dir, lister);
            }
        default: return null;
        }
    }

    public SLMap getMap() {
        return map;
    }

    public SLPalette getPalette() {
        return palette;
    }

    public SLSound getSound() {
        return sound;
    }

    public SLArt getArt() {
        return art;
    }

    public SLTiles getTiles() {
        return tiles;
    }

    public SLGumps getGumps() {
        return gumps;
    }

    public SLStatics getStatics() {
        return statics;
    }

    // get all points on a direct path from src to dest
    public List<Point3D> getDirectPath(Point3D src, Point3D dest) {
        List<Point3D> path = new LinkedList<Point3D>();

        double dirVec[] = {dest.getX() - src.getX(), dest.getY() - src.getY(), dest.getZ() - src.getZ()};
        double norm = Math.sqrt(dirVec[0] * dirVec[0] + dirVec[1] * dirVec[1] + dirVec[2] * dirVec[2]);
        dirVec[0] /= norm;
        dirVec[1] /= norm;
        dirVec[2] /= norm;

        double x = src.getX(),
               y = src.getY(),
               z = src.getZ();

        Point3D addedLast = src;
        while(!addedLast.equals(dest)) {
            Point3D next = new Point3D((int) Math.round(x), (int) Math.round(y), (int) Math.round(z));

            if(!addedLast.equals(next)) {
                path.add(next);
                addedLast = next;
            }

            x += dirVec[0];
            y += dirVec[1];
            z += dirVec[2];
        }

        return path;
    }

    public boolean hasLineOfSight(Point3D src, Point3D dest, int maxDistance, ObjectLister lister) {
        int distance = src.distanceTo(dest);
        if(distance > maxDistance) {
            return false;
        } else if(distance == 0) {
            return true;
        }

        List<Point3D> path = getDirectPath(src, dest);
        for(Point3D point : path) {
            for(SLStatic obj : lister.getStaticsAndDynamicsAtLocation(point)) {
                StaticTile stat = tiles.getStaticTile(obj.getStaticID());
                int lowerZ = obj.getLocation().getZ();
                int upperZ = lowerZ + stat.height;
                if(lowerZ <= point.getZ() && upperZ >= point.getZ()) {
                    // there's a static that potentially blocks the way.
                    if(!stat.isSurface()) {
                        return false;
                    } else {
                        // a surface only blocks if it is between on Z
                        int surfZ = obj.getLocation().getZ();
                        if((src.getZ() < surfZ && dest.getZ() >= surfZ) || (dest.getZ() < surfZ && src.getZ() >= surfZ)) {
                            return false;
                        }
                    }
                }
            }
        }
        return true;
    }

    public String getDataPath() {
        return dataPath;
    }
} public enum Direction {
    NORTH, NORTH_EAST, EAST, SOUTH_EAST, SOUTH, SOUTH_WEST, WEST, NORTH_WEST;

    public static Direction parse(short b) {
        switch(b) {
        case 0: return NORTH;
        case 1: return NORTH_EAST;
        case 2: return EAST;
        case 3: return SOUTH_EAST;
        case 4: return SOUTH;
        case 5: return SOUTH_WEST;
        case 6: return WEST;
        case 7: return NORTH_WEST;
        default: return NORTH;
        }
    }

    public short toByte() {
        switch(this) {
        case NORTH:         return 0;
        case NORTH_EAST:    return 1;
        case EAST:          return 2;
        case SOUTH_EAST:    return 3;
        case SOUTH:         return 4;
        case SOUTH_WEST:    return 5;
        case WEST:          return 6;
        case NORTH_WEST:    return 7;
        default:            return 0;
        }
    }

    // angle must be in [0, 360]. 0 is north west and positive angles turn clockwise
    public static Direction fromAngle(double angle) {
        double r = 360 / 16.0;
        if(angle < 45 - r) {
            return Direction.NORTH_WEST;
        } else if(angle < 90 - r) {
            return Direction.NORTH;
        } else if(angle < 135 - r) {
            return Direction.NORTH_EAST;
        } else if(angle < 180 - r) {
            return Direction.EAST;
        } else if(angle < 225 - r) {
            return Direction.SOUTH_EAST;
        } else if(angle < 270 - r) {
            return Direction.SOUTH;
        } else if(angle < 315 - r) {
            return Direction.SOUTH_WEST;
        } else if(angle < 360 - r) {
            return Direction.WEST;
        } else {
            return Direction.NORTH_WEST;
        }
    }

    public int getFrameIndex() {
        switch(this) {
        case NORTH:         return 3;
        case NORTH_EAST:    return 2;
        case EAST:          return 1;
        case SOUTH_EAST:    return 0;
        case SOUTH:         return 1;
        case SOUTH_WEST:    return 2;
        case WEST:          return 3;
        case NORTH_WEST:    return 4;
        default:            return -1;
        }
    }

    public Direction getOpposingDirection() {
        switch(this) {
        case NORTH:         return SOUTH;
        case NORTH_EAST:    return SOUTH_WEST;
        case EAST:          return WEST;
        case SOUTH_EAST:    return NORTH_WEST;
        case SOUTH:         return NORTH;
        case SOUTH_WEST:    return NORTH_EAST;
        case WEST:          return EAST;
        case NORTH_WEST:    return SOUTH_EAST;
        default:            return SOUTH;
        }
    }
} public class Gumps {
    public static final int ID_CORPSE      = 0x0009;
    public static final int ID_PAPERDOLL   = 0x000A;
    public static final int ID_SHOP        = 0x0030;
    public static final int ID_BACKPACK    = 0x003C;
    public static final int ID_BAG         = 0x003D;
    public static final int ID_BARREL      = 0x003E;
    public static final int ID_BOX         = 0x003F;
    public static final int ID_SMALL_BOX   = 0x0040;
    public static final int ID_BASKET      = 0x0041;
    public static final int ID_METAL_CHEST = 0x0042;
    public static final int ID_WOOD_CHEST  = 0x0043;
    public static final int ID_CRATE       = 0x0044;
    public static final int ID_SPELLBOOK   = 0xFFFF;

    public static Rectangle getGumpDimensions(int gumpID) {
        switch(gumpID) {
        case ID_BACKPACK:   return new Rectangle(44, 65, 98, 85);
        case ID_BAG:        return new Rectangle(29, 34, 64, 85);
        case ID_BASKET:     return new Rectangle(35, 38, 66, 69);
        case ID_BOX:        return new Rectangle(16, 45, 92, 71);
        case ID_SMALL_BOX:  return new Rectangle(19, 47, 106, 67);
        case ID_METAL_CHEST:return new Rectangle(14, 105, 108, 68);
        case ID_WOOD_CHEST: return new Rectangle(16, 51, 133, 65);
        case ID_BARREL:     return new Rectangle(33, 36, 65, 103);
        case ID_CORPSE:     return new Rectangle(4, 85, 89, 80);
        default: return new Rectangle(50, 60, 20, 20);
        }
    }

    public static boolean isContainerGump(int gumpID) {
        switch(gumpID) {
        case ID_BACKPACK:   return true;
        case ID_BAG:        return true;
        case ID_BASKET:     return true;
        case ID_BOX:        return true;
        case ID_SMALL_BOX:  return true;
        case ID_METAL_CHEST:return true;
        case ID_WOOD_CHEST: return true;
        case ID_BARREL:     return true;
        case ID_CORPSE:     return true;
        default: return false;
        }
    }

    public static int getItemGump(int graphicID) {
        switch(graphicID) {
        case Items.GFX_BACKPACK:        return ID_BACKPACK;
        case Items.GFX_BAG:             return ID_BAG;
        case Items.GFX_BASKET:          return ID_BASKET;
        case Items.GFX_BOX:             return ID_BOX;
        case Items.GFX_SMALL_BOX:       return ID_SMALL_BOX;
        case Items.GFX_BUSHEL_BASKET:   return ID_BASKET;
        case Items.GFX_METAL_CHEST:     return ID_METAL_CHEST;
        case Items.GFX_WOOD_CHEST:      return ID_WOOD_CHEST;
        case Items.GFX_BARREL:          return ID_BARREL;

        case Items.GFX_CORPSE_DEER:
        case Items.GFX_CORPSE_HUMAN:
        case Items.GFX_CORPSE_ORC:
        case Items.GFX_CORPSE_ORC_CAPTAIN:
        case Items.GFX_CORPSE_RABBIT:
        case Items.GFX_CORPSE_SKELETON:
        case Items.GFX_CORPSE_WOLF:
                                        return ID_CORPSE;

        case Items.GFX_SPELLBOOK:       return ID_SPELLBOOK;

        default:                        return ID_BACKPACK;
        }
    }
} public class Point3D extends Point2D {
    private static final long serialVersionUID = 1L;
    public static final int MIN_ELEVATION = -128;
    public static final int MAX_ELEVATION = 127;
    protected final int z;

    public Point3D(int x, int y) {
        super(x, y);
        this.z = 0;
    }

    public Point3D(int x, int y, int z) {
        super(x, y);
        if(z < MIN_ELEVATION || z > MAX_ELEVATION) {
            throw new IllegalArgumentException("invalid z coordinate: " + z);
        }
        this.z = z;
    }

    public Point3D(Point2D d2, int z) {
        super(d2.x, d2.y);
        if(z < MIN_ELEVATION || z > MAX_ELEVATION) {
            throw new IllegalArgumentException("invalid z coordinate: " + z);
        }
        this.z = z;
    }

    public int getZ() {
        return z;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = super.hashCode();
        result = prime * result + z;
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (!super.equals(obj))
            return false;
        if (!(obj instanceof Point3D))
            return false;
        Point3D other = (Point3D) obj;
        if (z != other.z)
            return false;
        return true;
    }

    @Override
    public String toString() {
        return String.format("<Point3D: x = %d, y = %d, z = %d>", x, y, z);
    }
} public class MainController {
    private static final Logger log = Logger.getLogger("slclient.main");
    private final GameState game;
    private final NetworkController networkController;
    private final SoundManager soundManager;

    private boolean gameLoopRunning;
    private final MainView mainView;
    private final LoginDialog loginView;
    private final GameView gameView;

    public MainController() {
        this.game = new GameState();
        this.networkController = new NetworkController(this);
        this.soundManager = new SoundManager(game);
        this.mainView = new MainView(this);

        gameView = mainView.getGameView();
        loginView = mainView.getLoginView();

        game.addStateListener(this::onGameStateChange);
        mainView.setVisible(true);
    }

    public void showLoginScreen() {
        mainView.showLoginDialog();
    }

    private void onGameStateChange(State oldState, State newState) {
        log.fine("Game state changed from " + oldState + " to " + newState);
        switch(newState) {
        case DISCONNECTED:
            onDisconnect(oldState);
            break;
        case CONNECTED:
            onConnected(oldState);
            break;
        case LOGGED_IN:
            onLogin(oldState);
            break;
        default:
            break;
        }
    }

    // user entered login data
    public void onLoginRequest(String host, String name, String password) {
        loginView.setBusy(true);
        game.setLoginDetails(name, password);
        networkController.tryConnect(host);
    }

    private void onDisconnect(GameState.State oldState) {
        if(oldState == State.CONNECTED) {
            // login view still active, but server kicked us
            loginView.setBusy(false);
        }
    }

    private void onConnected(GameState.State oldState) {
        log.info("Connected to server");
        game.tryLogin();
    }

    public void onLoginFail(String message) {
        networkController.stopNetwork();
        loginView.showError("Login failed: " + message);
        loginView.setBusy(false);
    }

    private void onLogin(State oldState) {
        loginView.close();
    }

    public void onNetworkError(String reason) {
        if(game.getState() == State.DISCONNECTED || game.getState() == State.CONNECTED) {
            // means we couldn't connect or were kicked while logging in
            loginView.showError("Couldn't connect: " + reason);
            loginView.setBusy(false);
        }
    }

    public void onGameError(String reason) {
        log.severe("Game error: " + reason);
        onGameWindowClosed();
    }

    // can be called from Swing thread or game thread
    public synchronized void onGameWindowClosed() {
        if(gameLoopRunning) {
            log.fine("Stopping game...");
            gameLoopRunning = false;
        }
        mainView.close();
    }

    // this is the main game thread, everything in GameView and GameState should run
    // in this thread. Events can be posted using scheduleUpdate
    public void gameLoop() {
        long lastFrameTime = game.getTimeMillis();
        long thisFrameTime = game.getTimeMillis();
        gameLoopRunning = true;

        try {
            startSystems();
            log.fine("Entering game loop");
            while(gameLoopRunning) {
                thisFrameTime = game.getTimeMillis();
                update(thisFrameTime - lastFrameTime);
                gameView.render();
                gameView.pause();
                lastFrameTime = thisFrameTime;
            }
            log.fine("Left game loop");
            shutdownSystems();
        } catch(Exception e) {
            log.log(Level.SEVERE, "Game crashed: " + e.getMessage(), e);
            onGameError("Game crashed: " + e.getMessage());
        }
    }

    private void startSystems() throws Exception {
        gameView.init();
        soundManager.init();
    }

    private void shutdownSystems() {
        soundManager.stop();
        networkController.stopNetwork();
    }

    private void update(long elapsedMillis) {
        try {
            networkController.update(elapsedMillis);
            gameView.update(elapsedMillis);
            soundManager.update(elapsedMillis);
        } catch(Exception e) {
            log.log(Level.SEVERE, "Exception in game: " + e.getMessage(), e);
            onGameError("Game has crashed: " + e.getMessage());
            return;
        }
    }

    public void onRequestMove(Direction dir) {
        game.playerMoveRequest(dir);
    }

    public void onUpdateRangeChange(int sceneRadius) {
        game.setUpdateRange(sceneRadius);
    }

    public void onTextEntered(String text) {
        if(text.length() > 0) {
            game.playerTextInput(text);
        }
    }

    public void onSingleClickObject(SLObject obj) {
        if(obj instanceof SLItem) {
            String name = obj.getName();
            if(name.length() > 0) {
                gameView.showTextAbove(obj, name, Color.WHITE);
            }
        } else if(obj instanceof SLMobile) {
            game.queryMobileInformation((SLMobile) obj);
        }
    }

    public void onDoubleClickObject(SLObject obj) {
        game.doubleClick(obj);
    }

    public void incomingSysMsg(String text, long color) {
        log.fine("SysMessage: " + text);
        int col = Integer.reverseBytes((int) color) >> 8;
        gameView.showSysMessage(text, new Color(col));
    }

    public void incomingSee(SLObject obj, String name, String text, long color) {
        if(obj == null) {
            log.warning("Received 'you see' message for unknown object");
            return;
        }
        // TODO: log to journal with name
        int col = Integer.reverseBytes((int) color) >> 8;
        gameView.showTextAbove(obj, text, new Color(col));
    }

    public void incomingSpeech(SLObject obj, String name, String text, long color) {
        if(obj == null) {
            // TODO: display in lower left or something
            log.warning("Received speech for unknown object");
            return;
        }
        // TODO: log to journal with name
        int col = Integer.reverseBytes((int) color) >> 8;
        gameView.showTextAbove(obj, text, new Color(col));
    }

    public void incomingSound(int soundID) {
        soundManager.playSound(soundID);
    }

    public void onReportFPS(long fps) {
        mainView.setTitleSuffix("| " + fps + " FPS");
    }

    public GameState getGameState() {
        return game;
    }

    public void setFrameLimit(int limit) {
        gameView.setFrameLimit(limit);
    }

    public void setEnableMusic(boolean state) {
        soundManager.setEnableMusic(state);
    }

    public void incomingGump(long serial, int gumpID) {
        SLObject obj = game.getObjectBySerial(serial);
        if(obj == null) {
            log.warning("Gump " + gumpID + " for unknown object " + serial);
            return;
        }
        gameView.openGump(obj, gumpID);
    }

    public void onOpenBackscroll() {
        log.warning("Backscroll not supported yet");
    }

    public void onOpenSkillWindow() {
        log.warning("Skill window not supported yet");
    }

    public void onOpenStatus(SLMobile mob, boolean fullStatus) {
        log.warning("Status gump not supported yet");
    }

    public void onToggleWarMode(SLMobile mob) {
        if(mob.getSerial() == game.getPlayerSerial()) {
            game.toggleWarmode();
        } else {
            log.warning("Warmode toggled for non-player mobile");
        }
    }
} 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 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 class SLMobile extends SLObject implements SendableMobile {
    private final Map<Attribute, Long> attributes;
    private final Map<Short, SLItem> equipment;
    private Direction facing;

    public SLMobile(long serial, int graphic) {
        super(serial, graphic);
        attributes = new HashMap<>();
        for(Attribute attr : Attribute.values()) {
            attributes.put(attr, 0L);
        }
        equipment = new HashMap<>();
    }

    @Override
    public long getAttribute(Attribute attr) {
        return attributes.get(attr);
    }

    public void setAttribute(Attribute attr, long value) {
        attributes.put(attr, value);
    }

    public void setFacing(Direction dir) {
        this.facing = dir;
    }

    @Override
    public Direction getFacing() {
        return facing;
    }

    public void equip(SLItem itm) {
        equipment.put(itm.getLayer(), itm);
        itm.setWorn(this);
    }

    public Collection<SLItem> getEquipment() {
        return Collections.unmodifiableCollection(equipment.values());
    }

    public void unequip(SLItem itm) {
        equipment.remove(itm.getLayer());
        itm.setWorn(null);
    }
} 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 class TexturePool {
    private static final Logger log = Logger.getLogger("slclient.texturepool");
    private static TextureAtlas landAtlas, staticAtlas;
    private static Texture[] animationFrames;
    private static Map<Integer, Texture> gumps;

    private TexturePool() {
    }

    private static void createLandAtlas() {
        SLArt art = SLData.get().getArt();
        SLMap map = SLData.get().getMap();

        // we need tile 0 to display invalid tiles, so always load it
        landAtlas.addImage(art.getLandArt(0).image, 0);

        // but otherweise only put tiles into the atlas that are actually in use by the map
        for(int x = 0; x < SLMap.MAP_WIDTH; x++) {
            for(int y = 0; y < SLMap.MAP_HEIGHT; y++) {
                int tileId = map.getTextureID(new Point2D(x, y));
                if(landAtlas.hasEntry(tileId)) {
                    continue;
                }
                ArtEntry entry = art.getLandArt(tileId);
                if(entry != null) {
                    landAtlas.addImage(entry.image, tileId);
                }
            }
        }
        landAtlas.generateTextureAndFreeImage();
    }

    private static void createStaticAtlas() {
        SLArt art = SLData.get().getArt();

        for(int i = 0; i < SLArt.NUM_STATIC_ARTS; i++) {
            StaticTile tile = SLData.get().getTiles().getStaticTile(i);
            if(tile == null) {
                continue;
            }
            boolean translucent = (tile.flags & StaticTile.FLAG_TRANSLUCENT) != 0;
            ArtEntry entry = art.getStaticArt(i, translucent);
            if(entry != null && entry.image != null) {
                staticAtlas.addImage(entry.image, i);
            }
        }
        staticAtlas.generateTextureAndFreeImage();
    }

    public static void load() {
        SLArt art = SLData.get().getArt();
        animationFrames = new Texture[SLArt.NUM_ANIMATION_ARTS];
        gumps = new HashMap<>();

        int maxWidth = GL11.glGetInteger(GL11.GL_MAX_TEXTURE_SIZE);
        int maxHeight = maxWidth;

        log.fine(String.format("Loading textures into GPU, max atlas size %dx%d...", maxWidth, maxHeight));

        landAtlas = new TextureAtlas(maxWidth, maxHeight);
        staticAtlas = new TextureAtlas(maxWidth, maxHeight);

        createLandAtlas();
        createStaticAtlas();

        for(int i = 0x4000; i < 0x4000 + SLArt.NUM_ANIMATION_ARTS; i++) {
            ArtEntry entry = art.getStaticArt(i, false);
            if(entry != null && entry.image != null) {
                animationFrames[i - 0x4000] = new Texture(entry.image);
            }
        }

        for(int id : SLData.get().getGumps().getAllGumpIDs()) {
            gumps.put(id, new Texture(SLData.get().getGumps().getGump(id).image));
        }
        log.fine("Done loading textures");
    }

    public static TextureAtlas getLandAtlas() {
        return landAtlas;
    }

    public static TextureAtlas getStaticAtlas() {
        return staticAtlas;
    }

    public static Texture getAnimationFrame(int id) {
        return animationFrames[id - 0x4000];
    }

    public static Texture getGumpTexture(int id) {
        return gumps.get(id);
    }
} public enum State {DISCONNECTED, CONNECTED, LOGGED_IN}; public abstract class BaseGump {
    private final int gumpID;
    private final SLObject object;
    private boolean wantsClose;
    private Point position;

    public class GumpPart {
        public BaseGump owner;
        public Texture texture;
        public Point relativePosition;
        public Object gumpData;
        public int clickCount;

        public GumpPart(BaseGump owner, Texture texture, Point relPos) {
            this.owner = owner;
            this.texture = texture;
            this.relativePosition = relPos;
        }
    }

    public BaseGump(SLObject obj, int gumpID) {
        this.object = obj;
        this.gumpID = gumpID;
        this.position = new Point(0, 0);
    }

    public int getGumpID() {
        return gumpID;
    }

    public SLObject getObject() {
        return object;
    }

    public void setPosition(Point position) {
        this.position = position;
    }

    public Point getPosition() {
        return position;
    }

    public void close() {
        wantsClose = true;
    }

    public boolean wantsClose() {
        return wantsClose;
    }

    public abstract List<GumpPart> render();

    public void onClick(GumpPart part) {
    }

    public abstract boolean canDragGumpWithPart(GumpPart part);
} public class ContainerGump extends BaseGump {
    private final GumpPart frame;
    private final MainController controller;
    private final Map<SLItem, GumpPart> itemParts;

    public ContainerGump(MainController controller, SLObject obj, int gumpID) {
        super(obj, gumpID);
        this.controller = controller;
        this.itemParts = new HashMap<>();

        frame = new GumpPart(this, TexturePool.getGumpTexture(gumpID), new Point(0, 0));
    }

    @Override
    public List<GumpPart> render() {
        SLItem container = (SLItem) getObject();
        Collection<SLItem> content = container.getContainerContents();

        List<GumpPart> res = new ArrayList<>(1 + content.size());
        res.add(frame);
        for(SLItem itm : content) {
            GumpPart part = itemParts.get(itm);
            Point pos = new Point(itm.getLocation().getX(), itm.getLocation().getY());
            if(part == null) {
                break;
                // part = new GumpPart(this, TexturePool.getStaticTexture(itm.getGraphic()), pos);
                // part.gumpData = itm;
                // itemParts.put(itm, part);
            } else {
                // part.texture = TexturePool.getStaticTexture(itm.getGraphic());
                part.relativePosition = pos;
            }
            res.add(part);
        }
        return res;
    }

    @Override
    public boolean canDragGumpWithPart(GumpPart part) {
        return part == frame;
    }

    @Override
    public void onClick(GumpPart part) {
        part.clickCount++;
        if(part.gumpData instanceof SLItem && part.clickCount >= 2) {
            controller.onDoubleClickObject((SLItem) part.gumpData);
            part.clickCount = 0;
        }
    }
} public class PaperdollGump extends BaseGump {
    private final MainController controller;
    private final GumpPart frame, charachter;
    private final GumpPart backscrollButton, skillsButton, statusButton, peaceButton, warButton, closeButton;
    private final boolean forPlayer;
    private final Map<Short, GumpPart> equipParts;

    public PaperdollGump(MainController controller, SLMobile mob, boolean forPlayer) {
        super(mob, Gumps.ID_PAPERDOLL);
        this.controller = controller;
        this.forPlayer = forPlayer;
        this.equipParts = new HashMap<>();

        frame            = new GumpPart(this, TexturePool.getGumpTexture(forPlayer ? 0x0A : 0x0B), new Point(0, 0));
        charachter       = new GumpPart(this, TexturePool.getGumpTexture(mob.getGraphic() + 0x0C), new Point(0, 0));
        backscrollButton = new GumpPart(this, TexturePool.getGumpTexture(0x10), new Point(166, 95));
        skillsButton     = new GumpPart(this, TexturePool.getGumpTexture(0x12), new Point(166, 132));
        statusButton     = new GumpPart(this, TexturePool.getGumpTexture(0x0E), new Point(166, 169));
        peaceButton      = new GumpPart(this, TexturePool.getGumpTexture(0x17), new Point(166, 207));
        warButton        = new GumpPart(this, TexturePool.getGumpTexture(0x18), new Point(166, 207));
        closeButton      = new GumpPart(this, TexturePool.getGumpTexture(0x19), new Point(229, 207));
    }

    @Override
    public List<GumpPart> render() {
        SLMobile mob = (SLMobile) getObject();
        Collection<SLItem> equipment = mob.getEquipment();

        // the client draws the equipment in a specific order, maybe this is important:
        // layer order: 3, 4, 11, 6, 5, 10, 12, 13, 14, 7, 15, 8, 9, 1, 2

        List<GumpPart> res = new ArrayList<>(7 + equipment.size());
        res.add(frame);
        res.add(charachter);
        if(forPlayer) {
            res.add(backscrollButton);
            res.add(skillsButton);
        }
        if(mob instanceof Player && ((Player) mob).isInWarMode()) {
            res.add(warButton);
        } else {
            res.add(peaceButton);
        }
        res.add(statusButton);
        res.add(closeButton);
        for(SLItem itm : equipment) {
            int animID = itm.getTileInfo().animationID;
            if(animID == 0) {
                continue;
            }
            int gID = animID + 0x64;

            GumpPart curPart = equipParts.get(itm.getLayer());
            if(curPart == null || curPart.gumpData != itm) {
                curPart = new GumpPart(this, TexturePool.getGumpTexture(gID), new Point(0, 0));
                curPart.gumpData = itm;
                equipParts.put(itm.getLayer(), curPart);
            }
            curPart.texture = TexturePool.getGumpTexture(gID);
            res.add(curPart);
        }
        return res;
    }

    @Override
    public boolean canDragGumpWithPart(GumpPart part) {
        return part == frame;
    }

    @Override
    public void onClick(GumpPart part) {
        part.clickCount++;
        if(part == closeButton) {
            close();
        } else if(part == backscrollButton) {
            controller.onOpenBackscroll();
        } else if(part == skillsButton) {
            controller.onOpenSkillWindow();
        } else if(part == statusButton) {
            controller.onOpenStatus((SLMobile) getObject(), true);
        } else if(part == peaceButton || part == warButton) {
            controller.onToggleWarMode((SLMobile) getObject());
        } else if(part.gumpData instanceof SLItem && part.clickCount >= 2) {
            // the real client does it in the same way,
            // i.e. there can be lots of time between the two clicks
            controller.onDoubleClickObject((SLItem) part.gumpData);
            part.clickCount = 0;
        }
    }
} public class GumpPart {
    public BaseGump owner;
    public Texture texture;
    public Point relativePosition;
    public Object gumpData;
    public int clickCount;

    public GumpPart(BaseGump owner, Texture texture, Point relPos) {
        this.owner = owner;
        this.texture = texture;
        this.relativePosition = relPos;
    }
} public class InputGump {
    private static final int INPUT_FONT_SIZE = 14;
    private final FontRenderer renderer;
    private final StringBuffer inputLine;
    private Texture texture;

    public InputGump() {
        this.renderer = new FontRenderer(new Font(Font.SANS_SERIF, Font.BOLD, INPUT_FONT_SIZE));
        this.inputLine = new StringBuffer();
    }

    public void feedCharacter(char c) {
        if(c == 127 || c == '\b') {
            // DEL
            int oldLen = inputLine.length();
            if(oldLen > 0) {
                inputLine.setLength(oldLen - 1);
            }
        } else {
            inputLine.append(c);
        }
        updateTexture();
    }

    public String getAndReset() {
        String res = inputLine.toString();
        inputLine.setLength(0);
        updateTexture();
        return res;
    }

    private void updateTexture() {
        if(texture != null) {
            texture.dispose();
        }
        texture = renderer.renderText(inputLine.toString() + "_", Color.RED);
    }

    public Texture getTexture() {
        if(texture == null) {
            updateTexture();
        }
        return texture;
    }

    public int getTextHeight() {
        return renderer.getTextHeight();
    }
} public class InputManager implements MouseListener, MouseMotionListener, KeyListener {
    private static final int MOUSE_DOUBLE_CLICK_MS = 300;

    private final boolean[] pressedKeys;
    private final StringBuilder typedKeys;

    private long lastMouseLeftClickTime;
    private Point lastLeftClick, lastDoubleClick;
    private MouseEvent curDragEvent;

    private boolean rightMouseButtonDown;
    private boolean mouseInsideWindow;


    public InputManager() {
        pressedKeys = new boolean[256];
        mouseInsideWindow = true;
        typedKeys = new StringBuilder();
    }

    @Override
    public synchronized void mousePressed(MouseEvent e) {
        if(SwingUtilities.isRightMouseButton(e)) {
            rightMouseButtonDown = true;
        }
    }

    @Override
    public synchronized void mouseReleased(MouseEvent e) {
        if(SwingUtilities.isRightMouseButton(e)) {
            rightMouseButtonDown = false;
        } else if(SwingUtilities.isLeftMouseButton(e)) {
            curDragEvent = null;
        }
    }

    @Override
    public synchronized void mouseClicked(MouseEvent e) {
        if(SwingUtilities.isLeftMouseButton(e)) {
            long msDiff = e.getWhen() - lastMouseLeftClickTime;
            if(msDiff < MOUSE_DOUBLE_CLICK_MS) {
                lastMouseLeftClickTime = 0;
                lastDoubleClick = e.getPoint();
            } else {
                // potential single click
                lastLeftClick = e.getPoint();
                lastMouseLeftClickTime = System.currentTimeMillis();
            }
        }
    }

    public synchronized Point pollLastDoubleClick() {
        Point res = lastDoubleClick;
        lastDoubleClick = null;
        return res;
    }

    public synchronized Point peekNextSingleClick() {
        return lastLeftClick;
    }

    public synchronized void abortNextSingleClick() {
        lastMouseLeftClickTime = 0;
        lastLeftClick = null;
    }

    public synchronized Point pollLastSingleClick() {
        if(lastMouseLeftClickTime != 0 && System.currentTimeMillis() - lastMouseLeftClickTime > MOUSE_DOUBLE_CLICK_MS) {
            lastMouseLeftClickTime = 0;
            return lastLeftClick;
        } else {
            return null;
        }
    }

    @Override
    public synchronized void mouseDragged(MouseEvent e) {
        curDragEvent = e;
    }

    public synchronized Point peekLastDragEvent() {
        if(curDragEvent != null) {
            return curDragEvent.getPoint();
        } else {
            return null;
        }
    }

    @Override
    public void mouseMoved(MouseEvent e) {
    }

    @Override
    public void mouseEntered(MouseEvent e) {
        mouseInsideWindow = true;
    }

    @Override
    public void mouseExited(MouseEvent e) {
        mouseInsideWindow = false;
    }

    public boolean isMouseInsideWindow() {
        return mouseInsideWindow;
    }

    public boolean isRightMouseButtonDown() {
        return rightMouseButtonDown;
    }

    @Override
    public synchronized void keyPressed(KeyEvent e) {
        if(e.getKeyCode() < pressedKeys.length) {
            pressedKeys[e.getKeyCode()] = true;
        }
    }

    @Override
    public synchronized void keyReleased(KeyEvent e) {
        if(e.getKeyCode() < pressedKeys.length) {
            pressedKeys[e.getKeyCode()] = false;
        }
    }

    public synchronized boolean isKeyDown(int code) {
        return pressedKeys[code];
    }

    @Override
    public synchronized void keyTyped(KeyEvent e) {
        char c = e.getKeyChar();
        typedKeys.append(c);
    }

    public synchronized String pollTypedKeys() {
        String res = typedKeys.toString();
        typedKeys.setLength(0);
        return res;
    }
} public class LandRenderer {
    private static final int SIZEOF_FLOAT = 4;
    private final ShaderProgram shader;
    private TextureAtlas landAtlas, staticAtlas;
    private Point3D center;
    private int radius;
    private Integer vaoID, eboID, vboID, dataID;
    private int locProjectionMatrix, locViewMatrix, locLandTexture, locStaticTexture;

    public LandRenderer() {
        shader = new ShaderProgram();
    }

    public void init() throws IOException {
        landAtlas = TexturePool.getLandAtlas();
        staticAtlas = TexturePool.getStaticAtlas();

        shader.setVertexShader(Paths.get("shaders", "land.vert"));
        shader.setFragmentShader(Paths.get("shaders", "land.frag"));
        shader.link();

        shader.bind();
        shader.setUniformFloat(shader.getUniformLocation("atlasDimensions"),
                landAtlas.getTexture().getWidth(), landAtlas.getTexture().getHeight(),
                staticAtlas.getTexture().getWidth(), staticAtlas.getTexture().getHeight());
        locProjectionMatrix = shader.getUniformLocation("projectionMatrix");
        locViewMatrix = shader.getUniformLocation("viewMatrix");
        locLandTexture = shader.getUniformLocation("landTexture");
        locStaticTexture = shader.getUniformLocation("staticTexture");
        shader.unbind();

        FloatBuffer vertices = BufferUtils.createFloatBuffer(8);
        vertices.put(new float[] {
                0, 0, // left bottom
                0, 1, // left top
                1, 1, // right top
                1, 0, // right bottom
        });
        vertices.rewind();

        ShortBuffer elements = BufferUtils.createShortBuffer(4);
        elements.put(new short[] {
                0, 1, 3, 2
        });
        elements.rewind();

        vaoID = glGenVertexArrays();
        glBindVertexArray(vaoID);
            vboID = glGenBuffers();
            glBindBuffer(GL_ARRAY_BUFFER, vboID);
            glBufferData(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW);
            glEnableVertexAttribArray(0);
            glVertexAttribPointer(0, 2, GL_FLOAT, false, 0, 0);

            eboID = glGenBuffers();
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboID);
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, elements, GL_STATIC_DRAW);
        glBindVertexArray(0);
    }

    private void updateBuffer() {
        int entrySize = 11;
        int centerX = center.getX();
        int centerY = center.getY();

        FloatBuffer data = BufferUtils.createFloatBuffer(2 * radius * 2 * radius * entrySize);
        for(int x = centerX - radius; x < centerX + radius; x++) {
            for(int y = centerY - radius; y < centerY + radius; y++) {
                Point3D point;
                Rectangle texRect;
                int selfZ = 0, eastZ = 0, southZ = 0, southEastZ = 0;
                float type;

                if(x < 0 || x >= SLMap.MAP_WIDTH || y < 0 || y >= SLMap.MAP_HEIGHT) {
                    type = 0.0f;
                    texRect = TexturePool.getLandAtlas().getEntry(1); // void like in real client
                } else {
                    boolean shouldProject = false, canProject = false;
                    point = new Point3D(x, y, getZ(x, y));
                    int landID = SLData.get().getMap().getTextureID(point);
                    LandTile landTile = SLData.get().getTiles().getLandTile(landID);
                    selfZ = point.getZ();
                    eastZ = getZ(x + 1, y);
                    southZ = getZ(x, y + 1);
                    southEastZ = getZ(x + 1, y + 1);
                    shouldProject = (selfZ != eastZ) || (selfZ != southZ) || (selfZ != southEastZ);
                    canProject = (landTile != null && landTile.textureID != 0);
                    if(shouldProject && canProject) {
                        texRect = TexturePool.getStaticAtlas().getEntry(landTile.textureID);
                        type = 1.0f;
                    } else {
                        texRect = TexturePool.getLandAtlas().getEntry(landID);
                        type = 0.0f;
                    }
                }

                if(texRect == null) {
                    // there are some land tiles that reference an invalid textureID,
                    // display the "unused" tile in such situations
                    type = 0.0f;
                    texRect = TexturePool.getLandAtlas().getEntry(0);
                }

                data.put(new float[] {
                        type,
                        x, y,
                        selfZ, southZ, southEastZ, eastZ,
                        texRect.x, texRect.y, texRect.width, texRect.height
                });
            }
        }
        data.rewind();

        if(dataID != null) {
            glDeleteBuffers(dataID);
            dataID = null;
        }

        glBindVertexArray(vaoID);
        dataID = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, dataID);
        glBufferData(GL_ARRAY_BUFFER, data, GL_STATIC_DRAW);

        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 1, GL_FLOAT, false, entrySize * SIZEOF_FLOAT, 0 * SIZEOF_FLOAT);

        glEnableVertexAttribArray(2);
        glVertexAttribPointer(2, 2, GL_FLOAT, false, entrySize * SIZEOF_FLOAT, 1 * SIZEOF_FLOAT);

        glEnableVertexAttribArray(3);
        glVertexAttribPointer(3, 4, GL_FLOAT, false, entrySize * SIZEOF_FLOAT, 3 * SIZEOF_FLOAT);

        glEnableVertexAttribArray(4);
        glVertexAttribPointer(4, 4, GL_FLOAT, false, entrySize * SIZEOF_FLOAT, 7 * SIZEOF_FLOAT);

        glVertexAttribDivisor(1, 1);
        glVertexAttribDivisor(2, 1);
        glVertexAttribDivisor(3, 1);
        glVertexAttribDivisor(4, 1);
        glBindVertexArray(0);
    }

    private int getZ(int x, int y) {
        return SLData.get().getMap().getTileElevation(new Point2D(x, y));
    }

    public void render(Transform projection, Transform view, Point3D pos, int radius) {
        if(center == null || !center.equals(pos) || this.radius != radius) {
            this.center = pos;
            this.radius = radius;
            updateBuffer();
        }
        shader.bind();
        shader.setUniformMatrix(locProjectionMatrix, projection);
        shader.setUniformMatrix(locViewMatrix, view);
        shader.setUniformInt(locLandTexture, 0);
        shader.setUniformInt(locStaticTexture, 1);

        TexturePool.getLandAtlas().getTexture().bind(0);
        TexturePool.getStaticAtlas().getTexture().bind(1);

        glBindVertexArray(vaoID);
        glDrawElementsInstanced(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_SHORT, 0, 2 * radius * 2 * radius);
        glBindVertexArray(0);
        shader.unbind();
    }

    public void dispose() {
        if(dataID != null) {
            glDeleteBuffers(dataID);
            dataID = null;
        }

        if(vboID != null) {
            glDeleteBuffers(vboID);
            vboID = null;
        }

        if(eboID != null) {
            glDeleteBuffers(eboID);
            eboID = null;
        }

        if(vaoID != null) {
            glBindVertexArray(0);
            glDeleteVertexArrays(vaoID);
            vaoID = null;
        }
    }
} public class PickList {
    private final Map<Integer, Object> pickEntries;
    private int nextId;
    private boolean isValid;

    public PickList() {
        pickEntries = new HashMap<>(1000);
        isValid = false;
    }

    public void setValid(boolean valid) {
        this.isValid = valid;
        if(!valid) {
            clear();
        }
    }

    public boolean isValid() {
        return isValid;
    }

    public void clear() {
        nextId = 1;
        pickEntries.clear();
    }

    public int enter(Object obj) {
        pickEntries.put(nextId, obj);
        return nextId++;
    }

    public Object get(int pickId) {
        if(!isValid) {
            throw new RuntimeException("pick from invalid list");
        }
        return pickEntries.get(pickId);
    }
} public class ShaderProgram {
    private Integer programId;
    private Integer vertexShaderId;
    private Integer fragmentShaderId;

    public ShaderProgram() {
    }

    public void setVertexShader(Path path) throws IOException {
        if(programId == null) {
            programId = glCreateProgram();
        }
        vertexShaderId = readShader(path, GL_VERTEX_SHADER);
    }

    public void setFragmentShader(Path path) throws IOException {
        if(programId == null) {
            programId = glCreateProgram();
        }
        fragmentShaderId = readShader(path, GL_FRAGMENT_SHADER);
    }

    public int getAttribLocation(String name) {
        int location =  glGetAttribLocation(programId, name);
        if(location == -1) {
            throw new RuntimeException("Invalid attrib: " + name);
        }
        return location;
    }

    public int getUniformLocation(String name) {
        int location = glGetUniformLocation(programId, name);
        if(location == -1) {
            throw new RuntimeException("Invalid uniform: " + name);
        }
        return location;
    }

    public void setUniformInt(int location, int... values) {
        if(location == -1) {
            throw new RuntimeException("Invalid uniform: " + location);
        }

        switch(values.length) {
        case 1:
            glUniform1i(location, values[0]);
            break;
        case 2:
            glUniform2i(location, values[0], values[1]);
            break;
        case 3:
            glUniform3i(location, values[0], values[1], values[2]);
            break;
        case 4:
            glUniform4i(location, values[0], values[1], values[2], values[3]);
            break;
        default:
            throw new RuntimeException("Invalid uniform size: " + values.length);
    }
    }

    public void setUniformFloat(int location, float... values) {
        if(location == -1) {
            throw new RuntimeException("Invalid uniform: " + location);
        }

        switch(values.length) {
            case 1:
                glUniform1f(location, values[0]);
                break;
            case 2:
                glUniform2f(location, values[0], values[1]);
                break;
            case 3:
                glUniform3f(location, values[0], values[1], values[2]);
                break;
            case 4:
                glUniform4f(location, values[0], values[1], values[2], values[3]);
                break;
            default:
                throw new RuntimeException("Invalid uniform size: " + values.length);
        }
    }

    public void setUniformBool(int location, boolean val) {
        if(location == -1) {
            throw new RuntimeException("Invalid uniform: " + location);
        }
        glUniform1i(location, val ? GL_TRUE : GL_FALSE);
    }

    public void setUniformMatrix(int location, Transform t) {
        if(location == -1) {
            throw new RuntimeException("Invalid uniform: " + location);
        }
        glUniformMatrix4(location, false, t.getFloatBuffer());
    }

    public void link() {
        glLinkProgram(programId);
        if(glGetProgrami(programId, GL_LINK_STATUS) == GL_FALSE) {
            String error = glGetProgramInfoLog(programId, glGetProgrami(programId, GL_INFO_LOG_LENGTH));
            throw new RuntimeException("Couldn't link shader: " + error);
        }
        if(vertexShaderId != null) {
            glDetachShader(programId, vertexShaderId);
            glDeleteShader(vertexShaderId);
            vertexShaderId = null;
        }

        if(fragmentShaderId != null) {
            glDetachShader(programId, fragmentShaderId);
            glDeleteShader(fragmentShaderId);
            fragmentShaderId = null;
        }
    }

    public void bind() {
        glUseProgram(programId);
    }

    public void unbind() {
        glUseProgram(0);
    }

    public void dispose() {
        unbind();

        if(programId == null) {
            return;
        }

        if(vertexShaderId != null) {
            glDetachShader(programId, vertexShaderId);
            glDeleteShader(vertexShaderId);
            vertexShaderId = null;
        }

        if(fragmentShaderId != null) {
            glDetachShader(programId, fragmentShaderId);
            glDeleteShader(fragmentShaderId);
            fragmentShaderId = null;
        }

        glDeleteProgram(programId);
        programId = null;
    }

    private int readShader(Path path, int type) throws IOException {
        byte[] fileContents = Files.readAllBytes(path);
        String shaderSource = new String(fileContents, StandardCharsets.UTF_8);
        int shaderId = glCreateShader(type);
        glShaderSource(shaderId, shaderSource);
        glCompileShader(shaderId);
        if(glGetShaderi(shaderId, GL_COMPILE_STATUS) == GL_FALSE) {
            String error = glGetShaderInfoLog(shaderId, glGetShaderi(shaderId, GL_INFO_LOG_LENGTH));
            throw new RuntimeException("Couldn't compile shader: " + error);
        }
        glAttachShader(programId, shaderId);
        return shaderId;
    }
} public class StaticRenderer {
    private static final Logger log = Logger.getLogger("slclient.staticrenderer");
    private final ShaderProgram shader;
    private final Transform projectionMat, modelMat;
    private Transform baseProjection, viewMat;
    private TextureAtlas staticAtlas;
    private Integer vaoID, vboID, eboID;
    private int locSelectionID, locMat, locTexRect;

    public StaticRenderer() {
        shader = new ShaderProgram();
        projectionMat = new Transform();
        modelMat = new Transform();
    }

    public void init() throws IOException {
        shader.setVertexShader(Paths.get("shaders", "static.vert"));
        shader.setFragmentShader(Paths.get("shaders", "static.frag"));
        shader.link();

        staticAtlas = TexturePool.getStaticAtlas();
        shader.bind();
        shader.setUniformInt(shader.getUniformLocation("tex"), 0);
        shader.setUniformFloat(shader.getUniformLocation("atlasDimensions"),
                staticAtlas.getTexture().getWidth(), staticAtlas.getTexture().getHeight());
        locSelectionID = shader.getUniformLocation("selectionID");
        locMat = shader.getUniformLocation("mat");
        locTexRect = shader.getUniformLocation("texRect");
        shader.unbind();

        FloatBuffer vertices = BufferUtils.createFloatBuffer(8);
        vertices.put(new float[] {
                0, 0, // left bottom
                0, 1, // left top
                1, 1, // right top
                1, 0, // right bottom
        });
        vertices.rewind();

        ShortBuffer elements = BufferUtils.createShortBuffer(4);
        elements.put(new short[] {
                0, 1, 3, 2
        });
        elements.rewind();

        vaoID = glGenVertexArrays();
        glBindVertexArray(vaoID);
            vboID = glGenBuffers();
            glBindBuffer(GL_ARRAY_BUFFER, vboID);
            glBufferData(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW);
            glEnableVertexAttribArray(0);
            glVertexAttribPointer(0, 2, GL_FLOAT, false, 0, 0);

            eboID = glGenBuffers();
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboID);
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, elements, GL_STATIC_DRAW);
        glBindVertexArray(0);
    }

    public void setTransformations(Transform projection, Transform view) {
        this.baseProjection = projection;
        this.viewMat = view;
    }

    public void renderStaticInGame(int staticID, Point3D location, int selectionID) {
        Rectangle texRect = staticAtlas.getEntry(staticID);
        if(texRect == null) {
            log.finer("No texture for item with graphic: " + staticID);
            return;
        }

        shader.bind();
        glBindVertexArray(vaoID);
        staticAtlas.getTexture().bind(0);
        shader.setUniformInt(locSelectionID, selectionID);
        shader.setUniformFloat(locTexRect, texRect.x, texRect.y, texRect.width, texRect.height);

        projectionMat.reset(baseProjection);
        projectionMat.translate(-texRect.width / 2.0f, GameView.GRID_DIAMETER - texRect.height, 0.0f);

        modelMat.reset();
        modelMat.translate(location.getX(), location.getY(), location.getZ());
        modelMat.rotate(0, 0, -45);
        modelMat.scale(texRect.width / GameView.GRID_EDGE, texRect.height / GameView.GRID_EDGE, 1f);

        shader.setUniformMatrix(locMat, modelMat.combine(viewMat).combine(projectionMat));
        glDrawElements(GL_TRIANGLE_STRIP, 4, GL_UNSIGNED_SHORT, 0);
        glBindVertexArray(0);
        shader.unbind();
    }

    public void dispose() {
        if(vboID != null) {
            glDeleteBuffers(vboID);
            vboID = null;
        }

        if(eboID != null) {
            glDeleteBuffers(eboID);
            eboID = null;
        }

        if(vaoID != null) {
            glBindVertexArray(0);
            glDeleteVertexArrays(vaoID);
            vaoID = null;
        }
    }
} public class TextLog {
    private static final int TEXT_SIZE = 14;
    private static final int MIN_DURATION = 2000;
    private static final int DURATION_PER_CHAR = 75;
    private final Map<Object, List<TextEntry>> entries;
    private final FontRenderer renderer;

    public class TextEntry {
        private final long expireAt;
        public Texture texture;
        public String text;
        public Color color;

        public TextEntry(String text, Color color) {
            this.expireAt = System.currentTimeMillis() + MIN_DURATION + DURATION_PER_CHAR * text.length();
            this.text = text;
            this.color = color;
        }
    }

    public TextLog() {
        renderer = new FontRenderer(new Font(Font.SANS_SERIF, Font.BOLD, TEXT_SIZE));
        entries = new HashMap<>();
    }

    public void update(long elapsedMillis) {
        long now = System.currentTimeMillis();
        for(Iterator<Object> it = entries.keySet().iterator(); it.hasNext(); ) {
            Object where = it.next();
            List<TextEntry> textEntries = entries.get(where);
            for(Iterator<TextEntry> textIt = textEntries.iterator(); textIt.hasNext(); ) {
                TextEntry entry = textIt.next();
                if(now > entry.expireAt) {
                    if(entry.texture != null) {
                        entry.texture.dispose();
                    }
                    textIt.remove();
                } else if(entry.texture == null) {
                    entry.texture = renderer.renderText(entry.text, entry.color);
                }
            }
            if(textEntries.isEmpty()) {
                it.remove();
            }
        }
    }

    public void addEntry(Object where, String text, Color color) {
        List<TextEntry> list = entries.get(where);
        if(list == null) {
            list = new LinkedList<>();
            entries.put(where, list);
        }
        list.add(new TextEntry(text, color));
    }

    public void visitEntries(BiConsumer<? super Object, ? super List<TextEntry>> c) {
        entries.forEach(c);
    }

    public int getLineHeight() {
        return renderer.getTextHeight();
    }
} public class Texture {
    private final int id, width, height;
    private final boolean useMipMapping = false;

    public Texture(BufferedImage image) {
        this.width = image.getWidth();
        this.height = image.getHeight();
        ByteBuffer buffer = BufferUtils.createByteBuffer(width * height * 4);
        if(image.getType() != BufferedImage.TYPE_INT_ARGB) {
            throw new RuntimeException("Can only open INT_ARGB images");
        }

        for(int y = 0; y < height; y++) {
            for(int x = 0; x < width; x++) {
                int argb = image.getRGB(x, y);
                buffer.put((byte) ((argb >> 16) & 0xFF));
                buffer.put((byte) ((argb >> 8) & 0xFF));
                buffer.put((byte) ((argb >> 0) & 0xFF));
                buffer.put((byte) ((argb >> 24) & 0xFF));
            }
        }
        buffer.rewind();

        id = glGenTextures();
        glBindTexture(GL_TEXTURE_2D, id);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, buffer);
        if(useMipMapping) {
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR_MIPMAP_LINEAR);
            glGenerateMipmap(GL_TEXTURE_2D);
        } else {
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
            glTexParameteri(GL_TEXTURE_2D, GL12.GL_TEXTURE_BASE_LEVEL, 0);
            glTexParameteri(GL_TEXTURE_2D, GL12.GL_TEXTURE_MAX_LEVEL, 0);
        }
    }

    public void bind(int textureUnit) {
        glActiveTexture(GL_TEXTURE0 + textureUnit);
        glBindTexture(GL_TEXTURE_2D, id);
    }

    public void dispose() {
        glDeleteTextures(id);
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }

    public int getTextureId() {
        return id;
    }
} public class Transform {
    public final Matrix4f mat;
    private FloatBuffer buffer;

    public Transform() {
        mat = new Matrix4f();
    }

    public Transform(Transform other) {
        this.mat = Matrix4f.load(other.mat, null);
    }

    public void reset() {
        mat.setIdentity();
    }

    public void reset(Transform projection) {
        mat.load(projection.mat);
    }

    public Transform translate(float x, float y, float z) {
        Matrix4f.translate(new Vector3f(x, y, z), mat, mat);
        return this;
    }

    public Transform scale(float s) {
        return scale(s, s, s);
    }

    public Transform scale(float sx, float sy, float sz) {
        Matrix4f.scale(new Vector3f(sx, sy, sz), mat, mat);
        return this;
    }

    public Transform rotate(float rx, float ry, float rz) {
        Matrix4f.rotate((float) Math.toRadians(rx), new Vector3f(1, 0, 0), mat, mat);
        Matrix4f.rotate((float) Math.toRadians(ry), new Vector3f(0, 1, 0), mat, mat);
        Matrix4f.rotate((float) Math.toRadians(rz), new Vector3f(0, 0, 1), mat, mat);
        return this;
    }

    public Transform combine(Transform other) {
        Matrix4f.mul(other.mat, this.mat, this.mat);
        return this;
    }

    public FloatBuffer getFloatBuffer() {
        if(buffer == null) {
            buffer = BufferUtils.createFloatBuffer(16);
        } else {
            buffer.clear();
        }
        mat.store(buffer);
        buffer.rewind();
        return buffer;
    }

    public static Transform orthographic(float left, float top, float right, float bottom, float zNear, float zFar) {
        Transform t = new Transform();
        // Scale so that (left, top, near) is mapped to (-1, 1, 1) and (right, bottom, far) to (1, -1, -1)
        t.mat.m00 = 2 / (right - left);
        t.mat.m11 = -2 / (bottom - top);
        t.mat.m22 = -2 / (zFar - zNear);
        return t;
    }

    public static Transform UO(float gridDiameter, float pc) {
        Transform res = new Transform();
        // rotate 45°, scale to grid, project z by moving y
        res.mat.m00 =  gridDiameter / 2.0f;
        res.mat.m01 =  gridDiameter / 2.0f;
        res.mat.m10 = -gridDiameter / 2.0f;
        res.mat.m11 =  gridDiameter / 2.0f;
        res.mat.m21 = -pc;
        return res;
    }
} public class TextEntry {
    private final long expireAt;
    public Texture texture;
    public String text;
    public Color color;

    public TextEntry(String text, Color color) {
        this.expireAt = System.currentTimeMillis() + MIN_DURATION + DURATION_PER_CHAR * text.length();
        this.text = text;
        this.color = color;
    }
}
import java.awt.BorderLayout;
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Component;
import java.awt.Point;
import java.awt.event.KeyEvent;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.ShortBuffer;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.ContextAttribs;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.PixelFormat;
import org.solhost.folko.uosl.libuosl.data.SLArt;
import org.solhost.folko.uosl.libuosl.data.SLData;
import org.solhost.folko.uosl.libuosl.data.SLMap;
import org.solhost.folko.uosl.libuosl.data.SLTiles;
import org.solhost.folko.uosl.libuosl.data.SLArt.ArtEntry;
import org.solhost.folko.uosl.libuosl.data.SLArt.MobileAnimation;
import org.solhost.folko.uosl.libuosl.types.Direction;
import org.solhost.folko.uosl.libuosl.types.Gumps;
import org.solhost.folko.uosl.libuosl.types.Point2D;
import org.solhost.folko.uosl.libuosl.types.Point3D;
import org.solhost.folko.uosl.slclient.controllers.MainController;
import org.solhost.folko.uosl.slclient.models.GameState;
import org.solhost.folko.uosl.slclient.models.SLItem;
import org.solhost.folko.uosl.slclient.models.SLMobile;
import org.solhost.folko.uosl.slclient.models.SLObject;
import org.solhost.folko.uosl.slclient.models.TexturePool;
import org.solhost.folko.uosl.slclient.models.GameState.State;
import org.solhost.folko.uosl.slclient.views.gumps.BaseGump;
import org.solhost.folko.uosl.slclient.views.gumps.ContainerGump;
import org.solhost.folko.uosl.slclient.views.gumps.PaperdollGump;
import org.solhost.folko.uosl.slclient.views.gumps.BaseGump.GumpPart;
import org.solhost.folko.uosl.slclient.views.util.InputGump;
import org.solhost.folko.uosl.slclient.views.util.InputManager;
import org.solhost.folko.uosl.slclient.views.util.LandRenderer;
import org.solhost.folko.uosl.slclient.views.util.PickList;
import org.solhost.folko.uosl.slclient.views.util.ShaderProgram;
import org.solhost.folko.uosl.slclient.views.util.StaticRenderer;
import org.solhost.folko.uosl.slclient.views.util.TextLog;
import org.solhost.folko.uosl.slclient.views.util.Texture;
import org.solhost.folko.uosl.slclient.views.util.Transform;
import org.solhost.folko.uosl.slclient.views.util.TextLog.TextEntry;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL15.*;
import static org.lwjgl.opengl.GL20.*;
import static org.lwjgl.opengl.GL30.*;
package org.solhost.folko.uosl.slclient.views;





public class GameView extends JPanel {
    private static final long serialVersionUID = 1L;
    private static final Logger log = Logger.getLogger("slclient.gameview");
    public static final float GRID_DIAMETER = 42.0f;
    public static final float GRID_EDGE     = GRID_DIAMETER / (float) Math.sqrt(2);
    public static final float PROJECTION_CONSTANT = 4.0f;

    private final Canvas glCanvas;

    private final MainController mainController;
    private final GameState game;

    // Client data
    private final SLTiles tiles;
    private final SLArt art;

    // Text stuff
    private final InputGump inputGump;
    private final TextLog textLog;
    private final Object sysMessageEntry;

    // Gumps
    private final List<BaseGump> openGumps;
    private Object dragObject;
    private Point dragOffset;

    // OpenGL stuff
    private final LandRenderer landRenderer;
    private final StaticRenderer staticRenderer;
    private int frameRateLimit;
    private Transform projection, view, model;
    private ShaderProgram shader;
    private Integer vaoID, vboID, eboID;
    private int zOffsetLocation, matLocation, texTypeLocation, selectionIDLocation;
    private float zoom = 1.0f;

    // Input helpers
    private final InputManager input;