Procházet zdrojové kódy

Merge branch 'renders-new'

jomo před 9 roky
rodič
revize
e8877c427a
12 změnil soubory, kde provedl 532 přidání a 486 odebrání
  1. 32 19
      lib/cache.js
  2. 45 39
      lib/helpers.js
  3. 30 50
      lib/networking.js
  4. 22 0
      lib/object-patch.js
  5. 216 170
      lib/renders.js
  6. 16 16
      lib/response.js
  7. 3 2
      lib/routes/avatars.js
  8. 6 5
      lib/routes/renders.js
  9. 4 3
      lib/routes/skins.js
  10. 6 2
      lib/skins.js
  11. 1 1
      package.json
  12. 151 179
      test/test.js

+ 32 - 19
lib/cache.js

@@ -70,6 +70,15 @@ exp.info = function(callback) {
   });
 };
 
+// set model type to value of *slim*
+exp.set_slim = function(rid, userId, slim, callback) {
+  logging.debug(rid, "setting slim for ", userId, "to " + slim);
+  // store userId in lower case if not null
+  userId = userId && userId.toLowerCase();
+
+  redis.hmset(userId, ["a", Number(slim)], callback);
+};
+
 // sets the timestamp for +userId+
 // if +temp+ is true, the timestamp is set so that the record will be outdated after 60 seconds
 // these 60 seconds match the duration of Mojang's rate limit ban
@@ -85,31 +94,34 @@ exp.update_timestamp = function(rid, userId, temp, callback) {
   });
 };
 
-// create the key +userId+, store +skin_hash+, +cape_hash+ and time
-// if either +skin_hash+ or +cape_hash+ are undefined, they will not be stored
-// this feature can be used to write both cape and skin at separate times
+// create the key +userId+, store +skin_hash+, +cape_hash+, +slim+ and current time
+// if +skin_hash+ or +cape_hash+ are undefined, they aren't stored
+// this is useful to store cape and skin at separate times, without overwriting the other
+// +slim+ can be true (alex) or false (steve)
 // +callback+ contans error
-exp.save_hash = function(rid, userId, skin_hash, cape_hash, callback) {
-  logging.debug(rid, "caching skin:" + skin_hash + " cape:" + cape_hash);
-  var time = Date.now();
-  // store shorter null byte instead of "null"
+exp.save_hash = function(rid, userId, skin_hash, cape_hash, slim, callback) {
+  logging.debug(rid, "caching skin:" + skin_hash + " cape:" + cape_hash + " slim:" + slim);
+  // store shorter null value instead of "null" string
   skin_hash = skin_hash === null ? "" : skin_hash;
   cape_hash = cape_hash === null ? "" : cape_hash;
   // store userId in lower case if not null
   userId = userId && userId.toLowerCase();
-  if (skin_hash === undefined) {
-    redis.hmset(userId, "c", cape_hash, "t", time, function(err) {
-      callback(err);
-    });
-  } else if (cape_hash === undefined) {
-    redis.hmset(userId, "s", skin_hash, "t", time, function(err) {
-      callback(err);
-    });
-  } else {
-    redis.hmset(userId, "s", skin_hash, "c", cape_hash, "t", time, function(err) {
-      callback(err);
-    });
+
+  var args = [];
+  if (cape_hash !== undefined) {
+    args.push("c", cape_hash);
+  }
+  if (skin_hash !== undefined) {
+    args.push("s", skin_hash);
+  }
+  if (slim !== undefined) {
+    args.push("a", Number(!!slim));
   }
+  args.push("t", Date.now());
+
+  redis.hmset(userId, args, function(err) {
+    callback(err);
+  });
 };
 
 // removes the hash for +userId+ from the cache
@@ -131,6 +143,7 @@ exp.get_details = function(userId, callback) {
       details = {
         skin: data.s === "" ? null : data.s,
         cape: data.c === "" ? null : data.c,
+        slim: data.a === "1",
         time: Number(data.t)
       };
     }

+ 45 - 39
lib/helpers.js

@@ -19,14 +19,19 @@ function get_hash(url) {
 // gets the skin for +userId+ with +profile+
 // uses +cache_details+ to determine if the skin needs to be downloaded or can be taken from cache
 // face and face+helm images are extracted and stored to files
-// callback: error, skin hash
+// callback: error, skin hash, callback
 function store_skin(rid, userId, profile, cache_details, callback) {
-  networking.get_skin_url(rid, userId, profile, function(err, url) {
+  networking.get_skin_info(rid, userId, profile, function(err, url, slim) {
+    if (!err && userId.length > 16) {
+      // updating username with model info from uuid details
+      cache.set_slim(rid, profile.name, slim);
+    }
+
     if (!err && url) {
       var skin_hash = get_hash(url);
       if (cache_details && cache_details.skin === skin_hash) {
         cache.update_timestamp(rid, userId, false, function(cache_err) {
-          callback(cache_err, skin_hash);
+          callback(cache_err, skin_hash, slim);
         });
       } else {
         logging.debug(rid, "new skin hash:", skin_hash);
@@ -36,25 +41,25 @@ function store_skin(rid, userId, profile, cache_details, callback) {
         fs.exists(facepath, function(exists) {
           if (exists) {
             logging.debug(rid, "skin already exists, not downloading");
-            callback(null, skin_hash);
+            callback(null, skin_hash, slim);
           } else {
             networking.get_from(rid, url, function(img, response, err1) {
               if (err1 || !img) {
-                callback(err1, null);
+                callback(err1, null, slim);
               } else {
                 skins.save_image(img, skinpath, function(skin_err, skin_img) {
                   if (skin_err) {
-                    callback(skin_err, null);
+                    callback(skin_err, null, slim);
                   } else {
                     skins.extract_face(img, facepath, function(err2) {
                       if (err2) {
-                        callback(err2, null);
+                        callback(err2, null, slim);
                       } else {
                         logging.debug(rid, "face extracted");
                         skins.extract_helm(rid, facepath, img, helmpath, function(err3) {
                           logging.debug(rid, "helm extracted");
                           logging.debug(rid, helmpath);
-                          callback(err3, skin_hash);
+                          callback(err3, skin_hash, slim);
                         });
                       }
                     });
@@ -128,7 +133,7 @@ function push_request(userId, type, fun) {
 }
 
 // calls back all queued requests that match userId and type
-function resume(userId, type, err, hash) {
+function resume(userId, type, err, hash, slim) {
   var userId_safe = "!" + userId;
   var callbacks = requests[type][userId_safe];
   if (callbacks) {
@@ -138,7 +143,7 @@ function resume(userId, type, err, hash) {
 
     for (var i = 0; i < callbacks.length; i++) {
       // continue the request
-      callbacks[i](err, hash);
+      callbacks[i](err, hash, slim);
       // remove from array
       callbacks.splice(i, 1);
       i--;
@@ -152,7 +157,7 @@ function resume(userId, type, err, hash) {
 // downloads the images for +userId+ while checking the cache
 // status based on +cache_details+. +type+ specifies which
 // image type should be called back on
-// callback: error, image hash
+// callback: error, image hash, slim
 function store_images(rid, userId, cache_details, type, callback) {
   var is_uuid = userId.length > 16;
   if (requests[type]["!" + userId]) {
@@ -167,34 +172,34 @@ function store_images(rid, userId, cache_details, type, callback) {
         // error or uuid without profile
         if (!err && !profile) {
           // no error, but uuid without profile
-          cache.save_hash(rid, userId, null, null, function(cache_err) {
+          cache.save_hash(rid, userId, null, null, undefined, function(cache_err) {
             // we have no profile, so we have neither skin nor cape
-            resume(userId, "skin", cache_err, null);
-            resume(userId, "cape", cache_err, null);
+            resume(userId, "skin", cache_err, null, false);
+            resume(userId, "cape", cache_err, null, false);
           });
         } else {
           // an error occured, not caching. we can try in 60 seconds
-          resume(userId, type, err, null);
+          resume(userId, type, err, null, false);
         }
       } else {
         // no error and we have a profile (if it's a uuid)
-        store_skin(rid, userId, profile, cache_details, function(store_err, skin_hash) {
+        store_skin(rid, userId, profile, cache_details, function(store_err, skin_hash, slim) {
           if (store_err && !skin_hash) {
             // an error occured, not caching. we can try in 60 seconds
-            resume(userId, "skin", store_err, null);
+            resume(userId, "skin", store_err, null, slim);
           } else {
-            cache.save_hash(rid, userId, skin_hash, undefined, function(cache_err) {
-              resume(userId, "skin", (store_err || cache_err), skin_hash);
+            cache.save_hash(rid, userId, skin_hash, undefined, slim, function(cache_err) {
+              resume(userId, "skin", (store_err || cache_err), skin_hash, slim);
             });
           }
         });
         store_cape(rid, userId, profile, cache_details, function(store_err, cape_hash) {
           if (store_err && !cape_hash) {
             // an error occured, not caching. we can try in 60 seconds
-            resume(userId, "cape", (store_err), cape_hash);
+            resume(userId, "cape", (store_err), cape_hash, false);
           } else {
-            cache.save_hash(rid, userId, undefined, cape_hash, function(cache_err) {
-              resume(userId, "cape", (store_err || cache_err), cape_hash);
+            cache.save_hash(rid, userId, undefined, cape_hash, undefined, function(cache_err) {
+              resume(userId, "cape", (store_err || cache_err), cape_hash, false);
             });
           }
         });
@@ -212,7 +217,7 @@ exp.id_valid = function(userId) {
 };
 
 // decides whether to get a +type+ image for +userId+ from disk or to download it
-// callback: error, status, hash
+// callback: error, status, hash, slim
 // for status, see response.js
 exp.get_image_hash = function(rid, userId, type, callback) {
   cache.get_details(userId, function(err, cache_details) {
@@ -221,31 +226,32 @@ exp.get_image_hash = function(rid, userId, type, callback) {
       cached_hash = type === "skin" ? cache_details.skin : cache_details.cape;
     }
     if (err) {
-      callback(err, -1, null);
+      callback(err, -1, null, false);
     } else {
       if (cache_details && cache_details[type] !== undefined && cache_details.time + config.caching.local * 1000 >= Date.now()) {
         // use cached image
         logging.debug(rid, "userId cached & recently updated");
-        callback(null, (cached_hash ? 1 : 0), cached_hash);
+        callback(null, (cached_hash ? 1 : 0), cached_hash, cache_details.slim);
       } else {
         // download image
-        if (cache_details) {
+        if (cache_details && cache_details[type] !== undefined) {
           logging.debug(rid, "userId cached, but too old");
+          logging.debug(rid, JSON.stringify(cache_details));
         } else {
           logging.debug(rid, "userId not cached");
         }
-        store_images(rid, userId, cache_details, type, function(store_err, new_hash) {
+        store_images(rid, userId, cache_details, type, function(store_err, new_hash, slim) {
           if (store_err) {
             // we might have a cached hash although an error occured
             // (e.g. Mojang servers not reachable, using outdated hash)
             cache.update_timestamp(rid, userId, true, function(err2) {
-              callback(err2 || store_err, -1, cache_details && cached_hash);
+              callback(err2 || store_err, -1, cache_details && cached_hash, slim);
             });
           } else {
             var status = cache_details && (cached_hash === new_hash) ? 3 : 2;
             logging.debug(rid, "cached hash:", (cache_details && cached_hash));
             logging.debug(rid, "new hash:", new_hash);
-            callback(null, status, new_hash);
+            callback(null, status, new_hash, slim);
           }
         });
       }
@@ -259,7 +265,7 @@ exp.get_image_hash = function(rid, userId, type, callback) {
 // image is the user's face+overlay when overlay is true, or the face otherwise
 // for status, see get_image_hash
 exp.get_avatar = function(rid, userId, overlay, size, callback) {
-  exp.get_image_hash(rid, userId, "skin", function(err, status, skin_hash) {
+  exp.get_image_hash(rid, userId, "skin", function(err, status, skin_hash, slim) {
     if (skin_hash) {
       var facepath = path.join(config.directories.faces, skin_hash + ".png");
       var helmpath = path.join(config.directories.helms, skin_hash + ".png");
@@ -284,25 +290,25 @@ exp.get_avatar = function(rid, userId, overlay, size, callback) {
 };
 
 // handles requests for +userId+ skins
-// callback: error, skin hash, status, image buffer
+// callback: error, skin hash, status, image buffer, slim
 exp.get_skin = function(rid, userId, callback) {
-  exp.get_image_hash(rid, userId, "skin", function(err, status, skin_hash) {
+  exp.get_image_hash(rid, userId, "skin", function(err, status, skin_hash, slim) {
     if (skin_hash) {
       var skinpath = path.join(config.directories.skins, skin_hash + ".png");
       fs.exists(skinpath, function(exists) {
         if (exists) {
           logging.debug(rid, "skin already exists, not downloading");
           skins.open_skin(rid, skinpath, function(skin_err, img) {
-            callback(skin_err || err, skin_hash, status, img);
+            callback(skin_err || err, skin_hash, status, img, slim);
           });
         } else {
           networking.save_texture(rid, skin_hash, skinpath, function(net_err, response, img) {
-            callback(net_err || err, skin_hash, status, img);
+            callback(net_err || err, skin_hash, status, img, slim);
           });
         }
       });
     } else {
-      callback(err, null, status, null);
+      callback(err, null, status, null, slim);
     }
   });
 };
@@ -318,12 +324,12 @@ function get_type(overlay, body) {
 // handles creations of 3D renders
 // callback: error, skin hash, image buffer
 exp.get_render = function(rid, userId, scale, overlay, body, callback) {
-  exp.get_skin(rid, userId, function(err, skin_hash, status, img) {
+  exp.get_skin(rid, userId, function(err, skin_hash, status, img, slim) {
     if (!skin_hash) {
       callback(err, status, skin_hash, null);
       return;
     }
-    var renderpath = path.join(config.directories.renders, [skin_hash, scale, get_type(overlay, body)].join("-") + ".png");
+    var renderpath = path.join(config.directories.renders, [skin_hash, scale, get_type(overlay, body), slim ? "s" : "t"].join("-") + ".png");
     fs.exists(renderpath, function(exists) {
       if (exists) {
         renders.open_render(rid, renderpath, function(render_err, rendered_img) {
@@ -335,7 +341,7 @@ exp.get_render = function(rid, userId, scale, overlay, body, callback) {
           callback(err, 0, skin_hash, null);
           return;
         }
-        renders.draw_model(rid, img, scale, overlay, body, function(draw_err, drawn_img) {
+        renders.draw_model(rid, img, scale, overlay, body, slim || userId.toLowerCase() === "mhf_alex", function(draw_err, drawn_img) {
           if (draw_err) {
             callback(draw_err, -1, skin_hash, null);
           } else if (!drawn_img) {
@@ -354,7 +360,7 @@ exp.get_render = function(rid, userId, scale, overlay, body, callback) {
 // handles requests for +userId+ capes
 // callback: error, cape hash, status, image buffer
 exp.get_cape = function(rid, userId, callback) {
-  exp.get_image_hash(rid, userId, "cape", function(err, status, cape_hash) {
+  exp.get_image_hash(rid, userId, "cape", function(err, status, cape_hash, slim) {
     if (!cape_hash) {
       callback(err, null, status, null);
       return;

+ 30 - 50
lib/networking.js

@@ -3,6 +3,7 @@ var logging = require("./logging");
 var request = require("request");
 var config = require("../config");
 var skins = require("./skins");
+require("./object-patch");
 
 var session_url = "https://sessionserver.mojang.com/session/minecraft/profile/";
 var skins_url = "https://skins.minecraft.net/MinecraftSkins/";
@@ -12,50 +13,22 @@ var mojang_urls = [skins_url, capes_url];
 
 var exp = {};
 
-// extracts the +type+ [SKIN|CAPE] URL
-// from the nested & encoded +profile+ object
-// returns the URL or null if not present
-function extract_url(profile, type) {
-  var url = null;
-  if (profile && profile.properties) {
-    profile.properties.forEach(function(prop) {
-      if (prop.name === "textures") {
-        var json = new Buffer(prop.value, "base64").toString();
-        var props = JSON.parse(json);
-        url = props && props.textures && props.textures[type] && props.textures[type].url || null;
-      }
-    });
-  }
-  return url;
-}
-
-// helper method that calls `get_username_url` or `get_uuid_url` based on the +usedId+
+// helper method that calls `get_username_url` or `get_uuid_info` based on the +usedId+
 // +userId+ is used for usernames, while +profile+ is used for UUIDs
-function get_url(rid, userId, profile, type, callback) {
+// callback: error, url, slim
+function get_info(rid, userId, profile, type, callback) {
   if (userId.length <= 16) {
     // username
     exp.get_username_url(rid, userId, type, function(err, url) {
-      callback(err, url || null);
+      callback(err, url || null, false);
     });
   } else {
-    exp.get_uuid_url(profile, type, function(url) {
-      callback(null, url || null);
+    exp.get_uuid_info(profile, type, function(url, slim) {
+      callback(null, url || null, slim);
     });
   }
 }
 
-// exracts the skin URL of a +profile+ object
-// returns null when no URL found (user has no skin)
-exp.extract_skin_url = function(profile) {
-  return extract_url(profile, "SKIN");
-};
-
-// exracts the cape URL of a +profile+ object
-// returns null when no URL found (user has no cape)
-exp.extract_cape_url = function(profile) {
-  return extract_url(profile, "CAPE");
-};
-
 // performs a GET request to the +url+
 // +options+ object includes these options:
 //   encoding (string), default is to return a buffer
@@ -133,6 +106,7 @@ exp.get_from = function(rid, url, callback) {
 // the skin url is taken from the HTTP redirect
 // type reference is above
 exp.get_username_url = function(rid, name, type, callback) {
+  type = Number(type === "CAPE");
   exp.get_from(rid, mojang_urls[type] + name + ".png", function(body, response, err) {
     if (!err) {
       if (response) {
@@ -147,15 +121,24 @@ exp.get_username_url = function(rid, name, type, callback) {
 };
 
 // gets the URL for a skin/cape from the profile
-// +type+ specifies which to retrieve
-exp.get_uuid_url = function(profile, type, callback) {
-  var url = null;
-  if (type === 0) {
-    url = exp.extract_skin_url(profile);
-  } else if (type === 1) {
-    url = exp.extract_cape_url(profile);
+// +type+ "SKIN" or "CAPE", specifies which to retrieve
+// callback: url, slim
+exp.get_uuid_info = function(profile, type, callback) {
+  var properties = Object.get(profile, "properties") || [];
+  properties.forEach(function(prop) {
+    if (prop.name === "textures") {
+      var json = new Buffer(prop.value, "base64").toString();
+      profile = JSON.parse(json);
+    }
+  });
+
+  var url = Object.get(profile, "textures." + type + ".url");
+  var slim;
+  if (type === "SKIN") {
+    slim = Object.get(profile, "textures.SKIN.metadata.model") === "slim";
   }
-  callback(url || null);
+
+  callback(url || null, !!slim);
 };
 
 // make a request to sessionserver for +uuid+
@@ -181,20 +164,17 @@ exp.get_profile = function(rid, uuid, callback) {
   }
 };
 
-// get the skin URL for +userId+
+// get the skin URL and type for +userId+
 // +profile+ is used if +userId+ is a uuid
-exp.get_skin_url = function(rid, userId, profile, callback) {
-  get_url(rid, userId, profile, 0, function(err, url) {
-    callback(err, url);
-  });
+// callback: error, url, slim
+exp.get_skin_info = function(rid, userId, profile, callback) {
+  get_info(rid, userId, profile, "SKIN", callback);
 };
 
 // get the cape URL for +userId+
 // +profile+ is used if +userId+ is a uuid
 exp.get_cape_url = function(rid, userId, profile, callback) {
-  get_url(rid, userId, profile, 1, function(err, url) {
-    callback(err, url);
-  });
+  get_info(rid, userId, profile, "CAPE", callback);
 };
 
 // download the +tex_hash+ image from the texture server

+ 22 - 0
lib/object-patch.js

@@ -0,0 +1,22 @@
+// Adds Object.get function
+// +pathstr+ is a string of dot-separated nested properties on +ojb+
+// returns undefined if any of the properties do not exist
+// returns the value of the last property otherwise
+//
+// Object.get({"foo": {"bar": 123}}, "foo.bar"); // 123
+// Object.get({"foo": {"bar": 123}}, "bar.foo"); // undefined
+Object.get = function(obj, pathstr) {
+  var path = pathstr.split(".");
+  var result = obj;
+
+  for (var i = 0; i < path.length; i++) {
+    var key = path[i];
+    if (!result || !result.hasOwnProperty(key)) {
+      return undefined;
+    } else {
+      result = result[key];
+    }
+  }
+
+  return result;
+};

+ 216 - 170
lib/renders.js

@@ -8,200 +8,246 @@ var Canvas = require("canvas");
 var Image = Canvas.Image;
 var exp = {};
 
-// scales an image from the +imagedata+ onto the +context+
-// scaled by a factor of +scale+ with options +d_x+ and +d_y+
-function scale_image(imageData, context, d_x, d_y, scale) {
-  var width = imageData.width;
-  var height = imageData.height;
-  context.clearRect(0, 0, width, height); // Clear the spot where it originated from
-  for (var y = 0; y < height; y++) { // height original
-    for (var x = 0; x < width; x++) { // width original
-      // Gets original colour, then makes a scaled square of the same colour
-      var index = (x + y * width) * 4;
-      context.fillStyle = "rgba(" + imageData.data[index + 0] + ", " + imageData.data[index + 1] + ", " + imageData.data[index + 2] + ", " + imageData.data[index + 3] + ")";
-      context.fillRect(d_x + x * scale, d_y + y * scale, scale, scale);
-    }
+// set alpha values to 255
+function removeTransparency(canvas) {
+  var ctx = canvas.getContext("2d");
+  var imagedata = ctx.getImageData(0, 0, canvas.width, canvas.height);
+  var data = imagedata.data;
+  // data is [r,g,b,a, r,g,b,a, *]
+  for (var i = 0; i < data.length; i += 4) {
+    // usually we would have to check for alpha = 0
+    // and set color to black here
+    // but node-canvas already does that for us
+
+    // remove transparency
+    data[i + 3] = 255;
   }
+  ctx.putImageData(imagedata, 0, 0);
+  return canvas;
 }
 
-// makes images less worse by using binary transparency
-function enhance(context) {
-  var imagedata = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
-  var data = imagedata.data;
-  // data is [r,g,b,a, r,g,b,a, *]
-  for (var i = 3; i < data.length; i += 4) {
-    // round to 0 or 255
-    data[i] = Math.round(data[i] / 255) * 255;
+function hasTransparency(canvas) {
+  var ctx = canvas.getContext("2d");
+  var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
+  for (var i = 3; i < imageData.length; i += 4) {
+    if (imageData[i] < 255) {
+      // found pixel with translucent alpha value
+      return true;
+    }
   }
-  context.putImageData(imagedata, 0, 0);
+  return false;
 }
 
-// draws the helmet on to the +skin_canvas+
-// using the skin from the +model_ctx+ at the +scale+
-exp.draw_helmet = function(skin_canvas, model_ctx, scale) {
-  // Helmet - Front
-  model_ctx.setTransform(1, -0.5, 0, 1.2, 0, 0);
-  model_ctx.drawImage(skin_canvas, 40 * scale, 8 * scale, 8 * scale, 8 * scale, 10 * scale, 13 / 1.2 * scale, 8 * scale, 8 * scale);
-  // Helmet - Right
-  model_ctx.setTransform(1, 0.5, 0, 1.2, 0, 0);
-  model_ctx.drawImage(skin_canvas, 32 * scale, 8 * scale, 8 * scale, 8 * scale, 2 * scale, 3 / 1.2 * scale, 8 * scale, 8 * scale);
-  // Helmet - Top
-  model_ctx.setTransform(-1, 0.5, 1, 0.5, 0, 0);
-  model_ctx.scale(-1, 1);
-  model_ctx.drawImage(skin_canvas, 40 * scale, 0, 8 * scale, 8 * scale, -3 * scale, 5 * scale, 8 * scale, 8 * scale);
-};
+function resize(src, scale) {
+  var dst = new Canvas();
+  dst.width = scale * src.width;
+  dst.height = scale * src.height;
+  var context = dst.getContext("2d");
 
-// draws the head on to the +skin_canvas+
-// using the skin from the +model_ctx+ at the +scale+
-exp.draw_head = function(skin_canvas, model_ctx, scale) {
-  // Head - Front
-  model_ctx.setTransform(1, -0.5, 0, 1.2, 0, 0);
-  model_ctx.drawImage(skin_canvas, 8 * scale, 8 * scale, 8 * scale, 8 * scale, 10 * scale, 13 / 1.2 * scale, 8 * scale, 8 * scale);
-  // Head - Right
-  model_ctx.setTransform(1, 0.5, 0, 1.2, 0, 0);
-  model_ctx.drawImage(skin_canvas, 0, 8 * scale, 8 * scale, 8 * scale, 2 * scale, 3 / 1.2 * scale, 8 * scale, 8 * scale);
-  // Head - Top
-  model_ctx.setTransform(-1, 0.5, 1, 0.5, 0, 0);
-  model_ctx.scale(-1, 1);
-  model_ctx.drawImage(skin_canvas, 8 * scale, 0, 8 * scale, 8 * scale, -3 * scale, 5 * scale, 8 * scale, 8 * scale);
-};
+  // don't blur on resize
+  context.patternQuality = "fast";
 
-// draws the body on to the +skin_canvas+
-// using the skin from the +model_ctx+ at the +scale+
-// parts are labeled as if drawn from the skin's POV
-exp.draw_body = function(rid, skin_canvas, model_ctx, scale) {
-  if (skin_canvas.height === 32 * scale) {
-    logging.debug(rid, "uses old skin format");
-    // Left Leg
-    // Left Leg - Front
-    model_ctx.setTransform(1, -0.5, 0, 1.2, 0, 0);
-    model_ctx.scale(-1, 1);
-    model_ctx.drawImage(skin_canvas, 4 * scale, 20 * scale, 4 * scale, 12 * scale, -16 * scale, 34.4 / 1.2 * scale, 4 * scale, 12 * scale);
-
-    // Right Leg
-    // Right Leg - Right
-    model_ctx.setTransform(1, 0.5, 0, 1.2, 0, 0);
-    model_ctx.drawImage(skin_canvas, 0 * scale, 20 * scale, 4 * scale, 12 * scale, 4 * scale, 26.4 / 1.2 * scale, 4 * scale, 12 * scale);
-    // Right Leg - Front
-    model_ctx.setTransform(1, -0.5, 0, 1.2, 0, 0);
-    model_ctx.drawImage(skin_canvas, 4 * scale, 20 * scale, 4 * scale, 12 * scale, 8 * scale, 34.4 / 1.2 * scale, 4 * scale, 12 * scale);
-
-    // Arm Left
-    // Arm Left - Front
-    model_ctx.setTransform(1, -0.5, 0, 1.2, 0, 0);
-    model_ctx.scale(-1, 1);
-    model_ctx.drawImage(skin_canvas, 44 * scale, 20 * scale, 4 * scale, 12 * scale, -20 * scale, 20 / 1.2 * scale, 4 * scale, 12 * scale);
-    // Arm Left - Top
-    model_ctx.setTransform(-1, 0.5, 1, 0.5, 0, 0);
-    model_ctx.drawImage(skin_canvas, 44 * scale, 16 * scale, 4 * scale, 4 * scale, 0, 16 * scale, 4 * scale, 4 * scale);
-
-    // Body
-    // Body - Front
-    model_ctx.setTransform(1, -0.5, 0, 1.2, 0, 0);
-    model_ctx.drawImage(skin_canvas, 20 * scale, 20 * scale, 8 * scale, 12 * scale, 8 * scale, 20 / 1.2 * scale, 8 * scale, 12 * scale);
-
-    // Arm Right
-    // Arm Right - Right
-    model_ctx.setTransform(1, 0.5, 0, 1.2, 0, 0);
-    model_ctx.drawImage(skin_canvas, 40 * scale, 20 * scale, 4 * scale, 12 * scale, 0, 16 / 1.2 * scale, 4 * scale, 12 * scale);
-    // Arm Right - Front
-    model_ctx.setTransform(1, -0.5, 0, 1.2, 0, 0);
-    model_ctx.drawImage(skin_canvas, 44 * scale, 20 * scale, 4 * scale, 12 * scale, 4 * scale, 20 / 1.2 * scale, 4 * scale, 12 * scale);
-    // Arm Right - Top
-    model_ctx.setTransform(-1, 0.5, 1, 0.5, 0, 0);
-    model_ctx.scale(-1, 1);
-    model_ctx.drawImage(skin_canvas, 44 * scale, 16 * scale, 4 * scale, 4 * scale, -16 * scale, 16 * scale, 4 * scale, 4 * scale);
-  } else {
-    logging.debug(rid, "uses new skin format");
-    // Left Leg
-    // Left Leg - Front
-    model_ctx.setTransform(1, -0.5, 0, 1.2, 0, 0);
-    model_ctx.drawImage(skin_canvas, 20 * scale, 52 * scale, 4 * scale, 12 * scale, 12 * scale, 34.4 / 1.2 * scale, 4 * scale, 12 * scale);
-
-    // Right Leg
-    // Right Leg - Right
-    model_ctx.setTransform(1, 0.5, 0, 1.2, 0, 0);
-    model_ctx.drawImage(skin_canvas, 0, 20 * scale, 4 * scale, 12 * scale, 4 * scale, 26.4 / 1.2 * scale, 4 * scale, 12 * scale);
-    // Right Leg - Front
-    model_ctx.setTransform(1, -0.5, 0, 1.2, 0, 0);
-    model_ctx.drawImage(skin_canvas, 4 * scale, 20 * scale, 4 * scale, 12 * scale, 8 * scale, 34.4 / 1.2 * scale, 4 * scale, 12 * scale);
-
-    // Arm Left
-    // Arm Left - Front
-    model_ctx.setTransform(1, -0.5, 0, 1.2, 0, 0);
-    model_ctx.drawImage(skin_canvas, 36 * scale, 52 * scale, 4 * scale, 12 * scale, 16 * scale, 20 / 1.2 * scale, 4 * scale, 12 * scale);
-    // Arm Left - Top
-    model_ctx.setTransform(-1, 0.5, 1, 0.5, 0, 0);
-    model_ctx.drawImage(skin_canvas, 36 * scale, 48 * scale, 4 * scale, 4 * scale, 0, 16 * scale, 4 * scale, 4 * scale);
-
-    // Body
-    // Body - Front
-    model_ctx.setTransform(1, -0.5, 0, 1.2, 0, 0);
-    model_ctx.drawImage(skin_canvas, 20 * scale, 20 * scale, 8 * scale, 12 * scale, 8 * scale, 20 / 1.2 * scale, 8 * scale, 12 * scale);
-
-    // Arm Right
-    // Arm Right - Right
-    model_ctx.setTransform(1, 0.5, 0, 1.2, 0, 0);
-    model_ctx.drawImage(skin_canvas, 40 * scale, 20 * scale, 4 * scale, 12 * scale, 0, 16 / 1.2 * scale, 4 * scale, 12 * scale);
-    // Arm Right - Front
-    model_ctx.setTransform(1, -0.5, 0, 1.2, 0, 0);
-    model_ctx.drawImage(skin_canvas, 44 * scale, 20 * scale, 4 * scale, 12 * scale, 4 * scale, 20 / 1.2 * scale, 4 * scale, 12 * scale);
-    // Arm Right - Top
-    model_ctx.setTransform(-1, 0.5, 1, 0.5, 0, 0);
-    model_ctx.scale(-1, 1);
-    model_ctx.drawImage(skin_canvas, 44 * scale, 16 * scale, 4 * scale, 4 * scale, -16 * scale, 16 * scale, 4 * scale, 4 * scale);
-  }
-};
+  context.drawImage(src, 0, 0, src.width * scale, src.height * scale);
+  return dst;
+}
 
-// sets up the necessary components to draw the skin model
-// uses the +img+ skin with options of drawing
-// the +overlay+ and the +body+
-// callback: error, image buffer
-exp.draw_model = function(rid, img, scale, overlay, body, callback) {
-  var image = new Image();
+function getPart(src, x, y, width, height, scale) {
+  var dst = new Canvas();
+  dst.width = scale * width;
+  dst.height = scale * height;
+  var context = dst.getContext("2d");
 
-  image.onerror = function(err) {
-    callback(err, null);
-  };
+  // don't blur on resize
+  context.patternQuality = "fast";
+
+  context.drawImage(src, x, y, width, height, 0, 0, width * scale, height * scale);
+  return dst;
+}
+
+function flip(src) {
+  var dst = new Canvas();
+  dst.width = src.width;
+  dst.height = src.height;
+  var context = dst.getContext("2d");
+  context.scale(-1, 1);
+  context.drawImage(src, -src.width, 0);
+  return dst;
+}
+
+// skew for isometric perspective
+var skew_a = 26 / 45;    // 0.57777777
+var skew_b = skew_a * 2; // 1.15555555
+
+exp.draw_model = function(rid, img, scale, overlay, is_body, slim, callback) {
+  var canvas = new Canvas();
+  canvas.width = scale * 20;
+  canvas.height = scale * (is_body ? 45.1 : 18.5);
+
+  var ctx = canvas.getContext("2d");
+  var skin = new Image();
+
+  skin.onload = function() {
+    var old_skin = skin.height === 32;
+    var arm_width = slim ? 3 : 4;
+
+    /* eslint-disable no-multi-spaces */
+    var head_top        = resize(removeTransparency(getPart(skin, 8, 0, 8, 8, 1)), scale);
+    var head_front      = resize(removeTransparency(getPart(skin, 8, 8, 8, 8, 1)), scale);
+    var head_right      = resize(removeTransparency(getPart(skin, 0, 8, 8, 8, 1)), scale);
+
+    var arm_right_top   = resize(removeTransparency(getPart(skin, 44, 16, arm_width, 4, 1)), scale);
+    var arm_right_front = resize(removeTransparency(getPart(skin, 44, 20, arm_width, 12, 1)), scale);
+    var arm_right_side  = resize(removeTransparency(getPart(skin, 40, 20, 4, 12, 1)), scale);
+
+    var arm_left_top    = old_skin ? flip(arm_right_top)   : resize(removeTransparency(getPart(skin, 36, 48, arm_width, 4, 1)), scale);
+    var arm_left_front  = old_skin ? flip(arm_right_front) : resize(removeTransparency(getPart(skin, 36, 52, arm_width, 12, 1)), scale);
+
+    var leg_right_front = resize(removeTransparency(getPart(skin, 4, 20, 4, 12, 1)), scale);
+    var leg_right_side  = resize(removeTransparency(getPart(skin, 0, 20, 4, 12, 1)), scale);
+
+    var leg_left_front  = old_skin ? flip(leg_right_front) : resize(removeTransparency(getPart(skin, 20, 52, 4, 12, 1)), scale);
+
+    var body_front      = resize(removeTransparency(getPart(skin, 20, 20, 8, 12, 1)), scale);
+    /* eslint-enable no-multi-spaces */
 
-  image.onload = function() {
-    var width = 64 * scale;
-    var original_height = image.height === 32 ? 32 : 64;
-    var height = original_height * scale;
-    var model_canvas = new Canvas(20 * scale, (body ? 44.8 : 17.6) * scale);
-    var skin_canvas = new Canvas(width, height);
-    var model_ctx = model_canvas.getContext("2d");
-    var skin_ctx = skin_canvas.getContext("2d");
-
-    skin_ctx.drawImage(image, 0, 0, 64, original_height);
-    // Scale it
-    scale_image(skin_ctx.getImageData(0, 0, 64, original_height), skin_ctx, 0, 0, scale);
-    if (body) {
-      exp.draw_body(rid, skin_canvas, model_ctx, scale);
-    }
-    exp.draw_head(skin_canvas, model_ctx, scale);
     if (overlay) {
-      exp.draw_helmet(skin_canvas, model_ctx, scale);
+      if (hasTransparency(getPart(skin, 32, 0, 32, 32, 1))) {
+        // render head overlay
+        head_top.getContext("2d").drawImage(getPart(skin, 40, 0, 8, 8, scale), 0, 0);
+        head_front.getContext("2d").drawImage(getPart(skin, 40, 8, 8, 8, scale), 0, 0);
+        head_right.getContext("2d").drawImage(getPart(skin, 32, 8, 8, 8, scale), 0, 0);
+      }
+
+      if (!old_skin) {
+        // See #117
+        // if MC-89760 gets fixed, we can (probably) simply check the whole skin for transparency
+
+        /* eslint-disable no-multi-spaces */
+        var body_region      = getPart(skin, 16, 32, 32, 16, 1);
+        var right_arm_region = getPart(skin, 48, 48, 16, 16, 1);
+        var left_arm_region  = getPart(skin, 40, 32, 16, 16, 1);
+        var right_leg_region = getPart(skin, 0, 32, 16, 16, 1);
+        var left_leg_region  = getPart(skin, 0, 48, 16, 16, 1);
+        /* eslint-enable no-multi-spaces */
+
+        if (hasTransparency(body_region)) {
+          // render body overlay
+          body_front.getContext("2d").drawImage(getPart(skin, 20, 36, 8, 12, scale), 0, 0);
+        }
+
+        if (hasTransparency(right_arm_region)) {
+          // render right arm overlay
+          arm_right_top.getContext("2d").drawImage(getPart(skin, 44, 32, arm_width, 4, scale), 0, 0);
+          arm_right_front.getContext("2d").drawImage(getPart(skin, 44, 36, arm_width, 12, scale), 0, 0);
+          arm_right_side.getContext("2d").drawImage(getPart(skin, 40, 36, 4, 12, scale), 0, 0);
+        }
+
+        if (hasTransparency(left_arm_region)) {
+          // render left arm overlay
+          arm_left_top.getContext("2d").drawImage(getPart(skin, 36 + 16, 48, arm_width, 4, scale), 0, 0);
+          arm_left_front.getContext("2d").drawImage(getPart(skin, 36 + 16, 52, arm_width, 12, scale), 0, 0);
+        }
+
+        if (hasTransparency(right_leg_region)) {
+          // render right leg overlay
+          leg_right_front.getContext("2d").drawImage(getPart(skin, 4, 36, 4, 12, scale), 0, 0);
+          leg_right_side.getContext("2d").drawImage(getPart(skin, 0, 36, 4, 12, scale), 0, 0);
+        }
+
+        if (hasTransparency(left_leg_region)) {
+          // render left leg overlay
+          leg_left_front.getContext("2d").drawImage(getPart(skin, 4, 52, 4, 12, scale), 0, 0);
+        }
+      }
     }
 
-    // FIXME: This is a temporary fix for #32
-    enhance(model_ctx);
+    var x = 0;
+    var y = 0;
+    var z = 0;
+
+    var z_offset = scale * 3;
+    var x_offset = scale * 2;
+
+    if (is_body) {
+      // pre-render front onto separate canvas
+      var front = new Canvas();
+      front.width = scale * 16;
+      front.height = scale * 24;
+      var frontc = front.getContext("2d");
+      frontc.patternQuality = "fast";
+
+      frontc.drawImage(arm_right_front, (4 - arm_width) * scale, 0 * scale, arm_width * scale, 12 * scale);
+      frontc.drawImage(arm_left_front, 12 * scale, 0 * scale, arm_width * scale, 12 * scale);
+      frontc.drawImage(body_front, 4 * scale, 0 * scale, 8 * scale, 12 * scale);
+      frontc.drawImage(leg_right_front, 4 * scale, 12 * scale, 4 * scale, 12 * scale);
+      frontc.drawImage(leg_left_front, 8 * scale, 12 * scale, 4 * scale, 12 * scale);
+
+
+      // top
+      x = x_offset + scale * 2;
+      y = scale * -arm_width;
+      z = z_offset + scale * 8;
+      ctx.setTransform(1, -skew_a, 1, skew_a, 0, 0);
+      ctx.drawImage(arm_right_top, y - z - 0.5, x + z, arm_right_top.width + 1, arm_right_top.height + 1);
+
+      y = scale * 8;
+      ctx.drawImage(arm_left_top, y - z, x + z, arm_left_top.width, arm_left_top.height + 1);
+
+      // right side
+      ctx.setTransform(1, skew_a, 0, skew_b, 0, 0);
+      x = x_offset + scale * 2;
+      y = 0;
+      z = z_offset + scale * 20;
+      ctx.drawImage(leg_right_side, x + y, z - y, leg_right_side.width, leg_right_side.height);
+
+      x = x_offset + scale * 2;
+      y = scale * -arm_width;
+      z = z_offset + scale * 8;
+      ctx.drawImage(arm_right_side, x + y, z - y - 0.5, arm_right_side.width, arm_right_side.height + 1);
+
+      // front
+      z = z_offset + scale * 12;
+      y = 0;
+      ctx.setTransform(1, -skew_a, 0, skew_b, 0, skew_a);
+      ctx.drawImage(front, y + x, x + z - 0.5, front.width, front.height);
+    }
 
-    model_canvas.toBuffer(function(err, buf) {
+    // head top
+    x = x_offset;
+    y = -0.5;
+    z = z_offset;
+    ctx.setTransform(1, -skew_a, 1, skew_a, 0, 0);
+    ctx.drawImage(head_top, y - z, x + z, head_top.width, head_top.height + 1);
+
+    // head front
+    x = x_offset + 8 * scale;
+    y = 0;
+    z = z_offset - 0.5;
+    ctx.setTransform(1, -skew_a, 0, skew_b, 0, skew_a);
+    ctx.drawImage(head_front, y + x, x + z, head_front.width, head_front.height);
+
+    // head right
+    x = x_offset;
+    y = 0;
+    z = z_offset;
+    ctx.setTransform(1, skew_a, 0, skew_b, 0, 0);
+    ctx.drawImage(head_right, x + y, z - y - 0.5, head_right.width, head_right.height + 1);
+
+    canvas.toBuffer(function(err, buf) {
+      if (err) {
+        logging.error(rid, "error creating buffer:", err);
+      }
       callback(err, buf);
     });
   };
 
-  image.src = img;
+  skin.src = img;
 };
 
 // helper method to open a render from +renderpath+
 // callback: error, image buffer
 exp.open_render = function(rid, renderpath, callback) {
-  fs.readFile(renderpath, function(err, buf) {
-    callback(err, buf);
-  });
+  fs.readFile(renderpath, callback);
 };
 
-
 module.exports = exp;

+ 16 - 16
lib/response.js

@@ -63,19 +63,15 @@ module.exports = function(request, response, result) {
     headers["X-Storage-Type"] = human_status[result.status];
   }
 
-  if (result.body) {
-    // use Mojang's image hash if available
-    // use crc32 as a hash function otherwise
-    var etag = result.hash && result.hash.substr(0, 10) || crc(result.body);
-    headers.Etag = "\"" + etag + "\"";
+  // use crc32 as a hash function for Etag
+  var etag = "\"" + crc(result.body || "") + "\"";
 
-    // handle etag caching
-    var incoming_etag = request.headers["if-none-match"];
-    if (incoming_etag && incoming_etag === headers.Etag) {
-      response.writeHead(304, headers);
-      response.end();
-      return;
-    }
+  // handle etag caching
+  var incoming_etag = request.headers["if-none-match"];
+  if (incoming_etag && incoming_etag === etag) {
+    response.writeHead(304, headers);
+    response.end();
+    return;
   }
 
   if (result.redirect) {
@@ -87,12 +83,16 @@ module.exports = function(request, response, result) {
 
   if (result.status === -2) {
     response.writeHead(result.code || 422, headers);
-    response.end(result.body);
   } else if (result.status === -1) {
     response.writeHead(500, headers);
-    response.end(result.body);
   } else {
-    response.writeHead(result.body ? 200 : 404, headers);
-    response.end(result.body);
+    if (result.body) {
+      headers.Etag = etag;
+      response.writeHead(200, headers);
+    } else {
+      response.writeHead(404, headers);
+    }
   }
+
+  response.end(result.body);
 };

+ 3 - 2
lib/routes/avatars.js

@@ -7,7 +7,8 @@ var url = require("url");
 
 function handle_default(img_status, userId, size, def, req, err, callback) {
   def = def || skins.default_skin(userId);
-  if (def !== "steve" && def !== "mhf_steve" && def !== "alex" && def !== "mhf_alex") {
+  var defname = def.toLowerCase();
+  if (defname !== "steve" && defname !== "mhf_steve" && defname !== "alex" && defname !== "mhf_alex") {
     if (helpers.id_valid(def)) {
       // clean up the old URL to match new image
       var parsed = req.url;
@@ -29,7 +30,7 @@ function handle_default(img_status, userId, size, def, req, err, callback) {
     }
   } else {
     // handle steve and alex
-    def = def.toLowerCase();
+    def = defname;
     if (def.substr(0, 4) !== "mhf_") {
       def = "mhf_" + def;
     }

+ 6 - 5
lib/routes/renders.js

@@ -12,7 +12,8 @@ var fs = require("fs");
 // overlay is query param
 function handle_default(rid, scale, overlay, body, img_status, userId, size, def, req, err, callback) {
   def = def || skins.default_skin(userId);
-  if (def !== "steve" && def !== "mhf_steve" && def !== "alex" && def !== "mhf_alex") {
+  var defname = def.toLowerCase();
+  if (defname !== "steve" && defname !== "mhf_steve" && defname !== "alex" && defname !== "mhf_alex") {
     if (helpers.id_valid(def)) {
       // clean up the old URL to match new image
       var parsed = req.url;
@@ -34,13 +35,13 @@ function handle_default(rid, scale, overlay, body, img_status, userId, size, def
     }
   } else {
     // handle steve and alex
-    def = def.toLowerCase();
+    def = defname;
     if (def.substr(0, 4) !== "mhf_") {
       def = "mhf_" + def;
     }
-    fs.readFile(path.join(__dirname, "..", "public", "images", def + "_skin.png"), function (fs_err, buf) {
+    fs.readFile(path.join(__dirname, "..", "public", "images", def + "_skin.png"), function(fs_err, buf) {
       // we render the default skins, but not custom images
-      renders.draw_model(rid, buf, scale, overlay, body, function(render_err, def_img) {
+      renders.draw_model(rid, buf, scale, overlay, body, def === "mhf_alex", function(render_err, def_img) {
         callback({
           status: img_status,
           body: def_img,
@@ -55,7 +56,7 @@ function handle_default(rid, scale, overlay, body, img_status, userId, size, def
 
 // GET render request
 module.exports = function(req, callback) {
-  var raw_type = (req.url.path_list[1] || "");
+  var raw_type = req.url.path_list[1] || "";
   var rid = req.id;
   var body = raw_type === "body";
   var userId = (req.url.path_list[2] || "").split(".")[0];

+ 4 - 3
lib/routes/skins.js

@@ -8,7 +8,8 @@ var url = require("url");
 
 function handle_default(img_status, userId, def, req, err, callback) {
   def = def || skins.default_skin(userId);
-  if (def !== "steve" && def !== "mhf_steve" && def !== "alex" && def !== "mhf_alex") {
+  var defname = def.toLowerCase();
+  if (defname !== "steve" && defname !== "mhf_steve" && defname !== "alex" && defname !== "mhf_alex") {
     if (helpers.id_valid(def)) {
       // clean up the old URL to match new image
       var parsed = req.url;
@@ -30,7 +31,7 @@ function handle_default(img_status, userId, def, req, err, callback) {
     }
   } else {
     // handle steve and alex
-    def = def.toLowerCase();
+    def = defname;
     if (def.substr(0, 4) !== "mhf_") {
       def = "mhf_" + def;
     }
@@ -83,7 +84,7 @@ module.exports = function(req, callback) {
   userId = userId.replace(/-/g, "");
 
   try {
-    helpers.get_skin(rid, userId, function(err, hash, status, image) {
+    helpers.get_skin(rid, userId, function(err, hash, status, image, slim) {
       if (err) {
         if (err.code === "ENOENT") {
           // no such file

+ 6 - 2
lib/skins.js

@@ -104,8 +104,12 @@ exp.resize_img = function(inname, size, callback) {
 // returns "mhf_alex" or "mhf_steve" calculated by the +uuid+
 exp.default_skin = function(uuid) {
   if (uuid.length <= 16) {
-    // we can't get the skin type by username
-    return "mhf_steve";
+    if (uuid.toLowerCase() === "mhf_alex") {
+      return uuid;
+    } else {
+      // we can't get the skin type by username
+      return "mhf_steve";
+    }
   } else {
     // great thanks to Minecrell for research into Minecraft and Java's UUID hashing!
     // https://git.io/xJpV

+ 1 - 1
package.json

@@ -34,7 +34,7 @@
     "iojs": "2.0.x"
   },
   "dependencies": {
-    "canvas": "^1.2.9",
+    "canvas": "^1.3.4",
     "crc": "~3.3.0",
     "ejs": "^2.3.4",
     "lwip": "~0.0.7",

+ 151 - 179
test/test.js

@@ -17,7 +17,7 @@ var fs = require("fs");
 config.server.http_timeout *= 3;
 
 // no spam
-if (process.env.VERBOSE_TEST !== "true") {
+if (process.env.VERBOSE_TEST !== "true" && process.env.TRAVIS !== "true") {
   logging.log = logging.debug = logging.warn = logging.error = function() {};
 }
 
@@ -52,7 +52,10 @@ var alex_ids = [
   "fffffff1" + "fffffff1" + "fffffff1" + "fffffff0",
 ];
 
-var rid = "TestReqID: ";
+// generates a 12 character random string
+function rid() {
+  return Math.random().toString(36).substring(2, 14);
+}
 
 function getRandomInt(min, max) {
   return Math.floor(Math.random() * (max - min + 1)) + min;
@@ -120,14 +123,14 @@ describe("Crafatar", function() {
     });
     it("should not exist (uuid)", function(done) {
       var number = getRandomInt(0, 9).toString();
-      networking.get_profile(rid, Array(33).join(number), function(err, profile) {
+      networking.get_profile(rid(), Array(33).join(number), function(err, profile) {
         assert.ifError(err);
         assert.strictEqual(profile, null);
         done();
       });
     });
     it("should not exist (username)", function(done) {
-      networking.get_username_url(rid, "Steve", 0, function(err, profile) {
+      networking.get_username_url(rid(), "Steve", 0, function(err, profile) {
         assert.ifError(err);
         done();
       });
@@ -136,10 +139,10 @@ describe("Crafatar", function() {
   describe("Avatar", function() {
     it("uuid's account should exist, but skin should not", function(done) {
       // profile "Alex" - hoping it'll never have a skin
-      networking.get_profile(rid, "ec561538f3fd461daff5086b22154bce", function(err, profile) {
+      networking.get_profile(rid(), "ec561538f3fd461daff5086b22154bce", function(err, profile) {
         assert.ifError(err);
         assert.notStrictEqual(profile, null);
-        networking.get_uuid_url(profile, 1, function(url) {
+        networking.get_uuid_info(profile, "CAPE", function(url) {
           assert.strictEqual(url, null);
           done();
         });
@@ -172,7 +175,7 @@ describe("Crafatar", function() {
     it("should time out on uuid info download", function(done) {
       var original_timeout = config.server.http_timeout;
       config.server.http_timeout = 1;
-      networking.get_profile(rid, "069a79f444e94726a5befca90e38aaf5", function(err, profile) {
+      networking.get_profile(rid(), "069a79f444e94726a5befca90e38aaf5", function(err, profile) {
         assert.strictEqual(err.code, "ETIMEDOUT");
         config.server.http_timeout = original_timeout;
         done();
@@ -181,7 +184,7 @@ describe("Crafatar", function() {
     it("should time out on username info download", function(done) {
       var original_timeout = config.server.http_timeout;
       config.server.http_timeout = 1;
-      networking.get_username_url(rid, "jomo", 0, function(err, url) {
+      networking.get_username_url(rid(), "jomo", 0, function(err, url) {
         assert.strictEqual(err.code, "ETIMEDOUT");
         config.server.http_timeout = original_timeout;
         done();
@@ -190,7 +193,7 @@ describe("Crafatar", function() {
     it("should time out on skin download", function(done) {
       var original_timeout = config.http_timeout;
       config.server.http_timeout = 1;
-      networking.get_from(rid, "http://textures.minecraft.net/texture/477be35554684c28bdeee4cf11c591d3c88afb77e0b98da893fd7bc318c65184", function(body, res, error) {
+      networking.get_from(rid(), "http://textures.minecraft.net/texture/477be35554684c28bdeee4cf11c591d3c88afb77e0b98da893fd7bc318c65184", function(body, res, error) {
         assert.strictEqual(error.code, "ETIMEDOUT");
         config.server.http_timeout = original_timeout;
         done();
@@ -198,14 +201,14 @@ describe("Crafatar", function() {
     });
     it("should not find the skin", function(done) {
       assert.doesNotThrow(function() {
-        networking.get_from(rid, "http://textures.minecraft.net/texture/this-does-not-exist", function(img, response, err) {
+        networking.get_from(rid(), "http://textures.minecraft.net/texture/this-does-not-exist", function(img, response, err) {
           assert.strictEqual(err, null); // no error here, but it shouldn't throw exceptions
           done();
         });
       });
     });
     it("should not find the file", function(done) {
-      skins.open_skin(rid, "non/existent/path", function(err, img) {
+      skins.open_skin(rid(), "non/existent/path", function(err, img) {
         assert(err);
         done();
       });
@@ -233,7 +236,6 @@ describe("Crafatar", function() {
         assert.ifError(error);
         assert.ifError(body);
         assert.equal(res.statusCode, 304);
-        assert(res.headers.etag);
         assert_headers(res);
         callback();
       });
@@ -338,440 +340,396 @@ describe("Crafatar", function() {
     var server_tests = {
       "avatar with existing username": {
         url: "http://localhost:3000/avatars/jeb_?size=16",
-        etag: '"a846b82963"',
-        crc32: 1623808067
+        crc32: [1623808067]
       },
       "avatar with non-existent username": {
         url: "http://localhost:3000/avatars/0?size=16",
-        etag: '"mhf_steve"',
         crc32: [2416827277, 1243826040]
       },
-      "avatar with non-existent username defaulting to alex": {
+      "avatar with non-existent username defaulting to mhf_alex": {
         url: "http://localhost:3000/avatars/0?size=16&default=mhf_alex",
-        etag: '"mhf_alex"',
         crc32: [862751081, 809395677]
       },
       "avatar with non-existent username defaulting to username": {
         url: "http://localhost:3000/avatars/0?size=16&default=jeb_",
-        crc32: 0,
+        crc32: [0],
         redirect: "/avatars/jeb_?size=16"
       },
       "avatar with non-existent username defaulting to uuid": {
         url: "http://localhost:3000/avatars/0?size=16&default=853c80ef3c3749fdaa49938b674adae6",
-        crc32: 0,
+        crc32: [0],
         redirect: "/avatars/853c80ef3c3749fdaa49938b674adae6?size=16"
       },
       "avatar with non-existent username defaulting to url": {
         url: "http://localhost:3000/avatars/0?size=16&default=http%3A%2F%2Fexample.com%2FCaseSensitive",
-        crc32: 0,
+        crc32: [0],
         redirect: "http://example.com/CaseSensitive"
       },
       "overlay avatar with existing username": {
         url: "http://localhost:3000/avatars/jeb_?size=16&overlay",
-        etag: '"a846b82963"',
-        crc32: 646871998
+        crc32: [646871998]
       },
       "overlay avatar with non-existent username": {
         url: "http://localhost:3000/avatars/0?size=16&overlay",
-        etag: '"mhf_steve"',
         crc32: [2416827277, 1243826040]
       },
-      "overlay avatar with non-existent username defaulting to alex": {
+      "overlay avatar with non-existent username defaulting to mhf_alex": {
         url: "http://localhost:3000/avatars/0?size=16&overlay&default=mhf_alex",
-        etag: '"mhf_alex"',
         crc32: [862751081, 809395677]
       },
       "overlay avatar with non-existent username defaulting to username": {
         url: "http://localhost:3000/avatars/0?size=16&overlay&default=jeb_",
-        crc32: 0,
+        crc32: [0],
         redirect: "/avatars/jeb_?size=16&overlay="
       },
       "overlay avatar with non-existent username defaulting to uuid": {
         url: "http://localhost:3000/avatars/0?size=16&overlay&default=853c80ef3c3749fdaa49938b674adae6",
-        crc32: 0,
+        crc32: [0],
         redirect: "/avatars/853c80ef3c3749fdaa49938b674adae6?size=16&overlay="
       },
       "overlay avatar with non-existent username defaulting to url": {
         url: "http://localhost:3000/avatars/0?size=16&overlay&default=http%3A%2F%2Fexample.com%2FCaseSensitive",
-        crc32: 0,
+        crc32: [0],
         redirect: "http://example.com/CaseSensitive"
       },
       "avatar with existing uuid": {
         url: "http://localhost:3000/avatars/853c80ef3c3749fdaa49938b674adae6?size=16",
-        etag: '"a846b82963"',
-        crc32: 1623808067
+        crc32: [1623808067]
       },
       "avatar with non-existent uuid": {
         url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16",
-        etag: '"mhf_steve"',
         crc32: [2416827277, 1243826040]
       },
-      "avatar with non-existent uuid defaulting to alex": {
+      "avatar with non-existent uuid defaulting to mhf_alex": {
         url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=mhf_alex",
-        etag: '"mhf_alex"',
         crc32: [862751081, 809395677]
       },
       "avatar with non-existent uuid defaulting to username": {
         url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=jeb_",
-        crc32: 0,
+        crc32: [0],
         redirect: "/avatars/jeb_?size=16"
       },
       "avatar with non-existent uuid defaulting to uuid": {
         url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=853c80ef3c3749fdaa49938b674adae6",
-        crc32: 0,
+        crc32: [0],
         redirect: "/avatars/853c80ef3c3749fdaa49938b674adae6?size=16"
       },
       "avatar with non-existent uuid defaulting to url": {
         url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=http%3A%2F%2Fexample.com%2FCaseSensitive",
-        crc32: 0,
+        crc32: [0],
         redirect: "http://example.com/CaseSensitive"
       },
       "overlay avatar with existing uuid": {
         url: "http://localhost:3000/avatars/853c80ef3c3749fdaa49938b674adae6?size=16&overlay",
-        etag: '"a846b82963"',
-        crc32: 646871998
+        crc32: [646871998]
       },
       "overlay avatar with non-existent uuid": {
         url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&overlay",
-        etag: '"mhf_steve"',
         crc32: [2416827277, 1243826040]
       },
-      "overlay avatar with non-existent uuid defaulting to alex": {
+      "overlay avatar with non-existent uuid defaulting to mhf_alex": {
         url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&overlay&default=mhf_alex",
-        etag: '"mhf_alex"',
         crc32: [862751081, 809395677]
       },
       "overlay avatar with non-existent uuid defaulting to username": {
         url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=jeb_",
-        crc32: 0,
+        crc32: [0],
         redirect: "/avatars/jeb_?size=16"
       },
       "overlay avatar with non-existent uuid defaulting to uuid": {
         url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&default=853c80ef3c3749fdaa49938b674adae6",
-        crc32: 0,
+        crc32: [0],
         redirect: "/avatars/853c80ef3c3749fdaa49938b674adae6?size=16"
       },
       "overlay avatar with non-existent uuid defaulting to url": {
         url: "http://localhost:3000/avatars/00000000000000000000000000000000?size=16&overlay&default=http%3A%2F%2Fexample.com%2FCaseSensitive",
-        crc32: 0,
+        crc32: [0],
         redirect: "http://example.com/CaseSensitive"
       },
       "cape with existing username": {
         url: "http://localhost:3000/capes/jeb_",
-        etag: '"3f688e0e69"',
         crc32: [989800403, 1901140141]
       },
       "cape with non-existent username": {
         url: "http://localhost:3000/capes/0",
-        crc32: 0
+        crc32: [0]
       },
       "cape with non-existent username defaulting to url": {
         url: "http://localhost:3000/capes/0?default=http%3A%2F%2Fexample.com%2FCaseSensitive",
-        crc32: 0,
+        crc32: [0],
         redirect: "http://example.com/CaseSensitive"
       },
       "cape with existing uuid": {
         url: "http://localhost:3000/capes/853c80ef3c3749fdaa49938b674adae6",
-        etag: '"3f688e0e69"',
         crc32: [989800403, 1901140141]
       },
       "cape with non-existent uuid": {
         url: "http://localhost:3000/capes/00000000000000000000000000000000",
-        crc32: 0
+        crc32: [0]
       },
       "cape with non-existent uuid defaulting to url": {
         url: "http://localhost:3000/capes/00000000000000000000000000000000?default=http%3A%2F%2Fexample.com%2FCaseSensitive",
-        crc32: 0,
+        crc32: [0],
         redirect: "http://example.com/CaseSensitive"
       },
       "skin with existing username": {
         url: "http://localhost:3000/skins/jeb_",
-        etag: '"a846b82963"',
-        crc32: 26500336
+        crc32: [26500336]
       },
       "skin with non-existent username": {
         url: "http://localhost:3000/skins/0",
-        etag: '"mhf_steve"',
-        crc32: 981937087
+        crc32: [981937087]
       },
-      "skin with non-existent username defaulting to alex": {
+      "skin with non-existent username defaulting to mhf_alex": {
         url: "http://localhost:3000/skins/0?default=mhf_alex",
-        etag: '"mhf_alex"',
-        crc32: 2298915739
+        crc32: [2298915739]
       },
       "skin with non-existent username defaulting to username": {
         url: "http://localhost:3000/skins/0?size=16&default=jeb_",
-        crc32: 0,
+        crc32: [0],
         redirect: "/skins/jeb_?size=16"
       },
       "skin with non-existent username defaulting to uuid": {
         url: "http://localhost:3000/skins/0?size=16&default=853c80ef3c3749fdaa49938b674adae6",
-        crc32: 0,
+        crc32: [0],
         redirect: "/skins/853c80ef3c3749fdaa49938b674adae6?size=16"
       },
       "skin with non-existent username defaulting to url": {
         url: "http://localhost:3000/skins/0?default=http%3A%2F%2Fexample.com%2FCaseSensitive",
-        crc32: 0,
+        crc32: [0],
         redirect: "http://example.com/CaseSensitive"
       },
       "skin with existing uuid": {
         url: "http://localhost:3000/skins/853c80ef3c3749fdaa49938b674adae6",
-        etag: '"a846b82963"',
-        crc32: 26500336
+        crc32: [26500336]
       },
       "skin with non-existent uuid": {
         url: "http://localhost:3000/skins/00000000000000000000000000000000",
-        etag: '"mhf_steve"',
-        crc32: 981937087
+        crc32: [981937087]
       },
-      "skin with non-existent uuid defaulting to alex": {
+      "skin with non-existent uuid defaulting to mhf_alex": {
         url: "http://localhost:3000/skins/00000000000000000000000000000000?default=mhf_alex",
-        etag: '"mhf_alex"',
-        crc32: 2298915739
+        crc32: [2298915739]
       },
       "skin with non-existent uuid defaulting to username": {
         url: "http://localhost:3000/skins/00000000000000000000000000000000?size=16&default=jeb_",
-        crc32: 0,
+        crc32: [0],
         redirect: "/skins/jeb_?size=16"
       },
       "skin with non-existent uuid defaulting to uuid": {
         url: "http://localhost:3000/skins/00000000000000000000000000000000?size=16&default=853c80ef3c3749fdaa49938b674adae6",
-        crc32: 0,
+        crc32: [0],
         redirect: "/skins/853c80ef3c3749fdaa49938b674adae6?size=16"
       },
       "skin with non-existent uuid defaulting to url": {
         url: "http://localhost:3000/skins/00000000000000000000000000000000?default=http%3A%2F%2Fexample.com%2FCaseSensitive",
-        crc32: 0,
+        crc32: [0],
         redirect: "http://example.com/CaseSensitive"
       },
       "head render with existing username": {
         url: "http://localhost:3000/renders/head/jeb_?scale=2",
-        etag: '"a846b82963"',
-        crc32: [1743362302, 208074514, 2506366593]
+        crc32: [3487896679, 3001090792]
       },
       "head render with non-existent username": {
         url: "http://localhost:3000/renders/head/0?scale=2",
-        etag: '"mhf_steve"',
-        crc32: [897270661, 1026982335, 1726107733]
+        crc32: [3257141069, 214248305]
       },
-      "head render with non-existent username defaulting to alex": {
+      "head render with non-existent username defaulting to mhf_alex": {
         url: "http://localhost:3000/renders/head/0?scale=2&default=mhf_alex",
-        etag: '"mhf_alex"',
-        crc32: [2357619670, 3172866498, 2214491831]
+        crc32: [263450586, 3116770561]
       },
       "head render with non-existent username defaulting to username": {
         url: "http://localhost:3000/avatars/0?scale=2&default=jeb_",
-        crc32: 0,
+        crc32: [0],
         redirect: "/avatars/jeb_?scale=2"
       },
       "head render with non-existent username defaulting to uuid": {
         url: "http://localhost:3000/avatars/0?scale=2&default=853c80ef3c3749fdaa49938b674adae6",
-        crc32: 0,
+        crc32: [0],
         redirect: "/avatars/853c80ef3c3749fdaa49938b674adae6?scale=2"
       },
       "head render with non-existent username defaulting to url": {
         url: "http://localhost:3000/renders/head/0?scale=2&default=http%3A%2F%2Fexample.com%2FCaseSensitive",
-        crc32: 0,
+        crc32: [0],
         redirect: "http://example.com/CaseSensitive"
       },
       "overlay head render with existing username": {
         url: "http://localhost:3000/renders/head/jeb_?scale=2&overlay",
-        etag: '"a846b82963"',
-        crc32: [4178514320, 2340078566, 3980890516]
+        crc32: [762377383, 1726474987]
       },
       "overlay head render with non-existent username": {
         url: "http://localhost:3000/renders/head/0?scale=2&overlay",
-        etag: '"mhf_steve"',
-        crc32: [507497693, 3868868707, 7372195]
+        crc32: [3257141069, 214248305]
       },
-      "overlay head render with non-existent username defaulting to alex": {
+      "overlay head render with non-existent username defaulting to mhf_alex": {
         url: "http://localhost:3000/renders/head/0?scale=2&overlay&default=mhf_alex",
-        etag: '"mhf_alex"',
-        crc32: [891113664, 1785326216, 622500655]
+        crc32: [263450586, 3116770561]
       },
       "overlay head render with non-existent username defaulting to username": {
         url: "http://localhost:3000/renders/head/0?scale=2&overlay&default=jeb_",
-        crc32: 0,
+        crc32: [0],
         redirect: "/renders/head/jeb_?scale=2&overlay="
       },
       "overlay head render with non-existent username defaulting to uuid": {
         url: "http://localhost:3000/renders/head/0?scale=2&overlay&default=853c80ef3c3749fdaa49938b674adae6",
-        crc32: 0,
+        crc32: [0],
         redirect: "/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&overlay="
       },
       "overlay head render with non-existent username defaulting to url": {
         url: "http://localhost:3000/renders/head/0?scale=2&overlay&default=http%3A%2F%2Fexample.com%2FCaseSensitive",
-        crc32: 0,
+        crc32: [0],
         redirect: "http://example.com/CaseSensitive"
       },
       "head render with existing uuid": {
         url: "http://localhost:3000/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2",
-        etag: '"a846b82963"',
-        crc32: [1743362302, 208074514, 2506366593]
+        crc32: [3487896679, 3001090792]
       },
       "head render with non-existent uuid": {
         url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2",
-        etag: '"mhf_steve"',
-        crc32: [897270661, 1026982335, 1726107733]
+        crc32: [3257141069, 214248305]
       },
-      "head render with non-existent uuid defaulting to alex": {
+      "head render with non-existent uuid defaulting to mhf_alex": {
         url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=mhf_alex",
-        etag: '"mhf_alex"',
-        crc32: [2357619670, 3172866498, 2214491831]
+        crc32: [263450586, 3116770561]
       },
       "head render with non-existent uuid defaulting to username": {
         url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=jeb_",
-        crc32: 0,
+        crc32: [0],
         redirect: "/renders/head/jeb_?scale=2"
       },
       "head render with non-existent uuid defaulting to uuid": {
         url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=853c80ef3c3749fdaa49938b674adae6",
-        crc32: 0,
+        crc32: [0],
         redirect: "/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2"
       },
       "head render with non-existent uuid defaulting to url": {
         url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=http%3A%2F%2Fexample.com%2FCaseSensitive",
-        crc32: 0,
+        crc32: [0],
         redirect: "http://example.com/CaseSensitive"
       },
       "overlay head render with existing uuid": {
         url: "http://localhost:3000/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&overlay",
-        etag: '"a846b82963"',
-        crc32: [4178514320, 2340078566, 3980890516]
+        crc32: [762377383, 1726474987]
       },
       "overlay head render with non-existent uuid": {
         url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&overlay",
-        etag: '"mhf_steve"',
-        crc32: [507497693, 3868868707, 7372195]
+        crc32: [3257141069, 214248305]
       },
-      "overlay head render with non-existent uuid defaulting to alex": {
+      "overlay head render with non-existent uuid defaulting to mhf_alex": {
         url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&overlay&default=mhf_alex",
-        etag: '"mhf_alex"',
-        crc32: [891113664, 1785326216, 622500655]
+        crc32: [263450586, 3116770561]
       },
       "overlay head with non-existent uuid defaulting to username": {
         url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&overlay&default=jeb_",
-        crc32: 0,
+        crc32: [0],
         redirect: "/renders/head/jeb_?scale=2&overlay="
       },
       "overlay head with non-existent uuid defaulting to uuid": {
         url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&overlay&default=853c80ef3c3749fdaa49938b674adae6",
-        crc32: 0,
+        crc32: [0],
         redirect: "/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&overlay="
       },
       "overlay head render with non-existent uuid defaulting to url": {
         url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&overlay&default=http%3A%2F%2Fexample.com%2FCaseSensitive",
-        crc32: 0,
+        crc32: [0],
         redirect: "http://example.com/CaseSensitive"
       },
       "body render with existing username": {
         url: "http://localhost:3000/renders/body/jeb_?scale=2",
-        etag: '"a846b82963"',
-        crc32: [1023392610, 4127764743, 3884408742]
+        crc32: [3127075871, 2595192206]
       },
       "body render with non-existent username": {
         url: "http://localhost:3000/renders/body/0?scale=2",
-        etag: '"mhf_steve"',
-        crc32: [3559591930, 3663447404, 1521463481]
+        crc32: [1046655221, 1620063267]
       },
-      "body render with non-existent username defaulting to alex": {
+      "body render with non-existent username defaulting to mhf_alex": {
         url: "http://localhost:3000/renders/body/0?scale=2&default=mhf_alex",
-        etag: '"mhf_alex"',
-        crc32: [470529151, 1823026927, 2079926997]
+        crc32: [549240598, 3952648540]
       },
       "body render with non-existent username defaulting to username": {
         url: "http://localhost:3000/renders/body/0?scale=2&default=jeb_",
-        crc32: 0,
+        crc32: [0],
         redirect: "/renders/body/jeb_?scale=2"
       },
       "body render with non-existent username defaulting to uuid": {
         url: "http://localhost:3000/renders/body/0?scale=2&default=853c80ef3c3749fdaa49938b674adae6",
-        crc32: 0,
+        crc32: [0],
         redirect: "/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2"
       },
       "body render with non-existent username defaulting to url": {
         url: "http://localhost:3000/renders/body/0?scale=2&default=http%3A%2F%2Fexample.com%2FCaseSensitive",
-        crc32: 0,
+        crc32: [0],
         redirect: "http://example.com/CaseSensitive"
       },
       "overlay body render with existing username": {
         url: "http://localhost:3000/renders/body/jeb_?scale=2&overlay",
-        etag: '"a846b82963"',
-        crc32: [3476579592, 97705180, 3086172613]
+        crc32: [699892097, 2732138694]
       },
       "overlay body render with non-existent username": {
         url: "http://localhost:3000/renders/body/0?scale=2&overlay",
-        etag: '"mhf_steve"',
-        crc32: [3992841063, 1025743887, 1906839968]
+        crc32: [1046655221, 1620063267]
       },
-      "overlay body render with non-existent username defaulting to alex": {
+      "overlay body render with non-existent username defaulting to mhf_alex": {
         url: "http://localhost:3000/renders/body/0?scale=2&overlay&default=mhf_alex",
-        etag: '"mhf_alex"',
-        crc32: [3317518715, 3621585514, 294661951]
+        crc32: [549240598, 3952648540]
       },
       "overlay body render with non-existent username defaulting to username": {
         url: "http://localhost:3000/renders/body/0?scale=2&overlay&default=jeb_",
-        crc32: 0,
+        crc32: [0],
         redirect: "/renders/body/jeb_?scale=2&overlay="
       },
       "overlay body render with non-existent username defaulting to uuid": {
         url: "http://localhost:3000/renders/body/0?scale=2&overlay&default=853c80ef3c3749fdaa49938b674adae6",
-        crc32: 0,
+        crc32: [0],
         redirect: "/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2&overlay="
       },
       "overlay body render with non-existent username defaulting to url": {
         url: "http://localhost:3000/renders/body/0?scale=2&overlay&default=http%3A%2F%2Fexample.com%2FCaseSensitive",
-        crc32: 0,
+        crc32: [0],
         redirect: "http://example.com/CaseSensitive"
       },
       "body render with existing uuid": {
         url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2",
-        etag: '"a846b82963"',
-        crc32: [1023392610, 4127764743, 3884408742]
+        crc32: [3127075871, 2595192206]
       },
       "body render with non-existent uuid": {
         url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2",
-        etag: '"mhf_steve"',
-        crc32: [3559591930, 3663447404, 1521463481]
+        crc32: [1046655221, 1620063267]
       },
-      "body render with non-existent uuid defaulting to alex": {
+      "body render with non-existent uuid defaulting to mhf_alex": {
         url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&default=mhf_alex",
-        etag: '"mhf_alex"',
-        crc32: [470529151, 1823026927, 2079926997]
+        crc32: [549240598, 3952648540]
       },
       "body render with non-existent uuid defaulting to username": {
         url: "http://localhost:3000/renders/body/0?scale=2&default=jeb_",
-        crc32: 0,
+        crc32: [0],
         redirect: "/renders/body/jeb_?scale=2"
       },
       "body render with non-existent uuid defaulting to uuid": {
         url: "http://localhost:3000/renders/body/0?scale=2&default=853c80ef3c3749fdaa49938b674adae6",
-        crc32: 0,
+        crc32: [0],
         redirect: "/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2"
       },
       "body render with non-existent uuid defaulting to url": {
         url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&default=http%3A%2F%2Fexample.com%2FCaseSensitive",
-        crc32: 0,
+        crc32: [0],
         redirect: "http://example.com/CaseSensitive"
       },
       "overlay body render with existing uuid": {
         url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2&overlay",
-        etag: '"a846b82963"',
-        crc32: [3476579592, 97705180, 3086172613]
+        crc32: [699892097, 2732138694]
       },
       "overlay body render with non-existent uuid": {
         url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&overlay",
-        etag: '"mhf_steve"',
-        crc32: [3992841063, 1025743887, 1906839968]
+        crc32: [1046655221, 1620063267]
       },
-      "overlay body render with non-existent uuid defaulting to alex": {
+      "overlay body render with non-existent uuid defaulting to mhf_alex": {
         url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&overlay&default=mhf_alex",
-        etag: '"mhf_alex"',
-        crc32: [3317518715, 3621585514, 294661951]
+        crc32: [549240598, 3952648540]
       },
       "overlay body render with non-existent uuid defaulting to url": {
         url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&overlay&default=http%3A%2F%2Fexample.com%2FCaseSensitive",
-        crc32: 0,
+        crc32: [0],
         redirect: "http://example.com/CaseSensitive"
       },
     };
@@ -784,32 +742,30 @@ describe("Crafatar", function() {
             assert.ifError(error);
             assert_headers(res);
             assert(res.headers["x-storage-type"]);
-            assert.strictEqual(res.headers.etag, location.etag);
+            var hash = crc(body);
             var matches = false;
-            if (location.crc32 instanceof Array) {
-              for (var i = 0; i < location.crc32.length; i++) {
-                if (location.crc32[i] === crc(body)) {
-                  matches = true;
-                  break;
-                }
+            for (var c = 0; c < location.crc32.length; c++) {
+              if (location.crc32[c] === hash) {
+                matches = true;
+                break;
               }
-            } else {
-              matches = location.crc32 === crc(body);
             }
             try {
-              assert.ok(matches);
+              assert(matches);
             } catch(e) {
-              throw new Error(crc(body) + " != " + location.crc32 + " | " + body.toString("base64"));
+              throw new Error(hash + " != " + location.crc32 + " | " + body.toString("base64"));
             }
             assert.strictEqual(res.headers.location, location.redirect);
-            if (location.etag === undefined) {
+            if (location.crc32[0] === 0) {
               assert.strictEqual(res.statusCode, location.redirect ? 307 : 404);
+              assert.ifError(res.headers.etag); // etag must not be present on non-200
               assert.strictEqual(res.headers["content-type"], "text/plain");
               done();
             } else {
-              assert(res.headers.etag);
               assert.strictEqual(res.headers["content-type"], "image/png");
               assert.strictEqual(res.statusCode, 200);
+              assert(res.headers.etag);
+              assert.strictEqual(res.headers.etag, '"' + hash + '"');
               assert_cache(location.url, res.headers.etag, function() {
                 done();
               });
@@ -819,6 +775,22 @@ describe("Crafatar", function() {
       }(loc));
     }
 
+    it("should update the username skin type on uuid request", function(done) {
+      /*eslint-disable handle-callback-err */
+      request.get("http://localhost:3000/renders/body/mhf_alex", function(error, res, body) {
+        cache.get_details("mhf_alex", function(err, details) {
+          assert.strictEqual(details.slim, false);
+          request.get("http://localhost:3000/renders/body/6ab4317889fd490597f60f67d9d76fd9", function(uerror, ures, ubody) {
+            cache.get_details("mhf_alex", function(cerr, cdetails) {
+              /*eslint-enable handle-callback-err */
+              assert.strictEqual(cdetails.slim, true);
+              done();
+            });
+          });
+        });
+      });
+    });
+
     it("should return a 422 (invalid size)", function(done) {
       var size = config.avatars.max_size + 1;
       request.get("http://localhost:3000/avatars/Jake_0?size=" + size, function(error, res, body) {
@@ -878,13 +850,13 @@ describe("Crafatar", function() {
   // we have to make sure that we test both a 32x64 and 64x64 skin
   describe("Networking: Render", function() {
     it("should not fail (username, 32x64 skin)", function(done) {
-      helpers.get_render(rid, "md_5", 6, true, true, function(err, hash, img) {
+      helpers.get_render(rid(), "md_5", 6, true, true, function(err, hash, img) {
         assert.strictEqual(err, null);
         done();
       });
     });
     it("should not fail (username, 64x64 skin)", function(done) {
-      helpers.get_render(rid, "Jake_0", 6, true, true, function(err, hash, img) {
+      helpers.get_render(rid(), "Jake_0", 6, true, true, function(err, hash, img) {
         assert.strictEqual(err, null);
         done();
       });
@@ -893,7 +865,7 @@ describe("Crafatar", function() {
 
   describe("Networking: Cape", function() {
     it("should not fail (guaranteed cape)", function(done) {
-      helpers.get_cape(rid, "Dinnerbone", function(err, hash, status, img) {
+      helpers.get_cape(rid(), "Dinnerbone", function(err, hash, status, img) {
         assert.strictEqual(err, null);
         done();
       });
@@ -902,13 +874,13 @@ describe("Crafatar", function() {
       before(function() {
         cache.get_redis().flushall();
       });
-      helpers.get_cape(rid, "Dinnerbone", function(err, hash, status, img) {
+      helpers.get_cape(rid(), "Dinnerbone", function(err, hash, status, img) {
         assert.strictEqual(err, null);
         done();
       });
     });
     it("should not be found", function(done) {
-      helpers.get_cape(rid, "Jake_0", function(err, hash, status, img) {
+      helpers.get_cape(rid(), "Jake_0", function(err, hash, status, img) {
         assert.ifError(err);
         assert.strictEqual(img, null);
         done();
@@ -918,7 +890,7 @@ describe("Crafatar", function() {
 
   describe("Networking: Skin", function() {
     it("should not fail", function(done) {
-      helpers.get_cape(rid, "Jake_0", function(err, hash, status, img) {
+      helpers.get_cape(rid(), "Jake_0", function(err, hash, status, img) {
         assert.strictEqual(err, null);
         done();
       });
@@ -927,7 +899,7 @@ describe("Crafatar", function() {
       before(function() {
         cache.get_redis().flushall();
       });
-      helpers.get_cape(rid, "Jake_0", function(err, hash, status, img) {
+      helpers.get_cape(rid(), "Jake_0", function(err, hash, status, img) {
         assert.strictEqual(err, null);
         done();
       });
@@ -948,14 +920,14 @@ describe("Crafatar", function() {
         });
 
         it("should be downloaded", function(done) {
-          helpers.get_avatar(rid, id, false, 160, function(err, status, image) {
+          helpers.get_avatar(rid(), id, false, 160, function(err, status, image) {
             assert.ifError(err);
             assert.strictEqual(status, 2);
             done();
           });
         });
         it("should be cached", function(done) {
-          helpers.get_avatar(rid, id, false, 160, function(err, status, image) {
+          helpers.get_avatar(rid(), id, false, 160, function(err, status, image) {
             assert.ifError(err);
             assert.strictEqual(status === 0 || status === 1, true);
             done();
@@ -967,7 +939,7 @@ describe("Crafatar", function() {
           it("should be checked", function(done) {
             var original_cache_time = config.caching.local;
             config.caching.local = 0;
-            helpers.get_avatar(rid, id, false, 160, function(err, status, image) {
+            helpers.get_avatar(rid(), id, false, 160, function(err, status, image) {
               assert.ifError(err);
               assert.strictEqual(status, 3);
               config.caching.local = original_cache_time;
@@ -979,7 +951,7 @@ describe("Crafatar", function() {
 
       describe("Networking: Skin", function() {
         it("should not fail (uuid)", function(done) {
-          helpers.get_skin(rid, id, function(err, hash, status, img) {
+          helpers.get_skin(rid(), id, function(err, hash, status, img) {
             assert.strictEqual(err, null);
             done();
           });
@@ -988,13 +960,13 @@ describe("Crafatar", function() {
 
       describe("Networking: Render", function() {
         it("should not fail (full body)", function(done) {
-          helpers.get_render(rid, id, 6, true, true, function(err, hash, img) {
+          helpers.get_render(rid(), id, 6, true, true, function(err, hash, img) {
             assert.ifError(err);
             done();
           });
         });
         it("should not fail (only head)", function(done) {
-          helpers.get_render(rid, id, 6, true, false, function(err, hash, img) {
+          helpers.get_render(rid(), id, 6, true, false, function(err, hash, img) {
             assert.ifError(err);
             done();
           });
@@ -1003,7 +975,7 @@ describe("Crafatar", function() {
 
       describe("Networking: Cape", function() {
         it("should not fail (possible cape)", function(done) {
-          helpers.get_cape(rid, id, function(err, hash, status, img) {
+          helpers.get_cape(rid(), id, function(err, hash, status, img) {
             assert.ifError(err);
             done();
           });
@@ -1018,8 +990,8 @@ describe("Crafatar", function() {
 
         if (id_type === "uuid") {
           it("uuid should be rate limited", function(done) {
-            networking.get_profile(rid, id, function() {
-              networking.get_profile(rid, id, function(err, profile) {
+            networking.get_profile(rid(), id, function() {
+              networking.get_profile(rid(), id, function(err, profile) {
                 assert.strictEqual(err.toString(), "HTTP: 429");
                 assert.strictEqual(profile, null);
                 done();
@@ -1028,8 +1000,8 @@ describe("Crafatar", function() {
           });
         } else {
           it("username should NOT be rate limited (username)", function(done) {
-            helpers.get_avatar(rid, id, false, 160, function() {
-              helpers.get_avatar(rid, id, false, 160, function(err, status, image) {
+            helpers.get_avatar(rid(), id, false, 160, function() {
+              helpers.get_avatar(rid(), id, false, 160, function(err, status, image) {
                 assert.strictEqual(err, null);
                 done();
               });