@NetworkEventMeta(direction = EventDirection.S2C)
public static class GlassesChangeBackgroundEvent extends GlassesEvent {

	@Serialize
	public int backgroundColor;

	public GlassesChangeBackgroundEvent(long guid, int backgroundColor) {
		super(guid);
		this.backgroundColor = backgroundColor;
	}
} @NetworkEventMeta(direction = EventDirection.S2C)
public static class GlassesSetDragParamsEvent extends GlassesEvent {

	@Serialize
	public int period;

	@Serialize
	public int threshold;

	public GlassesSetDragParamsEvent(long guid, int period, int threshold) {
		super(guid);
		this.period = period;
		this.threshold = threshold;
	}

} @NetworkEventMeta(direction = EventDirection.S2C)
public static class GlassesSetGuiVisibilityEvent extends GlassesEvent {

	@Serialize
	public Map<GuiElements, Boolean> visibility;

	public GlassesSetGuiVisibilityEvent(long guid, Map<GuiElements, Boolean> visibility) {
		super(guid);
		this.visibility = visibility;
	}
} @NetworkEventMeta(direction = EventDirection.S2C)
public static class GlassesSetKeyRepeatEvent extends GlassesEvent {

	@Serialize
	public boolean repeat;

	public GlassesSetKeyRepeatEvent(long guid, boolean repeat) {
		super(guid);
		this.repeat = repeat;
	}
} @NetworkEventMeta(direction = EventDirection.S2C)
public static class GlassesStopCaptureEvent extends GlassesEvent {

	public GlassesStopCaptureEvent(long guid) {
		super(guid);
	}
} public class GuiCapture extends GuiScreen {

	private int backgroundColor = 0x2A00FF00;
	private final long guid;

	private int mouseButtonsDown;

	private float lastDragX;
	private float lastDragY;
	private int dragInterval;

	private float dragThresholdSquared = Config.defaultDragThreshold * Config.defaultDragThreshold;
	private int dragPeriod = Config.defaultDragPeriod;

	private final Map<GuiElements, Boolean> originalState;

	private final Map<GuiElements, Boolean> updatedState;

	public GuiCapture(long guid) {
		this.guid = guid;
		this.originalState = GuiUtils.storeGuiElementsState();
		this.updatedState = Maps.newEnumMap(GuiElements.class);
	}

	@Override
	public void handleMouseInput() {
		super.handleMouseInput();

		final int button = Mouse.getEventButton();
		final int wheel = Mouse.getEventDWheel();
		final int mx = Mouse.getEventX();
		final int my = Mouse.getEventY();

		final float scaleX = (float)this.width / this.mc.displayWidth;
		final float scaleY = (float)this.height / this.mc.displayHeight;

		final float x = mx * scaleX;
		final float y = this.height - my * scaleY;

		if (button != -1 || wheel != 0) {
			final ScaledResolution resolution = new ScaledResolution(this.mc, this.mc.displayWidth, this.mc.displayHeight);
			final DrawableHitInfo hit = TerminalManagerClient.instance.findDrawableHit(guid, resolution, x, y);

			if (button != -1) {
				final boolean state = Mouse.getEventButtonState();
				createMouseButtonEvent(button, state, hit).sendToServer();
				final boolean draggingStarted = updateButtonCounter(state);
				if (draggingStarted) resetDraggingLimiter(x, y);
			}

			if (wheel != 0) createMouseWheelEvent(wheel, hit).sendToServer();
		}

		{
			final float dx = (x - lastDragX);
			final float dy = (y - lastDragY);

			if (canSendDragEvent(dx, dy)) {
				createDragEvent(dx, dy).sendToServer();
				resetDraggingLimiter(x, y);
			}
		}

	}

	private boolean updateButtonCounter(boolean isPressed) {
		if (isPressed) {
			return ++this.mouseButtonsDown == 1;
		} else {
			this.mouseButtonsDown = Math.max(this.mouseButtonsDown - 1, 0); // first event may be mouse_up, from keyboard click
		}

		return false;
	}

	private void resetDraggingLimiter(float x, float y) {
		this.lastDragX = x;
		this.lastDragY = y;
		this.dragInterval = dragPeriod;
	}

	private boolean canSendDragEvent(float dx, float dy) {
		if (mouseButtonsDown <= 0) return false;
		if (dragInterval >= 0) return false;

		final float d = dx * dx + dy * dy;
		return d >= dragThresholdSquared;
	}

	@Override
	public void updateScreen() {
		super.updateScreen();
		--dragInterval;
	}

	private NetworkEvent createDragEvent(float dx, float dy) {
		return new GlassesMouseDragEvent(guid, dx, dy);
	}

	private NetworkEvent createMouseButtonEvent(int button, boolean state, DrawableHitInfo hit) {
		return hit != null? new GlassesComponentMouseButtonEvent(guid, hit.id, hit.surfaceType, hit.dx, hit.dy, button, state) : new GlassesMouseButtonEvent(guid, button, state);
	}

	private NetworkEvent createMouseWheelEvent(int wheel, DrawableHitInfo hit) {
		return hit != null? new GlassesComponentMouseWheelEvent(guid, hit.id, hit.surfaceType, hit.dx, hit.dy, wheel) : new GlassesMouseWheelEvent(guid, wheel);
	}

	@Override
	public void handleKeyboardInput() {
		final int key = Keyboard.getEventKey();

		if (key == Keyboard.KEY_ESCAPE) {
			this.mc.displayGuiScreen(null);
			this.mc.setIngameFocus();
		} else {
			final boolean state = Keyboard.getEventKeyState();
			if (state) {
				final char ch = sanitizeKeyChar(Keyboard.getEventCharacter());

				final boolean isRepeat = Keyboard.isRepeatEvent();
				new GlassesKeyDownEvent(guid, ch, key, isRepeat).sendToServer();
			} else {
				new GlassesKeyUpEvent(guid, key).sendToServer();
			}
		}

		// looks like twitch controls
		super.handleKeyboardInput();
	}

	private static char sanitizeKeyChar(char ch) {
		switch (Character.getType(ch)) {
			case Character.PRIVATE_USE: // special case for Macs
			case Character.UNASSIGNED:
				return 0;
			default:
				return ch;
		}
	}

	@Override
	public void drawScreen(int mouseX, int mouseY, float partialTickTime) {
		drawRect(0, 0, width, height, backgroundColor);
	}

	@Override
	public void initGui() {
		new GlassesSignalCaptureEvent(guid, true).sendToServer();
		Keyboard.enableRepeatEvents(false);
	}

	@Override
	public void onGuiClosed() {
		new GlassesSignalCaptureEvent(guid, false).sendToServer();
		Keyboard.enableRepeatEvents(false);
		GuiUtils.loadGuiElementsState(originalState);
	}

	@Override
	public boolean doesGuiPauseGame() {
		return false;
	}

	public long getGuid() {
		return guid;
	}

	public void setBackground(int backgroundColor) {
		this.backgroundColor = backgroundColor;
	}

	public void setKeyRepeat(boolean repeat) {
		Keyboard.enableRepeatEvents(repeat);
	}

	public void setDragParameters(int threshold, int period) {
		this.dragPeriod = period;
		this.dragThresholdSquared = threshold * threshold;
	}

	public void updateGuiElementsState(Map<GuiElements, Boolean> visibility) {
		updatedState.putAll(visibility);
	}

	public void forceGuiElementsState() {
		GuiUtils.loadGuiElementsState(updatedState);
	}
} public class NbtGuidProviders {

	private static final String MAIN_TAG = "OPA-Terminal";

	private static final String GUID_TAG = "Guid";

	public static class NbtGetter extends HelmetGetterAdapter {
		@Override
		public Optional<Long> getFor(ItemStack stack) {
			return getTerminalGuid(stack);
		}
	}

	public static class NbtSetter extends HandSetterAdapter {
		@Override
		public boolean setFor(ItemStack stack, long guid) {
			final NBTTagCompound itemTag = stack.getTagCompound();
			if (itemTag != null && itemTag.hasKey(MAIN_TAG, Constants.NBT.TAG_COMPOUND)) {
				final NBTTagCompound terminalTag = itemTag.getCompoundTag(MAIN_TAG);
				terminalTag.setLong(GUID_TAG, guid);
				return true;
			}

			return false;
		}
	}

	public static NBTTagCompound getOrCreateTerminalTag(ItemStack stack) {
		final NBTTagCompound itemTag = ItemUtils.getItemTag(stack);
		if (!itemTag.hasKey(MAIN_TAG, Constants.NBT.TAG_COMPOUND)) {
			final NBTTagCompound result = new NBTTagCompound();
			itemTag.setTag(MAIN_TAG, result);
			return result;
		} else {
			return itemTag.getCompoundTag(MAIN_TAG);
		}
	}

	public static boolean hasTerminalCapabilities(ItemStack stack) {
		final NBTTagCompound itemTag = stack.getTagCompound();
		if (itemTag == null) return false;
		return itemTag.hasKey(MAIN_TAG, Constants.NBT.TAG_COMPOUND);
	}

	public static Optional<Long> getTerminalGuid(ItemStack stack) {
		final NBTTagCompound itemTag = stack.getTagCompound();
		if (itemTag != null && itemTag.hasKey(MAIN_TAG, Constants.NBT.TAG_COMPOUND)) {
			final NBTTagCompound terminalTag = itemTag.getCompoundTag(MAIN_TAG);
			if (terminalTag.hasKey(GUID_TAG, Constants.NBT.TAG_ANY_NUMERIC)) return Optional.of(terminalTag.getLong(GUID_TAG));
		}

		return Optional.absent();
	}

	public static void setTerminalGuid(ItemStack stack, Optional<Long> guid) {
		final NBTTagCompound terminalTag = getOrCreateTerminalTag(stack);
		if (guid.isPresent()) terminalTag.setLong(GUID_TAG, guid.get());
	}

} public enum SurfaceType {
	GLOBAL(Boolean.FALSE), PRIVATE(Boolean.TRUE);

	// legacy replacement for 'isPrivate'
	public final Object scriptValue;

	private SurfaceType(Object scriptValue) {
		this.scriptValue = scriptValue;
	}
} public abstract class TerminalEvent extends NetworkEvent {

	public long terminalId;

	public TerminalEvent() {}

	public TerminalEvent(long terminalId) {
		this.terminalId = terminalId;
	}

	@Override
	protected void readFromStream(DataInput input) throws IOException {
		terminalId = input.readLong();
	}

	@Override
	protected void writeToStream(DataOutput output) throws IOException {
		output.writeLong(terminalId);
	}

	@Override
	protected void appendLogInfo(List<String> info) {
		info.add(TerminalUtils.formatTerminalId(terminalId));
	}

	public abstract SurfaceType getSurfaceType();

	public abstract static class Reset extends TerminalEvent {
		public Reset() {
			super();
		}

		public Reset(long terminalId) {
			super(terminalId);
		}
	}

	public abstract static class DrawableReset extends Reset {
		public DrawableReset() {}

		public DrawableReset(long terminalId) {
			super(terminalId);
		}
	}

	@NetworkEventMeta(direction = EventDirection.C2S)
	public static class PublicDrawableReset extends DrawableReset {
		public PublicDrawableReset() {}

		public PublicDrawableReset(long terminalId) {
			super(terminalId);
		}

		@Override
		public SurfaceType getSurfaceType() {
			return SurfaceType.GLOBAL;
		}
	}

	@NetworkEventMeta(direction = EventDirection.C2S)
	public static class PublicStructureReset extends StructureReset {
		public PublicStructureReset() {}

		public PublicStructureReset(long terminalId) {
			super(terminalId);
		}

		@Override
		public SurfaceType getSurfaceType() {
			return SurfaceType.GLOBAL;
		}
	}

	public abstract static class StructureReset extends Reset {
		public StructureReset() {}

		public StructureReset(long terminalId) {
			super(terminalId);
		}
	}

	@NetworkEventMeta(direction = EventDirection.C2S)
	public static class PrivateDrawableReset extends DrawableReset {
		public PrivateDrawableReset() {}

		public PrivateDrawableReset(long terminalId) {
			super(terminalId);
		}

		@Override
		public SurfaceType getSurfaceType() {
			return SurfaceType.PRIVATE;
		}
	}

	@NetworkEventMeta(direction = EventDirection.C2S)
	public static class PrivateStructureReset extends StructureReset {
		public PrivateStructureReset() {}

		public PrivateStructureReset(long terminalId) {
			super(terminalId);
		}

		@Override
		public SurfaceType getSurfaceType() {
			return SurfaceType.PRIVATE;
		}
	}

	public abstract static class Clear extends TerminalEvent {
		public Clear() {}

		public Clear(long terminalId) {
			super(terminalId);
		}
	}

	@NetworkEventMeta(direction = EventDirection.S2C)
	public static class PrivateClear extends Clear {
		public PrivateClear() {}

		public PrivateClear(long terminalId) {
			super(terminalId);
		}

		@Override
		public SurfaceType getSurfaceType() {
			return SurfaceType.PRIVATE;
		}
	}

	@NetworkEventMeta(direction = EventDirection.S2C)
	public static class PublicClear extends Clear {
		public PublicClear() {}

		public PublicClear(long terminalId) {
			super(terminalId);
		}

		@Override
		public SurfaceType getSurfaceType() {
			return SurfaceType.GLOBAL;
		}
	}

	public abstract static class Data extends TerminalEvent {
		public final CommandList commands = new CommandList();

		public Data() {}

		public Data(long terminalId) {
			super(terminalId);
		}

		@Override
		protected void readFromStream(DataInput input) throws IOException {
			super.readFromStream(input);
			commands.readFromStream(input);
		}

		@Override
		protected void writeToStream(DataOutput output) throws IOException {
			super.writeToStream(output);
			commands.writeToStream(output);
		}

		@Override
		protected void appendLogInfo(List<String> info) {
			super.appendLogInfo(info);
			info.add(Integer.toString(commands.size()));
		}

		public SurfaceClient createSurface() {
			throw new AbstractMethodError();
		}
	}

	public abstract static class DrawableData extends Data {

		public DrawableData() {}

		public DrawableData(long terminalId) {
			super(terminalId);
		}

		@Override
		public SurfaceClient createSurface() {
			return SurfaceClient.createPrivateSurface(terminalId);
		}
	}

	@NetworkEventMeta(direction = EventDirection.S2C, chunked = true, compressed = true)
	public static class PrivateDrawableData extends DrawableData {

		public PrivateDrawableData() {}

		public PrivateDrawableData(long terminalId) {
			super(terminalId);
		}

		@Override
		public SurfaceType getSurfaceType() {
			return SurfaceType.PRIVATE;
		}
	}

	@NetworkEventMeta(direction = EventDirection.S2C, chunked = true, compressed = true)
	public static class PrivateStructureData extends StructureData {
		public PrivateStructureData() {}

		public PrivateStructureData(long terminalId) {
			super(terminalId);
		}

		@Override
		public SurfaceType getSurfaceType() {
			return SurfaceType.PRIVATE;
		}
	}

	public abstract static class StructureData extends Data {
		public StructureData() {}

		public StructureData(long terminalId) {
			super(terminalId);
		}

		@Override
		public SurfaceClient createSurface() {
			return SurfaceClient.createPublicSurface(terminalId);
		}
	}

	@NetworkEventMeta(direction = EventDirection.S2C, chunked = true, compressed = true)
	public static class PublicStructureData extends StructureData {
		public PublicStructureData() {}

		public PublicStructureData(long terminalId) {
			super(terminalId);
		}

		@Override
		public SurfaceType getSurfaceType() {
			return SurfaceType.GLOBAL;
		}
	}

	@NetworkEventMeta(direction = EventDirection.S2C, chunked = true, compressed = true)
	public static class PublicDrawableData extends DrawableData {

		public PublicDrawableData() {}

		public PublicDrawableData(long terminalId) {
			super(terminalId);
		}

		@Override
		public SurfaceType getSurfaceType() {
			return SurfaceType.GLOBAL;
		}
	}

} @ApiSingleton
public class TerminalIdAccess implements ITerminalIdAccess {

	abstract static class HelmetGetterAdapter implements ITerminalIdGetter {
		@Override
		public Optional<Long> getFor(EntityPlayer player) {
			final ItemStack stack = TerminalUtils.getHeadSlot(player);
			return stack != null? getFor(stack) : Optional.<Long> absent();
		}

		protected abstract Optional<Long> getFor(ItemStack helmet);

		@Override
		public int getPriority() {
			return 0;
		}
	}

	abstract static class HandSetterAdapter implements ITerminalIdSetter {
		@Override
		public boolean setFor(EntityPlayer player, long guid) {
			final ItemStack stack = player.getHeldItem();
			return stack != null? setFor(stack, guid) : false;
		}

		protected abstract boolean setFor(ItemStack helmet, long guid);

		@Override
		public int getPriority() {
			return 0;
		}
	}

	public static class InterfaceGetter extends HelmetGetterAdapter {
		@Override
		public Optional<Long> getFor(ItemStack stack) {
			final Item item = stack.getItem();
			if (item instanceof ITerminalItem) {
				final ITerminalItem terminalItem = (ITerminalItem)item;
				return Optional.fromNullable(terminalItem.getTerminalGuid(stack));
			}

			return Optional.absent();
		}
	}

	public static class InterfaceSetter extends HandSetterAdapter {
		@Override
		public boolean setFor(ItemStack stack, long guid) {
			final Item item = stack.getItem();
			if (item instanceof ITerminalItem) {
				final ITerminalItem terminalItem = (ITerminalItem)item;
				terminalItem.bindToTerminal(stack, guid);
				return true;
			}

			return false;
		}
	}

	public static final TerminalIdAccess instance = new TerminalIdAccess();

	private ITerminalIdGetter[] getters = new ITerminalIdGetter[0];

	private ITerminalIdSetter[] setters = new ITerminalIdSetter[0];

	private TerminalIdAccess() {}

	@Override
	public Optional<Long> getIdFrom(EntityPlayer player) {
		for (ITerminalIdGetter getter : getters) {
			final Optional<Long> result = getter.getFor(player);
			if (result.isPresent()) return result;
		}

		return Optional.absent();
	}

	@Override
	public void register(ITerminalIdGetter getter) {
		Preconditions.checkNotNull(getter);
		getters = ArrayUtils.add(getters, getter);
		Arrays.sort(getters, new Comparator<ITerminalIdGetter>() {
			@Override
			public int compare(ITerminalIdGetter o1, ITerminalIdGetter o2) {
				return Ints.compare(o1.getPriority(), o2.getPriority());
			}
		});
	}

	@Override
	public boolean setIdFor(EntityPlayer player, long guid) {
		for (ITerminalIdSetter setter : setters)
			if (setter.setFor(player, guid)) return true;

		return false;
	}

	@Override
	public void register(ITerminalIdSetter setter) {
		Preconditions.checkNotNull(setter);
		setters = ArrayUtils.add(setters, setter);

		Arrays.sort(setters, new Comparator<ITerminalIdSetter>() {
			@Override
			public int compare(ITerminalIdSetter o1, ITerminalIdSetter o2) {
				return Ints.compare(o1.getPriority(), o2.getPriority());
			}
		});
	}

} public class TerminalUtils {

	public static Long extractGuid(NBTTagCompound tag) {
		NBTBase guidTag = tag.getTag("guid");
		if (guidTag instanceof NBTTagString) {
			String value = ((NBTTagString)guidTag).func_150285_a_();
			return Long.parseLong(value.toLowerCase(), 36);
		} else if (guidTag instanceof NBTTagLong) return ((NBTPrimitive)guidTag).func_150291_c();

		return null;
	}

	public static String formatTerminalId(long terminalId) {
		return Long.toString(terminalId, 36).toUpperCase();
	}

	static long generateGuid() {
		return Long.parseLong(StringUtils.randomString(8), 36);
	}

	public static final UUID GLOBAL_SURFACE_UUID = UUID.fromString("df66eff0-1cae-11e4-8c21-0800200c9a66");

	public static ItemStack getHeadSlot(EntityPlayer player) {
		return player.inventory.armorItemInSlot(3);
	}

} @Asynchronous
@AdapterSourceName("glasses_drawable")
public abstract class Drawable extends StructuredObjectBase {

	public static enum VerticalAlignment {
		TOP,
		MIDDLE,
		BOTTOM;

		public static final VerticalAlignment[] VALUES = values();
	}

	public static enum HorizontalAlignment {
		LEFT,
		MIDDLE,
		RIGHT;

		public static final HorizontalAlignment[] VALUES = values();
	}

	private static class Alignment implements IStructureElement {
		private static final int MASK = 0x3;

		private int id;

		public VerticalAlignment screenVerticalAnchor = VerticalAlignment.TOP;
		public HorizontalAlignment screenHorizontalAnchor = HorizontalAlignment.LEFT;

		public VerticalAlignment objectVerticalAnchor = VerticalAlignment.TOP;
		public HorizontalAlignment objectHorizontalAnchor = HorizontalAlignment.LEFT;

		@Override
		public void readFromStream(DataInput input) throws IOException {
			byte value = input.readByte();
			screenVerticalAnchor = VerticalAlignment.VALUES[(value >> 0) & MASK];
			screenHorizontalAnchor = HorizontalAlignment.VALUES[(value >> 2) & MASK];

			objectVerticalAnchor = VerticalAlignment.VALUES[(value >> 4) & MASK];
			objectHorizontalAnchor = HorizontalAlignment.VALUES[(value >> 6) & MASK];
		}

		@Override
		public void writeToStream(DataOutput output) throws IOException {
			byte value = (byte)((screenVerticalAnchor.ordinal() << 0) |
					(screenHorizontalAnchor.ordinal() << 2) |
					(objectVerticalAnchor.ordinal() << 4) |
					(objectHorizontalAnchor.ordinal() << 6));
			output.writeByte(value);
		}

		public float getScreenAnchorX(float screenWidth) {
			switch (screenHorizontalAnchor) {
				case MIDDLE:
					return screenWidth / 2.0f;
				case RIGHT:
					return screenWidth;
				case LEFT:
				default:
					return 0;

			}
		}

		public float getObjectAnchorX(Box2d box) {
			switch (objectHorizontalAnchor) {
				case MIDDLE:
					return -box.width / 2.0f;
				case RIGHT:
					return -box.width;
				case LEFT:
				default:
					return 0;

			}
		}

		public float getScreenAnchorY(float screenHeight) {
			switch (screenVerticalAnchor) {
				case BOTTOM:
					return screenHeight;
				case MIDDLE:
					return screenHeight / 2.0f;
				default:
				case TOP:
					return 0;
			}
		}

		public float getObjectAnchorY(Box2d box) {
			switch (objectVerticalAnchor) {
				case BOTTOM:
					return -box.height;
				case MIDDLE:
					return -box.height / 2.0f;
				default:
				case TOP:
					return 0;
			}
		}
	}

	private Box2d boundingBox = Box2d.NULL;

	private float rotationSin;

	private float rotationCos;

	private final Alignment alignment = new Alignment();

	@Property
	@StructureField
	public short z;

	@Property
	@StructureField
	public boolean visible = true;

	@Property
	@StructureField
	public boolean clickable = true;

	@Property
	@StructureField
	public float rotation = 0;

	@Property(type = ArgType.OBJECT,
			getterDesc = "Get userdata",
			setterDesc = "Set userdata (no restrictions, not sent to clients)",
			nullable = true)
	public Object userdata;

	protected Drawable() {}

	public void onUpdate() {
		final double rotationRad = rotation / 180 * Math.PI;
		rotationCos = (float)Math.cos(rotationRad);
		rotationSin = (float)Math.sin(rotationRad);
	}

	public Point2d transformToLocal(float worldX, float worldY, float screenWidth, float screenHeight) {
		final float worldLeft = alignment.getScreenAnchorX(screenWidth) + boundingBox.left;
		final float worldTop = alignment.getScreenAnchorY(screenHeight) + boundingBox.top;
		worldX -= worldLeft;
		worldY -= worldTop;

		float localX = worldX * +rotationCos + worldY * +rotationSin;
		float localY = worldX * -rotationSin + worldY * +rotationCos;

		localX -= alignment.getObjectAnchorX(boundingBox);
		localY -= alignment.getObjectAnchorY(boundingBox);

		return new Point2d(localX, localY);
	}

	@SideOnly(Side.CLIENT)
	public void draw(ScaledResolution resolution, RenderState renderState, float partialTicks) {
		final float screenX = alignment.getScreenAnchorX(resolution.getScaledWidth()) + boundingBox.left;
		final float screenY = alignment.getScreenAnchorY(resolution.getScaledHeight()) + boundingBox.top;

		final float anchorX = alignment.getObjectAnchorX(boundingBox);
		final float anchorY = alignment.getObjectAnchorY(boundingBox);

		GL11.glPushMatrix();
		if (rotation != 0) {
			GL11.glTranslatef(screenX, screenY, z);
			GL11.glRotated(rotation, 0, 0, 1);
			GL11.glTranslatef(anchorX, anchorY, 0);
		} else {
			GL11.glTranslatef(screenX + anchorX, screenY + anchorY, z);
		}

		drawContents(renderState, partialTicks);
		GL11.glPopMatrix();
	}

	protected void setBoundingBox(Box2d boundingBox) {
		Preconditions.checkNotNull(boundingBox);
		this.boundingBox = boundingBox;
	}

	@SideOnly(Side.CLIENT)
	protected abstract void drawContents(RenderState renderState, float partialTicks);

	protected abstract DrawableType getTypeEnum();

	protected abstract boolean isVisible();

	public final boolean shouldRender() {
		return visible && isVisible();
	}

	public boolean isClickable() {
		return clickable;
	}

	@Override
	public int getType() {
		return getTypeEnum().ordinal();
	}

	@ScriptCallable(returnTypes = ReturnType.STRING, name = "getType", description = "Get object type")
	public String getTypeName() {
		return getTypeEnum().name().toLowerCase();
	}

	public VerticalAlignment getScreenVerticalAnchor() {
		return alignment.screenVerticalAnchor;
	}

	@ScriptCallable
	public void setScreenAnchor(@Arg(name = "horizontal") HorizontalAlignment horizontal, @Arg(name = "vertical") VerticalAlignment vertical) {
		alignment.screenVerticalAnchor = vertical;
		alignment.screenHorizontalAnchor = horizontal;
		markModified(alignment.id);
	}

	@ScriptCallable
	public void setObjectAnchor(@Arg(name = "horizontal") HorizontalAlignment horizontal, @Arg(name = "vertical") VerticalAlignment vertical) {
		alignment.objectVerticalAnchor = vertical;
		alignment.objectHorizontalAnchor = horizontal;
		markModified(alignment.id);
	}

	@ScriptCallable
	public void setAlignment(@Arg(name = "horizontal") HorizontalAlignment horizontal, @Arg(name = "vertical") VerticalAlignment vertical) {
		alignment.objectVerticalAnchor = alignment.screenVerticalAnchor = vertical;
		alignment.objectHorizontalAnchor = alignment.screenHorizontalAnchor = horizontal;
		markModified(alignment.id);
	}

	@Override
	public void createElements(IElementAddCallback<IStructureElement> callback) {
		super.createElements(callback);
		alignment.id = callback.addElement(alignment);
	}

	public Box2d getBoundingBox() {
		return boundingBox;
	}
} @ScriptStruct
@SerializableClass
public class Point2d {

	public static final Point2d NULL = new Point2d(0, 0);

	public Point2d() {}

	public Point2d(float x, float y) {
		this.x = x;
		this.y = y;
	}

	@Serialize
	@StructField(index = 0)
	public float x;

	@Serialize
	@StructField(index = 1)
	public float y;

	@Override
	public String toString() {
		return "(" + x + "," + y + ")";
	}

} public class RenderState {

	private final FloatBuffer colors = BufferUtils.createFloatBuffer(16);

	private boolean lighting;

	private boolean alphaTest;

	private boolean depthTest;

	private boolean texture;

	private boolean blend;

	private int blendSrc;

	private int blendDst;

	private float pointSize;

	private float lineWidth;

	private boolean cullFace;

	private int color;

	public void forceKnownState() {
		GL11.glDisable(GL11.GL_LIGHTING);
		this.lighting = false;

		GL11.glDisable(GL11.GL_ALPHA_TEST);
		this.alphaTest = false;

		GL11.glDisable(GL11.GL_TEXTURE_2D);
		this.texture = false;

		GL11.glDisable(GL11.GL_DEPTH_TEST);
		this.depthTest = false;

		GL11.glEnable(GL11.GL_BLEND);
		this.blend = true;

		this.blendDst = GL11.GL_ONE_MINUS_SRC_ALPHA;
		this.blendSrc = GL11.GL_SRC_ALPHA;
		GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);

		GL11.glDisable(GL11.GL_CULL_FACE);
		this.cullFace = false;

		GL11.glPointSize(1.0f);
		this.pointSize = 1.0f;

		GL11.glLineWidth(1.0f);
		this.lineWidth = 1.0f;
	}

	public void readState() {
		this.lighting = GL11.glIsEnabled(GL11.GL_LIGHTING);
		this.alphaTest = GL11.glIsEnabled(GL11.GL_ALPHA_TEST);
		this.texture = GL11.glIsEnabled(GL11.GL_TEXTURE_2D);
		this.depthTest = GL11.glIsEnabled(GL11.GL_DEPTH_TEST);
		this.blend = GL11.glIsEnabled(GL11.GL_BLEND);
		this.blendSrc = GL11.glGetInteger(GL11.GL_BLEND_SRC);
		this.blendDst = GL11.glGetInteger(GL11.GL_BLEND_DST);
		this.cullFace = GL11.glIsEnabled(GL11.GL_CULL_FACE);
		this.lineWidth = GL11.glGetFloat(GL11.GL_LINE_WIDTH);
		this.pointSize = GL11.glGetFloat(GL11.GL_POINT_SIZE);

		GL11.glGetFloat(GL11.GL_CURRENT_COLOR, colors);

		float r = colors.get();
		float g = colors.get();
		float b = colors.get();
		float a = colors.get();
		this.color = (((int)(255 * r) & 0xFF) << 24) + (((int)(255 * g) & 0xFF) << 16) + (((int)(255 * b) & 0xFF) << 8) + (((int)(255 * a) & 0xFF) << 0);
		colors.clear();
	}

	public void setupCommonRender() {
		disableLight();
		disableAlphaTest();
		disableDepthTest();
		enableBlending();
		setBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
		disableCullFace();
	}

	public void setupSolidRender() {
		setupCommonRender();
		disableTexture();
	}

	public void setupTexturedRender() {
		setupCommonRender();
		enableTexture();
	}

	public void setColor(int rgb, float opacity) {
		final byte scaledOpacity = (byte)(opacity * 255);
		int newColor = rgb << 8 | (scaledOpacity & 0xFF);

		if (newColor != color) {
			final byte r = (byte)(rgb >> 16);
			final byte g = (byte)(rgb >> 8);
			final byte b = (byte)(rgb >> 0);

			GL11.glColor4ub(r, g, b, scaledOpacity);
			this.color = newColor;
		}
	}

	public void setLineWidth(float lineWidth) {
		if (lineWidth != this.lineWidth) {
			GL11.glLineWidth(lineWidth);
			this.lineWidth = lineWidth;
		}
	}

	public void setPointSize(final float pointSize) {
		if (pointSize != this.pointSize) {
			GL11.glPointSize(pointSize);
			this.pointSize = pointSize;
		}
	}

	public void enableBlending() {
		if (!blend) {
			GL11.glEnable(GL11.GL_BLEND);
			blend = true;
		}
	}

	public void setBlendFunc(int src, int dst) {
		if (dst != this.blendDst || src != this.blendSrc) {
			GL11.glBlendFunc(src, dst);
			this.blendDst = dst;
			this.blendSrc = src;
		}
	}

	public void disableBlending() {
		if (blend) {
			GL11.glDisable(GL11.GL_BLEND);
			blend = false;
		}
	}

	public void enableDepthTest() {
		if (!depthTest) {
			GL11.glEnable(GL11.GL_DEPTH_TEST);
			depthTest = true;
		}
	}

	public void disableDepthTest() {
		if (depthTest) {
			GL11.glDisable(GL11.GL_DEPTH_TEST);
			depthTest = false;
		}
	}

	public void enableTexture() {
		if (!texture) {
			GL11.glEnable(GL11.GL_TEXTURE_2D);
			texture = true;
		}
	}

	public void disableTexture() {
		if (texture) {
			GL11.glDisable(GL11.GL_TEXTURE_2D);
			texture = false;
		}
	}

	public void enableAlphaTest() {
		if (!alphaTest) {
			GL11.glEnable(GL11.GL_ALPHA_TEST);
			alphaTest = true;
		}
	}

	public void disableAlphaTest() {
		if (alphaTest) {
			GL11.glDisable(GL11.GL_ALPHA_TEST);
			alphaTest = false;
		}
	}

	public void enableLight() {
		if (!lighting) {
			GL11.glEnable(GL11.GL_LIGHTING);
			lighting = true;
		}
	}

	public void disableLight() {
		if (lighting) {
			GL11.glDisable(GL11.GL_LIGHTING);
			lighting = false;
		}
	}

	public void enableCullFace() {
		if (!cullFace) {
			GL11.glEnable(GL11.GL_CULL_FACE);
			cullFace = true;
		}
	}

	public void disableCullFace() {
		if (cullFace) {
			GL11.glDisable(GL11.GL_CULL_FACE);
			cullFace = false;
		}
	}

}
import com.google.common.base.Optional;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Lists;
import com.google.common.collect.Table;
import cpw.mods.fml.client.FMLClientHandler;
import cpw.mods.fml.common.FMLCommonHandler;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.common.gameevent.TickEvent.ClientTickEvent;
import cpw.mods.fml.common.gameevent.TickEvent.Phase;
import cpw.mods.fml.common.network.FMLNetworkEvent.ClientDisconnectionFromServerEvent;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiScreen;
import net.minecraft.client.gui.ScaledResolution;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.util.StatCollector;
import net.minecraftforge.client.event.RenderGameOverlayEvent;
import net.minecraftforge.client.event.RenderGameOverlayEvent.ElementType;
import net.minecraftforge.event.entity.player.ItemTooltipEvent;
import openmods.geometry.Box2d;
import openperipheral.addons.glasses.GlassesEvent.GlassesChangeBackgroundEvent;
import openperipheral.addons.glasses.GlassesEvent.GlassesSetDragParamsEvent;
import openperipheral.addons.glasses.GlassesEvent.GlassesSetGuiVisibilityEvent;
import openperipheral.addons.glasses.GlassesEvent.GlassesSetKeyRepeatEvent;
import openperipheral.addons.glasses.GlassesEvent.GlassesStopCaptureEvent;
import openperipheral.addons.glasses.GuiCapture;
import openperipheral.addons.glasses.NbtGuidProviders;
import openperipheral.addons.glasses.SurfaceType;
import openperipheral.addons.glasses.TerminalEvent;
import openperipheral.addons.glasses.TerminalIdAccess;
import openperipheral.addons.glasses.TerminalUtils;
import openperipheral.addons.glasses.drawable.Drawable;
import openperipheral.addons.glasses.utils.Point2d;
import openperipheral.addons.glasses.utils.RenderState;
import org.lwjgl.opengl.GL11;

			for (Drawable drawable : surface.getSortedDrawables())
				if (drawable.shouldRender()) drawable.draw(resolution, renderState, partialTicks);
			GL11.glPopAttrib();
		}
	}

	public class ForgeBusListener {

		@SubscribeEvent
		public void onRenderGameOverlay(RenderGameOverlayEvent.Pre evt) {
			if (evt.type == ElementType.ALL) {
				GuiScreen gui = FMLClientHandler.instance().getClient().currentScreen;

				if (gui instanceof GuiCapture) {
					final GuiCapture capture = (GuiCapture)gui;
					// this must be here, since there are some elements (like food bar) that are overriden every tick
					capture.forceGuiElementsState();
				}
			}
		}

		@SubscribeEvent
		public void onRenderGameOverlay(RenderGameOverlayEvent.Post evt) {
			if (evt.type == ElementType.HELMET) {
				if (terminalGuid.isPresent()) {
					final long guid = terminalGuid.get();
					tryDrawSurface(guid, SurfaceType.PRIVATE, evt.partialTicks, evt.resolution);
					tryDrawSurface(guid, SurfaceType.GLOBAL, evt.partialTicks, evt.resolution);
				}
			}
		}

		@SubscribeEvent
		public void onItemTooltip(ItemTooltipEvent evt) {
			if (evt.itemStack != null && NbtGuidProviders.hasTerminalCapabilities(evt.itemStack)) {
				final Optional<Long> guid = NbtGuidProviders.getTerminalGuid(evt.itemStack);
				if (guid.isPresent()) {
					evt.toolTip.add(StatCollector.translateToLocalFormatted("openperipheral.terminal.key", TerminalUtils.formatTerminalId(guid.get())));
				} else {
					evt.toolTip.add(StatCollector.translateToLocal("openperipheral.terminal.unbound"));
				}
			}
		}

		private SurfaceClient getOrCreateSurface(TerminalEvent.Data evt) {
			final SurfaceType surfaceType = evt.getSurfaceType();
			SurfaceClient surface = surfaces.get(evt.terminalId, surfaceType);

			if (surface == null) {
				surface = evt.createSurface();
				surfaces.put(evt.terminalId, surfaceType, surface);
			}
			return surface;
		}

		@SubscribeEvent
		public void onTerminalData(TerminalEvent.PrivateDrawableData evt) {
			updateTerminalDrawables(evt);
		}

		@SubscribeEvent
		public void onTerminalData(TerminalEvent.PublicDrawableData evt) {
			updateTerminalDrawables(evt);
		}

		private void updateTerminalDrawables(TerminalEvent.DrawableData evt) {
			final SurfaceClient surface = getOrCreateSurface(evt);
			surface.drawablesContainer.interpretCommandList(evt.commands);
		}

		@SubscribeEvent
		public void onTerminalClear(TerminalEvent.PrivateClear evt) {
			clearTerminal(evt);
		}

		@SubscribeEvent
		public void onTerminalClear(TerminalEvent.PublicClear evt) {
			clearTerminal(evt);
		}

		private void clearTerminal(TerminalEvent.Clear evt) {
			final SurfaceType surfaceType = evt.getSurfaceType();
			surfaces.remove(evt.terminalId, surfaceType);
		}

		@SubscribeEvent
		public void onBackgroundChange(GlassesChangeBackgroundEvent evt) {
			GuiScreen gui = FMLClientHandler.instance().getClient().currentScreen;

			if (gui instanceof GuiCapture) {
				final GuiCapture capture = (GuiCapture)gui;
				long guid = capture.getGuid();
				if (guid == evt.guid) capture.setBackground(evt.backgroundColor);
			}
		}

		@SubscribeEvent
		public void onKeyRepeatSet(GlassesSetKeyRepeatEvent evt) {
			GuiScreen gui = FMLClientHandler.instance().getClient().currentScreen;

			if (gui instanceof GuiCapture) {
				final GuiCapture capture = (GuiCapture)gui;
				long guid = capture.getGuid();
				if (guid == evt.guid) capture.setKeyRepeat(evt.repeat);
			}
		}

		@SubscribeEvent
		public void onDragParamsSet(GlassesSetDragParamsEvent evt) {
			GuiScreen gui = FMLClientHandler.instance().getClient().currentScreen;

			if (gui instanceof GuiCapture) {
				final GuiCapture capture = (GuiCapture)gui;
				long guid = capture.getGuid();
				if (guid == evt.guid) capture.setDragParameters(evt.threshold, evt.period);
			}
		}

		@SubscribeEvent
		public void onGuiVisibilitySet(GlassesSetGuiVisibilityEvent evt) {