Java tutorial
/* * Visage * Copyright (c) 2015-2016, Aesen Vismea <aesen@unascribed.com> * * The MIT License * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.surgeplay.visage.slave; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.util.ArrayDeque; import java.util.Deque; import java.util.Map; import java.util.logging.Level; import java.util.zip.InflaterInputStream; import javax.imageio.ImageIO; import org.spacehq.mc.auth.GameProfile; import com.google.common.collect.Maps; import com.rabbitmq.client.AMQP.BasicProperties; import com.rabbitmq.client.QueueingConsumer.Delivery; import com.sixlegs.png.PngImage; import com.surgeplay.visage.RenderMode; import com.surgeplay.visage.Visage; import com.surgeplay.visage.slave.render.Renderer; import com.surgeplay.visage.util.Images; import com.surgeplay.visage.util.Profiles; public class RenderThread extends Thread { private static int nextId = 1; private VisageSlave parent; private Renderer[] renderers; private boolean run = true; private Deque<Delivery> toProcess = new ArrayDeque<>(); public RenderThread(VisageSlave parent) { super("Render thread #" + (nextId++)); this.parent = parent; RenderMode[] modes = RenderMode.values(); renderers = new Renderer[modes.length]; for (int i = 0; i < modes.length; i++) { renderers[i] = modes[i].newRenderer(); } } @Override public void run() { try { Visage.log.info("Waiting for jobs"); try { while (run) { if (!toProcess.isEmpty()) { Delivery delivery = toProcess.pop(); try { processDelivery(delivery); } catch (Exception e) { Visage.log.log(Level.SEVERE, "An unexpected error occurred while rendering", e); BasicProperties props = delivery.getProperties(); BasicProperties replyProps = new BasicProperties.Builder() .correlationId(props.getCorrelationId()).build(); ByteArrayOutputStream ex = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(ex); oos.writeObject(e); oos.flush(); parent.channel.basicPublish("", props.getReplyTo(), replyProps, buildResponse(1, ex.toByteArray())); parent.channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); } } else { synchronized (toProcess) { toProcess.wait(); } } } for (Renderer r : renderers) { if (r != null) { r.destroy(); } } } catch (Exception e) { Visage.log.log(Level.SEVERE, "A fatal error has occurred in the render thread run loop.", e); } } catch (Exception e) { Visage.log.log(Level.SEVERE, "A fatal error has occurred while setting up a render thread.", e); } } public void process(Delivery delivery) throws IOException { toProcess.addLast(delivery); synchronized (toProcess) { toProcess.notify(); } } private void processDelivery(Delivery delivery) throws Exception { BasicProperties props = delivery.getProperties(); BasicProperties replyProps = new BasicProperties.Builder().correlationId(props.getCorrelationId()).build(); DataInputStream data = new DataInputStream( new InflaterInputStream(new ByteArrayInputStream(delivery.getBody()))); RenderMode mode = RenderMode.values()[data.readUnsignedByte()]; int width = data.readUnsignedShort(); int height = data.readUnsignedShort(); int supersampling = data.readUnsignedByte(); GameProfile profile = Profiles.readGameProfile(data); Map<String, String[]> params = Maps.newHashMap(); int len = data.readUnsignedShort(); for (int i = 0; i < len; i++) { String key = data.readUTF(); String[] val = new String[data.readUnsignedByte()]; for (int v = 0; v < val.length; v++) { val[v] = data.readUTF(); } params.put(key, val); } byte[] skinData = new byte[data.readInt()]; data.readFully(skinData); BufferedImage skinRaw = new PngImage().read(new ByteArrayInputStream(skinData), false); BufferedImage skin = Images.toARGB(skinRaw); Visage.log.info("Received a job to render a " + width + "x" + height + " " + mode.name().toLowerCase() + " (" + supersampling + "x supersampling) for " + (profile == null ? "null" : profile.getName())); byte[] pngBys = draw(mode, width, height, supersampling, profile, skin, params); if (Visage.trace) Visage.log.finest("Got png bytes"); parent.channel.basicPublish("", props.getReplyTo(), replyProps, buildResponse(0, pngBys)); if (Visage.trace) Visage.log.finest("Published response"); parent.channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); if (Visage.trace) Visage.log.finest("Ack'd message"); } private byte[] buildResponse(int type, byte[] payload) throws IOException { if (Visage.trace) Visage.log.finest("Building response of type " + type); ByteArrayOutputStream result = new ByteArrayOutputStream(); new DataOutputStream(result).writeUTF(parent.name); result.write(type); result.write(payload); byte[] resp = result.toByteArray(); if (Visage.trace) Visage.log.finest("Built - " + resp.length + " bytes long"); return resp; } public byte[] draw(RenderMode mode, int width, int height, int supersampling, GameProfile profile, BufferedImage skin, Map<String, String[]> params) throws Exception { boolean slim = Profiles.isSlim(profile); //BufferedImage cape; BufferedImage out; if (skin.getHeight() == 32) { if (Visage.debug) Visage.log.finer("Skin is legacy; painting onto new-style canvas"); BufferedImage canvas = new BufferedImage(64, 64, BufferedImage.TYPE_INT_ARGB); Graphics2D g = canvas.createGraphics(); g.drawImage(skin, 0, 0, null); g.drawImage(flipLimb(skin.getSubimage(0, 16, 16, 16)), 16, 48, null); g.drawImage(flipLimb(skin.getSubimage(40, 16, 16, 16)), 32, 48, null); g.dispose(); skin = canvas; } int color = skin.getRGB(32, 8); boolean equal = true; for (int x = 32; x < 64; x++) { for (int y = 0; y < 16; y++) { if (x < 40 && y < 8) continue; if (x > 54 && y < 8) continue; if (skin.getRGB(x, y) != color) { equal = false; break; } } } if (equal) { if (Visage.debug) Visage.log.finer("Skin has solid colored helm, stripping"); skin.setRGB(32, 0, 32, 16, new int[32 * 64], 0, 32); } if (Visage.trace) Visage.log.finest("Got skin"); if (Visage.trace) Visage.log.finest(mode.name()); switch (mode) { case SKIN: out = skin; break; default: { Renderer renderer = renderers[mode.ordinal()]; if (!renderer.isInitialized()) { if (Visage.trace) Visage.log.finest("Initialized renderer"); renderer.init(supersampling); } try { if (Visage.trace) Visage.log.finest("Uploading"); renderer.setSkin(skin); if (Visage.trace) Visage.log.finest("Rendering"); renderer.render(width, height); if (Visage.trace) Visage.log.finest("Rendered - reading pixels"); out = renderer.readPixels(width, height); if (Visage.trace) Visage.log.finest("Rescaled image"); } finally { renderer.finish(); if (Visage.trace) Visage.log.finest("Finished renderer"); } break; } } ByteArrayOutputStream png = new ByteArrayOutputStream(); ImageIO.write(out, "PNG", png); if (Visage.trace) Visage.log.finest("Wrote png"); return png.toByteArray(); } private BufferedImage flipLimb(BufferedImage in) { BufferedImage out = new BufferedImage(in.getWidth(), in.getHeight(), BufferedImage.TYPE_INT_ARGB); BufferedImage front = flipHorziontally(in.getSubimage(4, 4, 4, 12)); BufferedImage back = flipHorziontally(in.getSubimage(12, 4, 4, 12)); BufferedImage top = flipHorziontally(in.getSubimage(4, 0, 4, 4)); BufferedImage bottom = flipHorziontally(in.getSubimage(8, 0, 4, 4)); BufferedImage left = in.getSubimage(8, 4, 4, 12); BufferedImage right = in.getSubimage(0, 4, 4, 12); Graphics2D g = out.createGraphics(); g.drawImage(front, 4, 4, null); g.drawImage(back, 12, 4, null); g.drawImage(top, 4, 0, null); g.drawImage(bottom, 8, 0, null); g.drawImage(left, 0, 4, null); // left goes to right g.drawImage(right, 8, 4, null); // right goes to left g.dispose(); return out; } private BufferedImage flipHorziontally(BufferedImage in) { BufferedImage out = new BufferedImage(in.getWidth(), in.getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D g = out.createGraphics(); g.drawImage(in, 0, 0, in.getWidth(), in.getHeight(), in.getWidth(), 0, 0, in.getHeight(), null); g.dispose(); return out; } public void finish() { run = false; interrupt(); } }