From 6213344090592605992be4d8a6a6fff58987f3c0 Mon Sep 17 00:00:00 2001 From: jomo Date: Thu, 23 Jul 2015 11:13:24 +0200 Subject: [PATCH 01/19] lots of rendering improvements - we don't need to resize images. canvas can do that for us - we don't need to use `scale(-1, 1)` to draw flipped - most of the old/new skin format shares the same code - we can draw the skin image directly on the canvas --- lib/renders.js | 193 ++++++++++++++++++------------------------------- 1 file changed, 70 insertions(+), 123 deletions(-) diff --git a/lib/renders.js b/lib/renders.js index 470006d..dc362bd 100644 --- a/lib/renders.js +++ b/lib/renders.js @@ -8,136 +8,90 @@ 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); - } - } -} - -// 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) { +// draws the helmet on to the +skin+ +// using the skin from the +ctx+ at the +scale+ +exp.draw_helmet = function(skin, 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); + ctx.setTransform(1, -0.5, 0, 1.2, 0, 0); + ctx.drawImage(skin, 40, 8, 8, 8, 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); + ctx.setTransform(1, 0.5, 0, 1.2, 0, 0); + ctx.drawImage(skin, 32, 8, 8, 8, 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); + ctx.setTransform(-1, 0.5, 1, 0.5, 0, 0); + ctx.drawImage(skin, 48, 0, -8, 8, -5 * scale, 5 * scale, 8 * scale, 8 * scale); }; -// 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) { +// draws the head on to the +skin+ +// using the skin from the +ctx+ at the +scale+ +exp.draw_head = function(skin, 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); + ctx.setTransform(1, -0.5, 0, 1.2, 0, 0); + ctx.drawImage(skin, 8, 8, 8, 8, 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); + ctx.setTransform(1, 0.5, 0, 1.2, 0, 0); + ctx.drawImage(skin, 0, 8, 8, 8, 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); + ctx.setTransform(-1, 0.5, 1, 0.5, 0, 0); + ctx.drawImage(skin, 16, 0, -8, 8, -5 * scale, 5 * scale, 8 * scale, 8 * scale); }; -// draws the body on to the +skin_canvas+ -// using the skin from the +model_ctx+ at the +scale+ +// draws the body on to the +skin+ +// using the skin from the +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) { +exp.draw_body = function(rid, skin, ctx, scale) { + // Right Leg + // Right Leg - Right + ctx.setTransform(1, 0.5, 0, 1.2, 0, 0); + ctx.drawImage(skin, 0, 20, 4, 12, 4 * scale, 26.4 / 1.2 * scale, 4 * scale, 12 * scale); + // Right Leg - Front + ctx.setTransform(1, -0.5, 0, 1.2, 0, 0); + ctx.drawImage(skin, 4, 20, 4, 12, 8 * scale, 34.4 / 1.2 * scale, 4 * scale, 12 * scale); + + // Body + // Body - Front + ctx.setTransform(1, -0.5, 0, 1.2, 0, 0); + ctx.drawImage(skin, 20, 20, 8, 12, 8 * scale, 20 / 1.2 * scale, 8 * scale, 12 * scale); + + // Arm Right + // Arm Right - Right + ctx.setTransform(1, 0.5, 0, 1.2, 0, 0); + ctx.drawImage(skin, 40, 20, 4, 12, 0, 16 / 1.2 * scale, 4 * scale, 12 * scale); + // Arm Right - Front + ctx.setTransform(1, -0.5, 0, 1.2, 0, 0); + ctx.drawImage(skin, 44, 20, 4, 12, 4 * scale, 20 / 1.2 * scale, 4 * scale, 12 * scale); + // Arm Right - Top + ctx.setTransform(-1, 0.5, 1, 0.5, 0, 0); + ctx.drawImage(skin, 48, 16, -4, 4, 12 * scale, 16 * scale, 4 * scale, 4 * scale); + + if (skin.height === 32) { 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); + ctx.setTransform(1, -0.5, 0, 1.2, 0, 0); + ctx.drawImage(skin, 8, 20, -4, 12, 12 * 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); + ctx.setTransform(1, -0.5, 0, 1.2, 0, 0); + ctx.drawImage(skin, 48, 20, -4, 12, 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, 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); + ctx.setTransform(-1, 0.5, 1, 0.5, 0, 0); + ctx.drawImage(skin, 44, 16, 4, 4, 0, 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); + ctx.setTransform(1, -0.5, 0, 1.2, 0, 0); + ctx.drawImage(skin, 20, 52, 4, 12, 12 * 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); + ctx.setTransform(1, -0.5, 0, 1.2, 0, 0); + ctx.drawImage(skin, 36, 52, 4, 12, 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); + ctx.setTransform(-1, 0.5, 1, 0.5, 0, 0); + ctx.drawImage(skin, 36, 48, 4, 4, 0, 16 * scale, 4 * scale, 4 * scale); } }; @@ -146,34 +100,27 @@ exp.draw_body = function(rid, skin_canvas, model_ctx, scale) { // the +helm+ and the +body+ // callback: error, image buffer exp.draw_model = function(rid, img, scale, helm, body, callback) { - var image = new Image(); + var skin = new Image(); - image.onerror = function(err) { + skin.onerror = function(err) { logging.error(rid, "render error:", err.stack); callback(err, null); }; - 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.onload = function() { + var canvas = new Canvas(20 * scale, (body ? 44.8 : 17.6) * scale); + var ctx = 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); + ctx.patternQuality = "fast"; if (body) { - exp.draw_body(rid, skin_canvas, model_ctx, scale); + exp.draw_body(rid, skin, ctx, scale); } - exp.draw_head(skin_canvas, model_ctx, scale); + exp.draw_head(skin, ctx, scale); if (helm) { - exp.draw_helmet(skin_canvas, model_ctx, scale); + exp.draw_helmet(skin, ctx, scale); } - model_canvas.toBuffer(function(err, buf){ + canvas.toBuffer(function(err, buf) { if (err) { logging.error(rid, "error creating buffer:", err); } @@ -181,13 +128,13 @@ exp.draw_model = function(rid, img, scale, helm, body, callback) { }); }; - 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) { + fs.readFile(renderpath, function(err, buf) { if (err) { logging.error(rid, "error while opening skin file:", err); } From cb2587249859f52715796c1e3b277b7127e5d713 Mon Sep 17 00:00:00 2001 From: jomo Date: Sat, 5 Sep 2015 23:57:10 +0200 Subject: [PATCH 02/19] rewrite renders - renders are now fully isometric - used position -0.5 and size +1 at some places to fix #32 - does not support overlay yet - does not support left/right arms/legs - does not support slim renders yet - currently only renders full body, not head --- lib/renders.js | 207 +++++++++++++++++++++--------------------- lib/routes/renders.js | 4 +- 2 files changed, 108 insertions(+), 103 deletions(-) diff --git a/lib/renders.js b/lib/renders.js index dc362bd..ca6c8f2 100644 --- a/lib/renders.js +++ b/lib/renders.js @@ -8,117 +8,123 @@ var Canvas = require("canvas"); var Image = Canvas.Image; var exp = {}; -// draws the helmet on to the +skin+ -// using the skin from the +ctx+ at the +scale+ -exp.draw_helmet = function(skin, ctx, scale) { - // Helmet - Front - ctx.setTransform(1, -0.5, 0, 1.2, 0, 0); - ctx.drawImage(skin, 40, 8, 8, 8, 10 * scale, 13 / 1.2 * scale, 8 * scale, 8 * scale); - // Helmet - Right - ctx.setTransform(1, 0.5, 0, 1.2, 0, 0); - ctx.drawImage(skin, 32, 8, 8, 8, 2 * scale, 3 / 1.2 * scale, 8 * scale, 8 * scale); - // Helmet - Top - ctx.setTransform(-1, 0.5, 1, 0.5, 0, 0); - ctx.drawImage(skin, 48, 0, -8, 8, -5 * scale, 5 * scale, 8 * scale, 8 * scale); -}; +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"); -// draws the head on to the +skin+ -// using the skin from the +ctx+ at the +scale+ -exp.draw_head = function(skin, ctx, scale) { - // Head - Front - ctx.setTransform(1, -0.5, 0, 1.2, 0, 0); - ctx.drawImage(skin, 8, 8, 8, 8, 10 * scale, 13 / 1.2 * scale, 8 * scale, 8 * scale); - // Head - Right - ctx.setTransform(1, 0.5, 0, 1.2, 0, 0); - ctx.drawImage(skin, 0, 8, 8, 8, 2 * scale, 3 / 1.2 * scale, 8 * scale, 8 * scale); - // Head - Top - ctx.setTransform(-1, 0.5, 1, 0.5, 0, 0); - ctx.drawImage(skin, 16, 0, -8, 8, -5 * scale, 5 * scale, 8 * scale, 8 * scale); -}; + // don't blur on resize + context.patternQuality = "fast"; -// draws the body on to the +skin+ -// using the skin from the +ctx+ at the +scale+ -// parts are labeled as if drawn from the skin's POV -exp.draw_body = function(rid, skin, ctx, scale) { - // Right Leg - // Right Leg - Right - ctx.setTransform(1, 0.5, 0, 1.2, 0, 0); - ctx.drawImage(skin, 0, 20, 4, 12, 4 * scale, 26.4 / 1.2 * scale, 4 * scale, 12 * scale); - // Right Leg - Front - ctx.setTransform(1, -0.5, 0, 1.2, 0, 0); - ctx.drawImage(skin, 4, 20, 4, 12, 8 * scale, 34.4 / 1.2 * scale, 4 * scale, 12 * scale); + context.drawImage(src, x, y, width, height, 0, 0, width * scale, height * scale); + return dst; +} - // Body - // Body - Front - ctx.setTransform(1, -0.5, 0, 1.2, 0, 0); - ctx.drawImage(skin, 20, 20, 8, 12, 8 * scale, 20 / 1.2 * scale, 8 * scale, 12 * scale); +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; +} - // Arm Right - // Arm Right - Right - ctx.setTransform(1, 0.5, 0, 1.2, 0, 0); - ctx.drawImage(skin, 40, 20, 4, 12, 0, 16 / 1.2 * scale, 4 * scale, 12 * scale); - // Arm Right - Front - ctx.setTransform(1, -0.5, 0, 1.2, 0, 0); - ctx.drawImage(skin, 44, 20, 4, 12, 4 * scale, 20 / 1.2 * scale, 4 * scale, 12 * scale); - // Arm Right - Top - ctx.setTransform(-1, 0.5, 1, 0.5, 0, 0); - ctx.drawImage(skin, 48, 16, -4, 4, 12 * scale, 16 * scale, 4 * scale, 4 * scale); +var skew_a = 26 / 45; // 0.57777777 +var skew_b = skew_a * 2; // 1.15555555 - if (skin.height === 32) { - logging.debug(rid, "uses old skin format"); - // Left Leg - // Left Leg - Front - ctx.setTransform(1, -0.5, 0, 1.2, 0, 0); - ctx.drawImage(skin, 8, 20, -4, 12, 12 * scale, 34.4 / 1.2 * scale, 4 * scale, 12 * scale); +exp.draw_model = function(rid, img, scale, helm, type, callback) { + var canvas = new Canvas(); + canvas.width = scale * 20; + canvas.height = scale * 45.1; - // Arm Left - // Arm Left - Front - ctx.setTransform(1, -0.5, 0, 1.2, 0, 0); - ctx.drawImage(skin, 48, 20, -4, 12, 16 * scale, 20 / 1.2 * scale, 4 * scale, 12 * scale); - // Arm Left - Top - ctx.setTransform(-1, 0.5, 1, 0.5, 0, 0); - ctx.drawImage(skin, 44, 16, 4, 4, 0, 16 * scale, 4 * scale, 4 * scale); - } else { - logging.debug(rid, "uses new skin format"); - // Left Leg - // Left Leg - Front - ctx.setTransform(1, -0.5, 0, 1.2, 0, 0); - ctx.drawImage(skin, 20, 52, 4, 12, 12 * scale, 34.4 / 1.2 * scale, 4 * scale, 12 * scale); + var ctx = canvas.getContext("2d"); - // Arm Left - // Arm Left - Front - ctx.setTransform(1, -0.5, 0, 1.2, 0, 0); - ctx.drawImage(skin, 36, 52, 4, 12, 16 * scale, 20 / 1.2 * scale, 4 * scale, 12 * scale); - // Arm Left - Top - ctx.setTransform(-1, 0.5, 1, 0.5, 0, 0); - ctx.drawImage(skin, 36, 48, 4, 4, 0, 16 * scale, 4 * scale, 4 * scale); - } -}; - -// sets up the necessary components to draw the skin model -// uses the +img+ skin with options of drawing -// the +helm+ and the +body+ -// callback: error, image buffer -exp.draw_model = function(rid, img, scale, helm, body, callback) { var skin = new Image(); - skin.onerror = function(err) { - logging.error(rid, "render error:", err.stack); - callback(err, null); - }; - skin.onload = function() { - var canvas = new Canvas(20 * scale, (body ? 44.8 : 17.6) * scale); - var ctx = canvas.getContext("2d"); + var old_type = skin.height === 32; + var arm_width = 4; - ctx.patternQuality = "fast"; - if (body) { - exp.draw_body(rid, skin, ctx, scale); - } - exp.draw_head(skin, ctx, scale); - if (helm) { - exp.draw_helmet(skin, ctx, scale); - } + var face = getPart(skin, 8, 8, 8, 8, scale); + var head_right = getPart(skin, 0, 8, 8, 8, scale); + var head_top = getPart(skin, 8, 0, 8, 8, scale); + var body = getPart(skin, 20, 20, 8, 12, scale); + var right_arm = getPart(skin, 44, 20, arm_width, 12, scale); + var right_arm_side = getPart(skin, 40, 20, arm_width, 12, scale); + var left_arm = flip(right_arm); // TODO + var right_leg = getPart(skin, 4, 20, 4, 12, scale); + var right_leg_side = getPart(skin, 0, 20, 4, 12, scale); + var left_leg = flip(right_leg); // TODO + var right_shoulder = getPart(skin, 44, 16, 4, 4, scale); + var left_shoulder = flip(right_shoulder); // TODO + + + // 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(right_arm, (4 - arm_width) * scale, 0 * scale, arm_width * scale, 12 * scale); + frontc.drawImage(left_arm, 12 * scale, 0 * scale, arm_width * scale, 12 * scale); + frontc.drawImage(body, 4 * scale, 0 * scale, 8 * scale, 12 * scale); + frontc.drawImage(right_leg, 4 * scale, 12 * scale, 4 * scale, 12 * scale); + frontc.drawImage(left_leg, 8 * scale, 12 * scale, 4 * scale, 12 * scale); + + var x = 0; + var y = 0; + var z = 0; + + var z_offset = scale * 3; + var x_offset = scale * 2; + + // 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); + + x = x_offset + scale * 2; + y = scale * -4; + z = z_offset + scale * 8; + ctx.drawImage(right_shoulder, y - z - 0.5, x + z, right_shoulder.width + 1, right_shoulder.height + 1); + + y = scale * 8; + ctx.drawImage(left_shoulder, y - z, x + z, 4 * scale, 4 * scale + 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(right_leg_side, x + y, z - y, right_leg_side.width, right_leg_side.height); + + x = x_offset + scale * 2; + y = scale * -4; + z = z_offset + scale * 8; + ctx.drawImage(right_arm_side, x + y, z - y - 0.5, right_arm_side.width, right_arm_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); + + x = x_offset + 8 * scale; + y = 0; + z = z_offset - 0.5; + ctx.drawImage(face, y + x, x + z, face.width, face.height); + + // right head + 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) { @@ -142,5 +148,4 @@ exp.open_render = function(rid, renderpath, callback) { }); }; - module.exports = exp; \ No newline at end of file diff --git a/lib/routes/renders.js b/lib/routes/renders.js index aa3288d..65b2214 100644 --- a/lib/routes/renders.js +++ b/lib/routes/renders.js @@ -34,7 +34,7 @@ function handle_default(rid, scale, helm, body, img_status, userId, size, def, r } } else { // handle steve and alex - 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, helm, body, function(render_err, def_img) { callback({ @@ -51,7 +51,7 @@ function handle_default(rid, scale, helm, body, img_status, userId, size, def, r // 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]; From 82727cb24d0cc2824bb7a3cd89afd903a50be5fa Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 22 Nov 2015 03:28:09 +0100 Subject: [PATCH 03/19] add back head support --- lib/renders.js | 93 ++++++++++++++++++++++++++------------------------ 1 file changed, 49 insertions(+), 44 deletions(-) diff --git a/lib/renders.js b/lib/renders.js index ca6c8f2..8e6df1c 100644 --- a/lib/renders.js +++ b/lib/renders.js @@ -34,10 +34,10 @@ function flip(src) { var skew_a = 26 / 45; // 0.57777777 var skew_b = skew_a * 2; // 1.15555555 -exp.draw_model = function(rid, img, scale, helm, type, callback) { +exp.draw_model = function(rid, img, scale, helm, is_body, callback) { var canvas = new Canvas(); canvas.width = scale * 20; - canvas.height = scale * 45.1; + canvas.height = scale * (is_body ? 45.1 : 18.5); var ctx = canvas.getContext("2d"); @@ -60,20 +60,6 @@ exp.draw_model = function(rid, img, scale, helm, type, callback) { var right_shoulder = getPart(skin, 44, 16, 4, 4, scale); var left_shoulder = flip(right_shoulder); // TODO - - // 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(right_arm, (4 - arm_width) * scale, 0 * scale, arm_width * scale, 12 * scale); - frontc.drawImage(left_arm, 12 * scale, 0 * scale, arm_width * scale, 12 * scale); - frontc.drawImage(body, 4 * scale, 0 * scale, 8 * scale, 12 * scale); - frontc.drawImage(right_leg, 4 * scale, 12 * scale, 4 * scale, 12 * scale); - frontc.drawImage(left_leg, 8 * scale, 12 * scale, 4 * scale, 12 * scale); - var x = 0; var y = 0; var z = 0; @@ -81,45 +67,64 @@ exp.draw_model = function(rid, img, scale, helm, type, callback) { var z_offset = scale * 3; var x_offset = scale * 2; - // top + 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(right_arm, (4 - arm_width) * scale, 0 * scale, arm_width * scale, 12 * scale); + frontc.drawImage(left_arm, 12 * scale, 0 * scale, arm_width * scale, 12 * scale); + frontc.drawImage(body, 4 * scale, 0 * scale, 8 * scale, 12 * scale); + frontc.drawImage(right_leg, 4 * scale, 12 * scale, 4 * scale, 12 * scale); + frontc.drawImage(left_leg, 8 * scale, 12 * scale, 4 * scale, 12 * scale); + + // top + x = x_offset + scale * 2; + y = scale * -4; + z = z_offset + scale * 8; + ctx.setTransform(1, -skew_a, 1, skew_a, 0, 0); + ctx.drawImage(right_shoulder, y - z - 0.5, x + z, right_shoulder.width + 1, right_shoulder.height + 1); + + y = scale * 8; + ctx.drawImage(left_shoulder, y - z, x + z, 4 * scale, 4 * scale + 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(right_leg_side, x + y, z - y, right_leg_side.width, right_leg_side.height); + + x = x_offset + scale * 2; + y = scale * -4; + z = z_offset + scale * 8; + ctx.drawImage(right_arm_side, x + y, z - y - 0.5, right_arm_side.width, right_arm_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); + } + + // 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); - x = x_offset + scale * 2; - y = scale * -4; - z = z_offset + scale * 8; - ctx.drawImage(right_shoulder, y - z - 0.5, x + z, right_shoulder.width + 1, right_shoulder.height + 1); - - y = scale * 8; - ctx.drawImage(left_shoulder, y - z, x + z, 4 * scale, 4 * scale + 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(right_leg_side, x + y, z - y, right_leg_side.width, right_leg_side.height); - - x = x_offset + scale * 2; - y = scale * -4; - z = z_offset + scale * 8; - ctx.drawImage(right_arm_side, x + y, z - y - 0.5, right_arm_side.width, right_arm_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); - + // 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(face, y + x, x + z, face.width, face.height); - // right head + // head right x = x_offset; y = 0; z = z_offset; From abdacdc713a076f381f1cbb9b3c001c212f018e6 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 22 Nov 2015 03:52:03 +0100 Subject: [PATCH 04/19] fix rendering of slim arms --- lib/helpers.js | 385 ------------------------------------------------- lib/renders.js | 9 +- 2 files changed, 4 insertions(+), 390 deletions(-) delete mode 100644 lib/helpers.js diff --git a/lib/helpers.js b/lib/helpers.js deleted file mode 100644 index 549e35a..0000000 --- a/lib/helpers.js +++ /dev/null @@ -1,385 +0,0 @@ -var networking = require("./networking"); -var logging = require("./logging"); -var renders = require("./renders"); -var config = require("../config"); -var cache = require("./cache"); -var skins = require("./skins"); -var path = require("path"); -var fs = require("fs"); - -// 0098cb60-fa8e-427c-b299-793cbd302c9a -var valid_user_id = /^([0-9a-f-A-F-]{32,36}|[a-zA-Z0-9_]{1,16})$/; // uuid|username -var hash_pattern = /[0-9a-f]+$/; - -// gets the hash from the textures.minecraft.net +url+ -function get_hash(url) { - return hash_pattern.exec(url)[0].toLowerCase(); -} - -// 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 -function store_skin(rid, userId, profile, cache_details, callback) { - networking.get_skin_url(rid, userId, profile, function(err, url) { - if (!err && url) { - var skin_hash = get_hash(url); - if (cache_details && cache_details.skin === skin_hash) { - cache.update_timestamp(rid, userId, skin_hash, false, function(cache_err) { - callback(cache_err, skin_hash); - }); - } else { - logging.debug(rid, "new skin hash:", skin_hash); - var facepath = path.join(__dirname, "..", config.directories.faces, skin_hash + ".png"); - var helmpath = path.join(__dirname, "..", config.directories.helms, skin_hash + ".png"); - var skinpath = path.join(__dirname, "..", config.directories.skins, skin_hash + ".png"); - fs.exists(facepath, function(exists) { - if (exists) { - logging.debug(rid, "skin already exists, not downloading"); - callback(null, skin_hash); - } else { - networking.get_from(rid, url, function(img, response, err1) { - if (err1 || !img) { - callback(err1, null); - } else { - skins.save_image(img, skinpath, function(skin_err) { - if (skin_err) { - logging.error(rid, skin_err); - callback(skin_err, null); - } else { - skins.extract_face(img, facepath, function(err2) { - if (err2) { - logging.error(rid, err2.stack); - callback(err2, null); - } 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); - }); - } - }); - } - }); - } - }); - } - }); - } - } else { - callback(err, null); - } - }); -} - -// gets the cape for +userId+ with +profile+ -// uses +cache_details+ to determine if the cape needs to be downloaded or can be taken from cache -// the cape - if downloaded - is stored to file -// callback: error, cape hash -function store_cape(rid, userId, profile, cache_details, callback) { - networking.get_cape_url(rid, userId, profile, function(err, url) { - if (!err && url) { - var cape_hash = get_hash(url); - if (cache_details && cache_details.cape === cape_hash) { - cache.update_timestamp(rid, userId, cape_hash, false, function(cache_err) { - callback(cache_err, cape_hash); - }); - } else { - logging.debug(rid, "new cape hash:", cape_hash); - var capepath = path.join(__dirname, "..", config.directories.capes, cape_hash + ".png"); - fs.exists(capepath, function(exists) { - if (exists) { - logging.debug(rid, "cape already exists, not downloading"); - callback(null, cape_hash); - } else { - networking.get_from(rid, url, function(img, response, net_err) { - if (net_err || !img) { - logging.error(rid, net_err.stack); - callback(net_err, null); - } else { - skins.save_image(img, capepath, function(skin_err) { - logging.debug(rid, "cape saved"); - callback(skin_err, cape_hash); - }); - } - }); - } - }); - } - } else { - callback(err, null); - } - }); -} - -// used by store_images to queue simultaneous requests for identical userId -// the first request has to be completed until all others are continued -// otherwise we risk running into Mojang's rate limit and deleting the cached skin -var requests = { - skin: {}, - cape: {} -}; - -function push_request(userId, type, fun) { - if (!requests[type][userId]) { - requests[type][userId] = []; - } - requests[type][userId].push(fun); -} - -// calls back all queued requests that match userId and type -function resume(userId, type, err, hash) { - var callbacks = requests[type][userId]; - if (callbacks) { - if (callbacks.length > 1) { - logging.debug(callbacks.length, "simultaneous requests for", userId); - } - - for (var i = 0; i < callbacks.length; i++) { - // continue the request - callbacks[i](err, hash); - // remove from array - callbacks.splice(i, 1); - i--; - } - - // it's still an empty array - delete requests[type][userId]; - } -} - -// 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 -function store_images(rid, userId, cache_details, type, callback) { - var is_uuid = userId.length > 16; - if (requests[type][userId]) { - logging.debug(rid, "adding to request queue"); - push_request(userId, type, callback); - } else { - // add request to the queue - push_request(userId, type, callback); - - networking.get_profile(rid, (is_uuid ? userId : null), function(err, profile) { - if (err || (is_uuid && !profile)) { - // error or uuid without profile - if (!err && !profile) { - // no error, but uuid without profile - cache.save_hash(rid, userId, null, null, 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); - }); - } else { - // an error occured, not caching. we can try in 60 seconds - resume(userId, type, err, null); - } - } 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) { - if (store_err && !skin_hash) { - // an error occured, not caching. we can try in 60 seconds - resume(userId, "skin", store_err, null); - } else { - cache.save_hash(rid, userId, skin_hash, undefined, function(cache_err) { - resume(userId, "skin", (store_err || cache_err), skin_hash); - }); - } - }); - 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); - } else { - cache.save_hash(rid, userId, undefined, cape_hash, function(cache_err) { - resume(userId, "cape", (store_err || cache_err), cape_hash); - }); - } - }); - } - }); - } -} - -var exp = {}; - -// returns true if the +userId+ is a valid userId or username -// the userId may be not exist, however -exp.id_valid = function(userId) { - return valid_user_id.test(userId); -}; - -// decides whether to get a +type+ image for +userId+ from disk or to download it -// callback: error, status, hash -// for status, see response.js -exp.get_image_hash = function(rid, userId, type, callback) { - cache.get_details(userId, function(err, cache_details) { - var cached_hash = null; - if (cache_details !== null) { - cached_hash = type === "skin" ? cache_details.skin : cache_details.cape; - } - if (err) { - callback(err, -1, null); - } 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); - } else { - // download image - if (cache_details) { - logging.debug(rid, "userId cached, but too old"); - } else { - logging.debug(rid, "userId not cached"); - } - store_images(rid, userId, cache_details, type, function(store_err, new_hash) { - 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, cached_hash, true, function(err2) { - callback(err2 || store_err, -1, cache_details && cached_hash); - }); - } 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); - } - }); - } - } - }); -}; - - -// handles requests for +userId+ avatars with +size+ -// callback: error, status, image buffer, skin hash -// image is the user's face+helm when helm is true, or the face otherwise -// for status, see get_image_hash -exp.get_avatar = function(rid, userId, helm, size, callback) { - exp.get_image_hash(rid, userId, "skin", function(err, status, skin_hash) { - if (skin_hash) { - var facepath = path.join(__dirname, "..", config.directories.faces, skin_hash + ".png"); - var helmpath = path.join(__dirname, "..", config.directories.helms, skin_hash + ".png"); - var filepath = facepath; - fs.exists(helmpath, function(exists) { - if (helm && exists) { - filepath = helmpath; - } - skins.resize_img(filepath, size, function(img_err, image) { - if (img_err) { - callback(img_err, -1, null, skin_hash); - } else { - callback(err, (err ? -1 : status), image, skin_hash); - } - }); - }); - } else { - // hash is null when userId has no skin - callback(err, status, null, null); - } - }); -}; - -// handles requests for +userId+ skins -// callback: error, skin hash, status, image buffer -exp.get_skin = function(rid, userId, callback) { - exp.get_image_hash(rid, userId, "skin", function(err, status, skin_hash) { - if (skin_hash) { - var skinpath = path.join(__dirname, "..", 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); - }); - } else { - networking.save_texture(rid, skin_hash, skinpath, function(net_err, response, img) { - callback(net_err || err, skin_hash, status, img); - }); - } - }); - } else { - callback(err, null, status, null); - } - }); -}; - -// helper method used for file names -// possible returned names based on +helm+ and +body+ are: -// body, bodyhelm, head, headhelm -function get_type(helm, body) { - var text = body ? "body" : "head"; - return helm ? text + "helm" : text; -} - -// handles creations of 3D renders -// callback: error, skin hash, image buffer -exp.get_render = function(rid, userId, scale, helm, body, callback) { - exp.get_skin(rid, userId, function(err, skin_hash, status, img) { - if (!skin_hash) { - callback(err, status, skin_hash, null); - return; - } - var renderpath = path.join(__dirname, "..", config.directories.renders, [skin_hash, scale, get_type(helm, body)].join("-") + ".png"); - fs.exists(renderpath, function(exists) { - if (exists) { - renders.open_render(rid, renderpath, function(render_err, rendered_img) { - callback(render_err, 1, skin_hash, rendered_img); - }); - return; - } else { - if (!img) { - callback(err, 0, skin_hash, null); - return; - } - renders.draw_model(rid, img, scale, helm, body, function(draw_err, drawn_img) { - if (draw_err) { - callback(draw_err, -1, skin_hash, null); - } else if (!drawn_img) { - callback(null, 0, skin_hash, null); - } else { - fs.writeFile(renderpath, drawn_img, "binary", function(fs_err) { - if (fs_err) { - logging.error(rid, fs_err.stack); - } - callback(null, 2, skin_hash, drawn_img); - }); - } - }); - } - }); - }); -}; - -// 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) { - if (!cape_hash) { - callback(err, null, status, null); - return; - } - var capepath = path.join(__dirname, "..", config.directories.capes, cape_hash + ".png"); - fs.exists(capepath, function(exists) { - if (exists) { - logging.debug(rid, "cape already exists, not downloading"); - skins.open_skin(rid, capepath, function(skin_err, img) { - callback(skin_err || err, cape_hash, status, img); - }); - } else { - networking.save_texture(rid, cape_hash, capepath, function(net_err, response, img) { - if (response && response.statusCode === 404) { - callback(net_err, cape_hash, status, null); - } else { - callback(net_err, cape_hash, status, img); - } - }); - } - }); - }); -}; - -module.exports = exp; \ No newline at end of file diff --git a/lib/renders.js b/lib/renders.js index 8e6df1c..93c4053 100644 --- a/lib/renders.js +++ b/lib/renders.js @@ -40,7 +40,6 @@ exp.draw_model = function(rid, img, scale, helm, is_body, callback) { canvas.height = scale * (is_body ? 45.1 : 18.5); var ctx = canvas.getContext("2d"); - var skin = new Image(); skin.onload = function() { @@ -52,7 +51,7 @@ exp.draw_model = function(rid, img, scale, helm, is_body, callback) { var head_top = getPart(skin, 8, 0, 8, 8, scale); var body = getPart(skin, 20, 20, 8, 12, scale); var right_arm = getPart(skin, 44, 20, arm_width, 12, scale); - var right_arm_side = getPart(skin, 40, 20, arm_width, 12, scale); + var right_arm_side = getPart(skin, 40, 20, 4, 12, scale); var left_arm = flip(right_arm); // TODO var right_leg = getPart(skin, 4, 20, 4, 12, scale); var right_leg_side = getPart(skin, 0, 20, 4, 12, scale); @@ -83,12 +82,12 @@ exp.draw_model = function(rid, img, scale, helm, is_body, callback) { // top x = x_offset + scale * 2; - y = scale * -4; + y = scale * -arm_width; z = z_offset + scale * 8; ctx.setTransform(1, -skew_a, 1, skew_a, 0, 0); ctx.drawImage(right_shoulder, y - z - 0.5, x + z, right_shoulder.width + 1, right_shoulder.height + 1); - y = scale * 8; + y = scale * (4 + arm_width); ctx.drawImage(left_shoulder, y - z, x + z, 4 * scale, 4 * scale + 1); // right side @@ -99,7 +98,7 @@ exp.draw_model = function(rid, img, scale, helm, is_body, callback) { ctx.drawImage(right_leg_side, x + y, z - y, right_leg_side.width, right_leg_side.height); x = x_offset + scale * 2; - y = scale * -4; + y = scale * -arm_width; z = z_offset + scale * 8; ctx.drawImage(right_arm_side, x + y, z - y - 0.5, right_arm_side.width, right_arm_side.height + 1); From 25c4912db92e1e1b0f1e71b13eaaa65bf6ba46c4 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 22 Nov 2015 05:05:47 +0100 Subject: [PATCH 05/19] add back new (1.8) skin support --- lib/renders.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/renders.js b/lib/renders.js index 93c4053..5759156 100644 --- a/lib/renders.js +++ b/lib/renders.js @@ -52,12 +52,21 @@ exp.draw_model = function(rid, img, scale, helm, is_body, callback) { var body = getPart(skin, 20, 20, 8, 12, scale); var right_arm = getPart(skin, 44, 20, arm_width, 12, scale); var right_arm_side = getPart(skin, 40, 20, 4, 12, scale); - var left_arm = flip(right_arm); // TODO var right_leg = getPart(skin, 4, 20, 4, 12, scale); var right_leg_side = getPart(skin, 0, 20, 4, 12, scale); - var left_leg = flip(right_leg); // TODO - var right_shoulder = getPart(skin, 44, 16, 4, 4, scale); - var left_shoulder = flip(right_shoulder); // TODO + var right_shoulder = getPart(skin, 44, 16, arm_width, 4, scale); + var left_leg; + var left_arm; + var left_shoulder; + if (old_type) { + left_arm = flip(right_arm); + left_leg = flip(right_leg); + left_shoulder = flip(right_shoulder); + } else { + left_arm = getPart(skin, 36, 52, arm_width, 12, scale); + left_leg = getPart(skin, 20, 52, 4, 12, scale); + left_shoulder = getPart(skin, 36, 48, arm_width, 4, scale); + } var x = 0; var y = 0; From 4627aecd1707f996531b7dea6b858b364863b14b Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 22 Nov 2015 05:33:48 +0100 Subject: [PATCH 06/19] clean up uses 'parts' object instead of tons of variables --- lib/renders.js | 86 +++++++++++++++++++++++++++++++------------------- 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/lib/renders.js b/lib/renders.js index 5759156..9aa5cce 100644 --- a/lib/renders.js +++ b/lib/renders.js @@ -43,29 +43,49 @@ exp.draw_model = function(rid, img, scale, helm, is_body, callback) { var skin = new Image(); skin.onload = function() { - var old_type = skin.height === 32; var arm_width = 4; - var face = getPart(skin, 8, 8, 8, 8, scale); - var head_right = getPart(skin, 0, 8, 8, 8, scale); - var head_top = getPart(skin, 8, 0, 8, 8, scale); - var body = getPart(skin, 20, 20, 8, 12, scale); - var right_arm = getPart(skin, 44, 20, arm_width, 12, scale); - var right_arm_side = getPart(skin, 40, 20, 4, 12, scale); - var right_leg = getPart(skin, 4, 20, 4, 12, scale); - var right_leg_side = getPart(skin, 0, 20, 4, 12, scale); - var right_shoulder = getPart(skin, 44, 16, arm_width, 4, scale); - var left_leg; - var left_arm; - var left_shoulder; - if (old_type) { - left_arm = flip(right_arm); - left_leg = flip(right_leg); - left_shoulder = flip(right_shoulder); + var parts = { + head: { + front: getPart(skin, 8, 8, 8, 8, scale), + right: getPart(skin, 0, 8, 8, 8, scale), + top: getPart(skin, 8, 0, 8, 8, scale), + }, + arm: { + right: { + front: getPart(skin, 44, 20, arm_width, 12, scale), + side: getPart(skin, 40, 20, 4, 12, scale), + }, + left: { + front: null, + }, + }, + leg: { + right: { + front: getPart(skin, 4, 20, 4, 12, scale), + side: getPart(skin, 0, 20, 4, 12, scale), + }, + left: { + front: null, + } + }, + shoulder: { + right: getPart(skin, 44, 16, arm_width, 4, scale), + left: null, + }, + body: getPart(skin, 20, 20, 8, 12, scale), + }; + + if (skin.height === 32) { + // old skin + parts.arm.left.front = flip(parts.arm.right.front); + parts.leg.left.front = flip(parts.leg.right.front); + parts.shoulder.left = flip(parts.shoulder.right); } else { - left_arm = getPart(skin, 36, 52, arm_width, 12, scale); - left_leg = getPart(skin, 20, 52, 4, 12, scale); - left_shoulder = getPart(skin, 36, 48, arm_width, 4, scale); + // 1.8 skin - has separate left/right arms & legs + parts.arm.left.front = getPart(skin, 36, 52, arm_width, 12, scale); + parts.leg.left.front = getPart(skin, 20, 52, 4, 12, scale); + parts.shoulder.left = getPart(skin, 36, 48, arm_width, 4, scale); } var x = 0; @@ -83,33 +103,33 @@ exp.draw_model = function(rid, img, scale, helm, is_body, callback) { var frontc = front.getContext("2d"); frontc.patternQuality = "fast"; - frontc.drawImage(right_arm, (4 - arm_width) * scale, 0 * scale, arm_width * scale, 12 * scale); - frontc.drawImage(left_arm, 12 * scale, 0 * scale, arm_width * scale, 12 * scale); - frontc.drawImage(body, 4 * scale, 0 * scale, 8 * scale, 12 * scale); - frontc.drawImage(right_leg, 4 * scale, 12 * scale, 4 * scale, 12 * scale); - frontc.drawImage(left_leg, 8 * scale, 12 * scale, 4 * scale, 12 * scale); + frontc.drawImage(parts.arm.right.front, (4 - arm_width) * scale, 0 * scale, arm_width * scale, 12 * scale); + frontc.drawImage(parts.arm.left.front, 12 * scale, 0 * scale, arm_width * scale, 12 * scale); + frontc.drawImage(parts.body, 4 * scale, 0 * scale, 8 * scale, 12 * scale); + frontc.drawImage(parts.leg.right.front, 4 * scale, 12 * scale, 4 * scale, 12 * scale); + frontc.drawImage(parts.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(right_shoulder, y - z - 0.5, x + z, right_shoulder.width + 1, right_shoulder.height + 1); + ctx.drawImage(parts.shoulder.right, y - z - 0.5, x + z, parts.shoulder.right.width + 1, parts.shoulder.right.height + 1); - y = scale * (4 + arm_width); - ctx.drawImage(left_shoulder, y - z, x + z, 4 * scale, 4 * scale + 1); + y = scale * 8; + ctx.drawImage(parts.shoulder.left, y - z, x + z, parts.shoulder.left.width, parts.shoulder.left.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(right_leg_side, x + y, z - y, right_leg_side.width, right_leg_side.height); + ctx.drawImage(parts.leg.right.side, x + y, z - y, parts.leg.right.side.width, parts.leg.right.side.height); x = x_offset + scale * 2; y = scale * -arm_width; z = z_offset + scale * 8; - ctx.drawImage(right_arm_side, x + y, z - y - 0.5, right_arm_side.width, right_arm_side.height + 1); + ctx.drawImage(parts.arm.right.side, x + y, z - y - 0.5, parts.arm.right.side.width, parts.arm.right.side.height + 1); // front z = z_offset + scale * 12; @@ -123,21 +143,21 @@ exp.draw_model = function(rid, img, scale, helm, is_body, callback) { 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); + ctx.drawImage(parts.head.top, y - z, x + z, parts.head.top.width, parts.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(face, y + x, x + z, face.width, face.height); + ctx.drawImage(parts.head.front, y + x, x + z, parts.head.front.width, parts.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); + ctx.drawImage(parts.head.right, x + y, z - y - 0.5, parts.head.right.width, parts.head.right.height + 1); canvas.toBuffer(function(err, buf) { if (err) { From 67f317aeac0a2d8ca7b2ccef51a49357102da5b1 Mon Sep 17 00:00:00 2001 From: jomo Date: Mon, 23 Nov 2015 03:21:58 +0100 Subject: [PATCH 07/19] add overlay 'logic', deciding which overlays to render; tweak position & size +/- to get around anti-aliasing 'cracks'; remove transparency from base model fixes #32 fixes #112 fixes #117 fixes #153 --- lib/renders.js | 178 ++++++++++++++++++++++++++++++++++++++++++++----- package.json | 2 +- 2 files changed, 164 insertions(+), 16 deletions(-) diff --git a/lib/renders.js b/lib/renders.js index 9aa5cce..60540a6 100644 --- a/lib/renders.js +++ b/lib/renders.js @@ -8,6 +8,49 @@ var Canvas = require("canvas"); var Image = Canvas.Image; var exp = {}; +// 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; +} + +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; + } + } + return false; +} + +function resize(src, scale) { + var dst = new Canvas(); + dst.width = scale * src.width; + dst.height = scale * src.height; + var context = dst.getContext("2d"); + + // don't blur on resize + context.patternQuality = "fast"; + + context.drawImage(src, 0, 0, src.width * scale, src.height * scale); + return dst; +} + function getPart(src, x, y, width, height, scale) { var dst = new Canvas(); dst.width = scale * width; @@ -34,7 +77,7 @@ function flip(src) { var skew_a = 26 / 45; // 0.57777777 var skew_b = skew_a * 2; // 1.15555555 -exp.draw_model = function(rid, img, scale, helm, is_body, callback) { +exp.draw_model = function(rid, img, scale, overlay, is_body, callback) { var canvas = new Canvas(); canvas.width = scale * 20; canvas.height = scale * (is_body ? 45.1 : 18.5); @@ -43,18 +86,19 @@ exp.draw_model = function(rid, img, scale, helm, is_body, callback) { var skin = new Image(); skin.onload = function() { + var old_skin = skin.height === 32; var arm_width = 4; var parts = { head: { - front: getPart(skin, 8, 8, 8, 8, scale), - right: getPart(skin, 0, 8, 8, 8, scale), - top: getPart(skin, 8, 0, 8, 8, scale), + front: resize(removeTransparency(getPart(skin, 8, 8, 8, 8, 1)), scale), + right: resize(removeTransparency(getPart(skin, 0, 8, 8, 8, 1)), scale), + top: resize(removeTransparency(getPart(skin, 8, 0, 8, 8, 1)), scale), }, arm: { right: { - front: getPart(skin, 44, 20, arm_width, 12, scale), - side: getPart(skin, 40, 20, 4, 12, scale), + front: resize(removeTransparency(getPart(skin, 44, 20, arm_width, 12, 1)), scale), + side: resize(removeTransparency(getPart(skin, 40, 20, 4, 12, 1)), scale), }, left: { front: null, @@ -62,30 +106,75 @@ exp.draw_model = function(rid, img, scale, helm, is_body, callback) { }, leg: { right: { - front: getPart(skin, 4, 20, 4, 12, scale), - side: getPart(skin, 0, 20, 4, 12, scale), + front: resize(removeTransparency(getPart(skin, 4, 20, 4, 12, 1)), scale), + side: resize(removeTransparency(getPart(skin, 0, 20, 4, 12, 1)), scale), }, left: { front: null, } }, shoulder: { - right: getPart(skin, 44, 16, arm_width, 4, scale), + right: resize(removeTransparency(getPart(skin, 44, 16, arm_width, 4, 1)), scale), left: null, }, - body: getPart(skin, 20, 20, 8, 12, scale), + body: resize(removeTransparency(getPart(skin, 20, 20, 8, 12, 1)), scale), + }; + var overlays = { + head: {}, + arm: {right: {}, left: {}}, + leg: {right: {}, left: {}}, + shoulder: {}, + body: {}, }; - if (skin.height === 32) { - // old skin + // overlays + var render_head = overlay && hasTransparency(getPart(skin, 32, 0, 32, 32, 1)); + var render_body; + var render_rleg; + var render_lleg; + var render_larm; + var render_rarm; + + // head overlay is shifted 32px right + overlays.head.front = overlay && render_head && getPart(skin, 8 + 32, 8, 8, 8, scale); + overlays.head.right = overlay && render_head && getPart(skin, 0 + 32, 8, 8, 8, scale); + overlays.head.top = overlay && render_head && getPart(skin, 8 + 32, 0, 8, 8, scale); + + if ( old_skin) { parts.arm.left.front = flip(parts.arm.right.front); parts.leg.left.front = flip(parts.leg.right.front); parts.shoulder.left = flip(parts.shoulder.right); } else { // 1.8 skin - has separate left/right arms & legs - parts.arm.left.front = getPart(skin, 36, 52, arm_width, 12, scale); - parts.leg.left.front = getPart(skin, 20, 52, 4, 12, scale); - parts.shoulder.left = getPart(skin, 36, 48, arm_width, 4, scale); + parts.arm.left.front = resize(removeTransparency(getPart(skin, 36, 52, arm_width, 12, 1)), scale); + parts.leg.left.front = resize(removeTransparency(getPart(skin, 20, 52, 4, 12, 1)), scale); + parts.shoulder.left = resize(removeTransparency(getPart(skin, 36, 48, arm_width, 4, 1)), scale); + + // See #117 + // if MC-89760 gets fixed, we can (probably) simply check the whole skin for transparency + render_body = overlay && hasTransparency(getPart(skin, 16, 32, 32, 16, 1)); + render_rleg = overlay && hasTransparency(getPart(skin, 0, 32, 16, 16, 1)); + render_lleg = overlay && hasTransparency(getPart(skin, 0, 48, 16, 16, 1)); + render_larm = overlay && hasTransparency(getPart(skin, 40, 32, 16, 16, 1)); + render_rarm = overlay && hasTransparency(getPart(skin, 48, 48, 16, 16, 1)); + + // body overlay is shifted 16px down + overlays.body.front = render_body && getPart(skin, 20, 20 + 16, 8, 12, scale); + + // right arm overlay is shifted 16px down + overlays.arm.right.front = render_rarm && getPart(skin, 44, 20 + 16, arm_width, 12, scale); + overlays.arm.right.side = render_rarm && getPart(skin, 40, 20 + 16, 4, 12, scale); + overlays.shoulder.right = render_rarm && getPart(skin, 44, 16 + 16, arm_width, 4, scale); + + // left arm overlay is shifted 16px right + overlays.arm.left.front = render_larm && getPart(skin, 36 + 16, 52, arm_width, 12, scale); + overlays.shoulder.left = render_larm && getPart(skin, 36 + 16, 48, arm_width, 4, scale); + + // right leg overlay is shifted 16px down + overlays.leg.right.front = render_rleg && getPart(skin, 4, 20 + 16, 4, 12, scale); + overlays.leg.right.side = render_rleg && getPart(skin, 0, 20 + 16, 4, 12, scale); + // left leg overlay is shifted 16px left + overlays.leg.left.front = render_lleg && getPart(skin, 20 - 16, 52, 4, 12, scale); } var x = 0; @@ -109,15 +198,51 @@ exp.draw_model = function(rid, img, scale, helm, is_body, callback) { frontc.drawImage(parts.leg.right.front, 4 * scale, 12 * scale, 4 * scale, 12 * scale); frontc.drawImage(parts.leg.left.front, 8 * scale, 12 * scale, 4 * scale, 12 * scale); + // front overlay + var fronto = new Canvas(); + if (!old_skin) { + // pre-render front overlay onto separate canvas + fronto.width = scale * 16; + fronto.height = scale * 24; + var frontoc = fronto.getContext("2d"); + frontoc.patternQuality = "fast"; + + if (render_rarm) { + frontoc.drawImage(overlays.arm.right.front, (4 - arm_width) * scale, 0 * scale, arm_width * scale, 12 * scale + 1); + } + if (render_larm) { + frontoc.drawImage(overlays.arm.left.front, 12 * scale, 0 * scale, arm_width * scale, 12 * scale + 1); + } + if (render_body) { + frontoc.drawImage(overlays.body.front, 4 * scale, 0 * scale, 8 * scale, 12 * scale); + } + if (render_rleg) { + frontoc.drawImage(overlays.leg.right.front, 4 * scale, 12 * scale, 4 * scale, 12 * scale); + } + if (render_lleg) { + frontoc.drawImage(overlays.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(parts.shoulder.right, y - z - 0.5, x + z, parts.shoulder.right.width + 1, parts.shoulder.right.height + 1); + if (render_rarm) { + x -= 1; + ctx.drawImage(overlays.shoulder.right, y - z, x + z, overlays.shoulder.right.width + 2, overlays.shoulder.right.height + 2); + } y = scale * 8; ctx.drawImage(parts.shoulder.left, y - z, x + z, parts.shoulder.left.width, parts.shoulder.left.height + 1); + if (render_larm) { + console.log(overlays.shoulder.left); + z += 0.5; + ctx.drawImage(overlays.shoulder.left, y - z, x + z, overlays.shoulder.left.width + 1, overlays.shoulder.left.height + 1); + } // right side ctx.setTransform(1, skew_a, 0, skew_b, 0, 0); @@ -125,17 +250,27 @@ exp.draw_model = function(rid, img, scale, helm, is_body, callback) { y = 0; z = z_offset + scale * 20; ctx.drawImage(parts.leg.right.side, x + y, z - y, parts.leg.right.side.width, parts.leg.right.side.height); + if (render_rleg) { + ctx.drawImage(overlays.leg.right.side, x + y, z - y, overlays.leg.right.side.width, overlays.leg.right.side.height + 0.5); + } x = x_offset + scale * 2; y = scale * -arm_width; z = z_offset + scale * 8; ctx.drawImage(parts.arm.right.side, x + y, z - y - 0.5, parts.arm.right.side.width, parts.arm.right.side.height + 1); + if (render_rarm) { + z -= 1; + ctx.drawImage(overlays.arm.right.side, x + y, z - y, overlays.arm.right.side.width, overlays.arm.right.side.height + 2); + } // 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); + if (!old_skin) { + ctx.drawImage(fronto, y + x, x + z - 1, fronto.width, fronto.height + 1); + } } // head top @@ -144,6 +279,11 @@ exp.draw_model = function(rid, img, scale, helm, is_body, callback) { z = z_offset; ctx.setTransform(1, -skew_a, 1, skew_a, 0, 0); ctx.drawImage(parts.head.top, y - z, x + z, parts.head.top.width, parts.head.top.height + 1); + if (render_head) { + x -= 0.5; + z -= 0.5; + ctx.drawImage(overlays.head.top, y - z, x + z, overlays.head.top.width + 0.5, overlays.head.top.height + 1.5); + } // head front x = x_offset + 8 * scale; @@ -151,6 +291,10 @@ exp.draw_model = function(rid, img, scale, helm, is_body, callback) { z = z_offset - 0.5; ctx.setTransform(1, -skew_a, 0, skew_b, 0, skew_a); ctx.drawImage(parts.head.front, y + x, x + z, parts.head.front.width, parts.head.front.height); + if (render_head) { + z -= 1; + ctx.drawImage(overlays.head.front, y + x, x + z, overlays.head.front.width, overlays.head.front.height + 2); + } // head right x = x_offset; @@ -158,6 +302,10 @@ exp.draw_model = function(rid, img, scale, helm, is_body, callback) { z = z_offset; ctx.setTransform(1, skew_a, 0, skew_b, 0, 0); ctx.drawImage(parts.head.right, x + y, z - y - 0.5, parts.head.right.width, parts.head.right.height + 1); + if (render_head) { + z -= 1; + ctx.drawImage(overlays.head.right, x + y, z - y - 0.5, overlays.head.right.width, overlays.head.right.height + 3); + } canvas.toBuffer(function(err, buf) { if (err) { diff --git a/package.json b/package.json index 6428b81..6ab201c 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "iojs": "2.0.x" }, "dependencies": { - "canvas": "^1.2.3", + "canvas": "^1.3.4", "crc": "~3.3.0", "forever": "~0.14.2", "jade": "~1.11.0", From 6e6eff529984d34b6133cda3cab88bf5e418c134 Mon Sep 17 00:00:00 2001 From: jomo Date: Mon, 23 Nov 2015 03:45:17 +0100 Subject: [PATCH 08/19] add back missing helpers.js --- lib/helpers.js | 385 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 385 insertions(+) create mode 100644 lib/helpers.js diff --git a/lib/helpers.js b/lib/helpers.js new file mode 100644 index 0000000..549e35a --- /dev/null +++ b/lib/helpers.js @@ -0,0 +1,385 @@ +var networking = require("./networking"); +var logging = require("./logging"); +var renders = require("./renders"); +var config = require("../config"); +var cache = require("./cache"); +var skins = require("./skins"); +var path = require("path"); +var fs = require("fs"); + +// 0098cb60-fa8e-427c-b299-793cbd302c9a +var valid_user_id = /^([0-9a-f-A-F-]{32,36}|[a-zA-Z0-9_]{1,16})$/; // uuid|username +var hash_pattern = /[0-9a-f]+$/; + +// gets the hash from the textures.minecraft.net +url+ +function get_hash(url) { + return hash_pattern.exec(url)[0].toLowerCase(); +} + +// 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 +function store_skin(rid, userId, profile, cache_details, callback) { + networking.get_skin_url(rid, userId, profile, function(err, url) { + if (!err && url) { + var skin_hash = get_hash(url); + if (cache_details && cache_details.skin === skin_hash) { + cache.update_timestamp(rid, userId, skin_hash, false, function(cache_err) { + callback(cache_err, skin_hash); + }); + } else { + logging.debug(rid, "new skin hash:", skin_hash); + var facepath = path.join(__dirname, "..", config.directories.faces, skin_hash + ".png"); + var helmpath = path.join(__dirname, "..", config.directories.helms, skin_hash + ".png"); + var skinpath = path.join(__dirname, "..", config.directories.skins, skin_hash + ".png"); + fs.exists(facepath, function(exists) { + if (exists) { + logging.debug(rid, "skin already exists, not downloading"); + callback(null, skin_hash); + } else { + networking.get_from(rid, url, function(img, response, err1) { + if (err1 || !img) { + callback(err1, null); + } else { + skins.save_image(img, skinpath, function(skin_err) { + if (skin_err) { + logging.error(rid, skin_err); + callback(skin_err, null); + } else { + skins.extract_face(img, facepath, function(err2) { + if (err2) { + logging.error(rid, err2.stack); + callback(err2, null); + } 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); + }); + } + }); + } + }); + } + }); + } + }); + } + } else { + callback(err, null); + } + }); +} + +// gets the cape for +userId+ with +profile+ +// uses +cache_details+ to determine if the cape needs to be downloaded or can be taken from cache +// the cape - if downloaded - is stored to file +// callback: error, cape hash +function store_cape(rid, userId, profile, cache_details, callback) { + networking.get_cape_url(rid, userId, profile, function(err, url) { + if (!err && url) { + var cape_hash = get_hash(url); + if (cache_details && cache_details.cape === cape_hash) { + cache.update_timestamp(rid, userId, cape_hash, false, function(cache_err) { + callback(cache_err, cape_hash); + }); + } else { + logging.debug(rid, "new cape hash:", cape_hash); + var capepath = path.join(__dirname, "..", config.directories.capes, cape_hash + ".png"); + fs.exists(capepath, function(exists) { + if (exists) { + logging.debug(rid, "cape already exists, not downloading"); + callback(null, cape_hash); + } else { + networking.get_from(rid, url, function(img, response, net_err) { + if (net_err || !img) { + logging.error(rid, net_err.stack); + callback(net_err, null); + } else { + skins.save_image(img, capepath, function(skin_err) { + logging.debug(rid, "cape saved"); + callback(skin_err, cape_hash); + }); + } + }); + } + }); + } + } else { + callback(err, null); + } + }); +} + +// used by store_images to queue simultaneous requests for identical userId +// the first request has to be completed until all others are continued +// otherwise we risk running into Mojang's rate limit and deleting the cached skin +var requests = { + skin: {}, + cape: {} +}; + +function push_request(userId, type, fun) { + if (!requests[type][userId]) { + requests[type][userId] = []; + } + requests[type][userId].push(fun); +} + +// calls back all queued requests that match userId and type +function resume(userId, type, err, hash) { + var callbacks = requests[type][userId]; + if (callbacks) { + if (callbacks.length > 1) { + logging.debug(callbacks.length, "simultaneous requests for", userId); + } + + for (var i = 0; i < callbacks.length; i++) { + // continue the request + callbacks[i](err, hash); + // remove from array + callbacks.splice(i, 1); + i--; + } + + // it's still an empty array + delete requests[type][userId]; + } +} + +// 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 +function store_images(rid, userId, cache_details, type, callback) { + var is_uuid = userId.length > 16; + if (requests[type][userId]) { + logging.debug(rid, "adding to request queue"); + push_request(userId, type, callback); + } else { + // add request to the queue + push_request(userId, type, callback); + + networking.get_profile(rid, (is_uuid ? userId : null), function(err, profile) { + if (err || (is_uuid && !profile)) { + // error or uuid without profile + if (!err && !profile) { + // no error, but uuid without profile + cache.save_hash(rid, userId, null, null, 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); + }); + } else { + // an error occured, not caching. we can try in 60 seconds + resume(userId, type, err, null); + } + } 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) { + if (store_err && !skin_hash) { + // an error occured, not caching. we can try in 60 seconds + resume(userId, "skin", store_err, null); + } else { + cache.save_hash(rid, userId, skin_hash, undefined, function(cache_err) { + resume(userId, "skin", (store_err || cache_err), skin_hash); + }); + } + }); + 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); + } else { + cache.save_hash(rid, userId, undefined, cape_hash, function(cache_err) { + resume(userId, "cape", (store_err || cache_err), cape_hash); + }); + } + }); + } + }); + } +} + +var exp = {}; + +// returns true if the +userId+ is a valid userId or username +// the userId may be not exist, however +exp.id_valid = function(userId) { + return valid_user_id.test(userId); +}; + +// decides whether to get a +type+ image for +userId+ from disk or to download it +// callback: error, status, hash +// for status, see response.js +exp.get_image_hash = function(rid, userId, type, callback) { + cache.get_details(userId, function(err, cache_details) { + var cached_hash = null; + if (cache_details !== null) { + cached_hash = type === "skin" ? cache_details.skin : cache_details.cape; + } + if (err) { + callback(err, -1, null); + } 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); + } else { + // download image + if (cache_details) { + logging.debug(rid, "userId cached, but too old"); + } else { + logging.debug(rid, "userId not cached"); + } + store_images(rid, userId, cache_details, type, function(store_err, new_hash) { + 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, cached_hash, true, function(err2) { + callback(err2 || store_err, -1, cache_details && cached_hash); + }); + } 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); + } + }); + } + } + }); +}; + + +// handles requests for +userId+ avatars with +size+ +// callback: error, status, image buffer, skin hash +// image is the user's face+helm when helm is true, or the face otherwise +// for status, see get_image_hash +exp.get_avatar = function(rid, userId, helm, size, callback) { + exp.get_image_hash(rid, userId, "skin", function(err, status, skin_hash) { + if (skin_hash) { + var facepath = path.join(__dirname, "..", config.directories.faces, skin_hash + ".png"); + var helmpath = path.join(__dirname, "..", config.directories.helms, skin_hash + ".png"); + var filepath = facepath; + fs.exists(helmpath, function(exists) { + if (helm && exists) { + filepath = helmpath; + } + skins.resize_img(filepath, size, function(img_err, image) { + if (img_err) { + callback(img_err, -1, null, skin_hash); + } else { + callback(err, (err ? -1 : status), image, skin_hash); + } + }); + }); + } else { + // hash is null when userId has no skin + callback(err, status, null, null); + } + }); +}; + +// handles requests for +userId+ skins +// callback: error, skin hash, status, image buffer +exp.get_skin = function(rid, userId, callback) { + exp.get_image_hash(rid, userId, "skin", function(err, status, skin_hash) { + if (skin_hash) { + var skinpath = path.join(__dirname, "..", 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); + }); + } else { + networking.save_texture(rid, skin_hash, skinpath, function(net_err, response, img) { + callback(net_err || err, skin_hash, status, img); + }); + } + }); + } else { + callback(err, null, status, null); + } + }); +}; + +// helper method used for file names +// possible returned names based on +helm+ and +body+ are: +// body, bodyhelm, head, headhelm +function get_type(helm, body) { + var text = body ? "body" : "head"; + return helm ? text + "helm" : text; +} + +// handles creations of 3D renders +// callback: error, skin hash, image buffer +exp.get_render = function(rid, userId, scale, helm, body, callback) { + exp.get_skin(rid, userId, function(err, skin_hash, status, img) { + if (!skin_hash) { + callback(err, status, skin_hash, null); + return; + } + var renderpath = path.join(__dirname, "..", config.directories.renders, [skin_hash, scale, get_type(helm, body)].join("-") + ".png"); + fs.exists(renderpath, function(exists) { + if (exists) { + renders.open_render(rid, renderpath, function(render_err, rendered_img) { + callback(render_err, 1, skin_hash, rendered_img); + }); + return; + } else { + if (!img) { + callback(err, 0, skin_hash, null); + return; + } + renders.draw_model(rid, img, scale, helm, body, function(draw_err, drawn_img) { + if (draw_err) { + callback(draw_err, -1, skin_hash, null); + } else if (!drawn_img) { + callback(null, 0, skin_hash, null); + } else { + fs.writeFile(renderpath, drawn_img, "binary", function(fs_err) { + if (fs_err) { + logging.error(rid, fs_err.stack); + } + callback(null, 2, skin_hash, drawn_img); + }); + } + }); + } + }); + }); +}; + +// 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) { + if (!cape_hash) { + callback(err, null, status, null); + return; + } + var capepath = path.join(__dirname, "..", config.directories.capes, cape_hash + ".png"); + fs.exists(capepath, function(exists) { + if (exists) { + logging.debug(rid, "cape already exists, not downloading"); + skins.open_skin(rid, capepath, function(skin_err, img) { + callback(skin_err || err, cape_hash, status, img); + }); + } else { + networking.save_texture(rid, cape_hash, capepath, function(net_err, response, img) { + if (response && response.statusCode === 404) { + callback(net_err, cape_hash, status, null); + } else { + callback(net_err, cape_hash, status, img); + } + }); + } + }); + }); +}; + +module.exports = exp; \ No newline at end of file From 7eed1fa09b5bd0073a60a2da61244116c3ecd8a5 Mon Sep 17 00:00:00 2001 From: jomo Date: Mon, 23 Nov 2015 21:58:47 +0100 Subject: [PATCH 09/19] clean up renders again - improved readability a lot - now applies overlays directly to underlying skin part *before* transforming --- lib/renders.js | 228 +++++++++++++++++-------------------------------- 1 file changed, 77 insertions(+), 151 deletions(-) diff --git a/lib/renders.js b/lib/renders.js index 60540a6..d72ccd6 100644 --- a/lib/renders.js +++ b/lib/renders.js @@ -74,6 +74,7 @@ function flip(src) { return dst; } +// skew for isometric perspective var skew_a = 26 / 45; // 0.57777777 var skew_b = skew_a * 2; // 1.15555555 @@ -89,92 +90,75 @@ exp.draw_model = function(rid, img, scale, overlay, is_body, callback) { var old_skin = skin.height === 32; var arm_width = 4; - var parts = { - head: { - front: resize(removeTransparency(getPart(skin, 8, 8, 8, 8, 1)), scale), - right: resize(removeTransparency(getPart(skin, 0, 8, 8, 8, 1)), scale), - top: resize(removeTransparency(getPart(skin, 8, 0, 8, 8, 1)), scale), - }, - arm: { - right: { - front: resize(removeTransparency(getPart(skin, 44, 20, arm_width, 12, 1)), scale), - side: resize(removeTransparency(getPart(skin, 40, 20, 4, 12, 1)), scale), - }, - left: { - front: null, - }, - }, - leg: { - right: { - front: resize(removeTransparency(getPart(skin, 4, 20, 4, 12, 1)), scale), - side: resize(removeTransparency(getPart(skin, 0, 20, 4, 12, 1)), scale), - }, - left: { - front: null, + /* 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 */ + + if (overlay) { + 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); } - }, - shoulder: { - right: resize(removeTransparency(getPart(skin, 44, 16, arm_width, 4, 1)), scale), - left: null, - }, - body: resize(removeTransparency(getPart(skin, 20, 20, 8, 12, 1)), scale), - }; - var overlays = { - head: {}, - arm: {right: {}, left: {}}, - leg: {right: {}, left: {}}, - shoulder: {}, - body: {}, - }; - // overlays - var render_head = overlay && hasTransparency(getPart(skin, 32, 0, 32, 32, 1)); - var render_body; - var render_rleg; - var render_lleg; - var render_larm; - var render_rarm; + 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); + } - // head overlay is shifted 32px right - overlays.head.front = overlay && render_head && getPart(skin, 8 + 32, 8, 8, 8, scale); - overlays.head.right = overlay && render_head && getPart(skin, 0 + 32, 8, 8, 8, scale); - overlays.head.top = overlay && render_head && getPart(skin, 8 + 32, 0, 8, 8, scale); + 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 ( old_skin) { - parts.arm.left.front = flip(parts.arm.right.front); - parts.leg.left.front = flip(parts.leg.right.front); - parts.shoulder.left = flip(parts.shoulder.right); - } else { - // 1.8 skin - has separate left/right arms & legs - parts.arm.left.front = resize(removeTransparency(getPart(skin, 36, 52, arm_width, 12, 1)), scale); - parts.leg.left.front = resize(removeTransparency(getPart(skin, 20, 52, 4, 12, 1)), scale); - parts.shoulder.left = resize(removeTransparency(getPart(skin, 36, 48, arm_width, 4, 1)), scale); + 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); + } - // See #117 - // if MC-89760 gets fixed, we can (probably) simply check the whole skin for transparency - render_body = overlay && hasTransparency(getPart(skin, 16, 32, 32, 16, 1)); - render_rleg = overlay && hasTransparency(getPart(skin, 0, 32, 16, 16, 1)); - render_lleg = overlay && hasTransparency(getPart(skin, 0, 48, 16, 16, 1)); - render_larm = overlay && hasTransparency(getPart(skin, 40, 32, 16, 16, 1)); - render_rarm = overlay && hasTransparency(getPart(skin, 48, 48, 16, 16, 1)); - - // body overlay is shifted 16px down - overlays.body.front = render_body && getPart(skin, 20, 20 + 16, 8, 12, scale); - - // right arm overlay is shifted 16px down - overlays.arm.right.front = render_rarm && getPart(skin, 44, 20 + 16, arm_width, 12, scale); - overlays.arm.right.side = render_rarm && getPart(skin, 40, 20 + 16, 4, 12, scale); - overlays.shoulder.right = render_rarm && getPart(skin, 44, 16 + 16, arm_width, 4, scale); - - // left arm overlay is shifted 16px right - overlays.arm.left.front = render_larm && getPart(skin, 36 + 16, 52, arm_width, 12, scale); - overlays.shoulder.left = render_larm && getPart(skin, 36 + 16, 48, arm_width, 4, scale); - - // right leg overlay is shifted 16px down - overlays.leg.right.front = render_rleg && getPart(skin, 4, 20 + 16, 4, 12, scale); - overlays.leg.right.side = render_rleg && getPart(skin, 0, 20 + 16, 4, 12, scale); - // left leg overlay is shifted 16px left - overlays.leg.left.front = render_lleg && getPart(skin, 20 - 16, 52, 4, 12, scale); + if (hasTransparency(left_leg_region)) { + // render left leg overlay + leg_left_front.getContext("2d").drawImage(getPart(skin, 4, 52, 4, 12, scale), 0, 0); + } + } } var x = 0; @@ -192,37 +176,11 @@ exp.draw_model = function(rid, img, scale, overlay, is_body, callback) { var frontc = front.getContext("2d"); frontc.patternQuality = "fast"; - frontc.drawImage(parts.arm.right.front, (4 - arm_width) * scale, 0 * scale, arm_width * scale, 12 * scale); - frontc.drawImage(parts.arm.left.front, 12 * scale, 0 * scale, arm_width * scale, 12 * scale); - frontc.drawImage(parts.body, 4 * scale, 0 * scale, 8 * scale, 12 * scale); - frontc.drawImage(parts.leg.right.front, 4 * scale, 12 * scale, 4 * scale, 12 * scale); - frontc.drawImage(parts.leg.left.front, 8 * scale, 12 * scale, 4 * scale, 12 * scale); - - // front overlay - var fronto = new Canvas(); - if (!old_skin) { - // pre-render front overlay onto separate canvas - fronto.width = scale * 16; - fronto.height = scale * 24; - var frontoc = fronto.getContext("2d"); - frontoc.patternQuality = "fast"; - - if (render_rarm) { - frontoc.drawImage(overlays.arm.right.front, (4 - arm_width) * scale, 0 * scale, arm_width * scale, 12 * scale + 1); - } - if (render_larm) { - frontoc.drawImage(overlays.arm.left.front, 12 * scale, 0 * scale, arm_width * scale, 12 * scale + 1); - } - if (render_body) { - frontoc.drawImage(overlays.body.front, 4 * scale, 0 * scale, 8 * scale, 12 * scale); - } - if (render_rleg) { - frontoc.drawImage(overlays.leg.right.front, 4 * scale, 12 * scale, 4 * scale, 12 * scale); - } - if (render_lleg) { - frontoc.drawImage(overlays.leg.left.front, 8 * scale, 12 * scale, 4 * scale, 12 * scale); - } - } + 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 @@ -230,47 +188,28 @@ exp.draw_model = function(rid, img, scale, overlay, is_body, callback) { y = scale * -arm_width; z = z_offset + scale * 8; ctx.setTransform(1, -skew_a, 1, skew_a, 0, 0); - ctx.drawImage(parts.shoulder.right, y - z - 0.5, x + z, parts.shoulder.right.width + 1, parts.shoulder.right.height + 1); - if (render_rarm) { - x -= 1; - ctx.drawImage(overlays.shoulder.right, y - z, x + z, overlays.shoulder.right.width + 2, overlays.shoulder.right.height + 2); - } + 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(parts.shoulder.left, y - z, x + z, parts.shoulder.left.width, parts.shoulder.left.height + 1); - if (render_larm) { - console.log(overlays.shoulder.left); - z += 0.5; - ctx.drawImage(overlays.shoulder.left, y - z, x + z, overlays.shoulder.left.width + 1, overlays.shoulder.left.height + 1); - } + 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(parts.leg.right.side, x + y, z - y, parts.leg.right.side.width, parts.leg.right.side.height); - if (render_rleg) { - ctx.drawImage(overlays.leg.right.side, x + y, z - y, overlays.leg.right.side.width, overlays.leg.right.side.height + 0.5); - } + 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(parts.arm.right.side, x + y, z - y - 0.5, parts.arm.right.side.width, parts.arm.right.side.height + 1); - if (render_rarm) { - z -= 1; - ctx.drawImage(overlays.arm.right.side, x + y, z - y, overlays.arm.right.side.width, overlays.arm.right.side.height + 2); - } + 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); - if (!old_skin) { - ctx.drawImage(fronto, y + x, x + z - 1, fronto.width, fronto.height + 1); - } } // head top @@ -278,34 +217,21 @@ exp.draw_model = function(rid, img, scale, overlay, is_body, callback) { y = -0.5; z = z_offset; ctx.setTransform(1, -skew_a, 1, skew_a, 0, 0); - ctx.drawImage(parts.head.top, y - z, x + z, parts.head.top.width, parts.head.top.height + 1); - if (render_head) { - x -= 0.5; - z -= 0.5; - ctx.drawImage(overlays.head.top, y - z, x + z, overlays.head.top.width + 0.5, overlays.head.top.height + 1.5); - } + 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(parts.head.front, y + x, x + z, parts.head.front.width, parts.head.front.height); - if (render_head) { - z -= 1; - ctx.drawImage(overlays.head.front, y + x, x + z, overlays.head.front.width, overlays.head.front.height + 2); - } + 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(parts.head.right, x + y, z - y - 0.5, parts.head.right.width, parts.head.right.height + 1); - if (render_head) { - z -= 1; - ctx.drawImage(overlays.head.right, x + y, z - y - 0.5, overlays.head.right.width, overlays.head.right.height + 3); - } + ctx.drawImage(head_right, x + y, z - y - 0.5, head_right.width, head_right.height + 1); canvas.toBuffer(function(err, buf) { if (err) { From 3c21a59c94fe5896b75b2be0cfda18f03ce0b230 Mon Sep 17 00:00:00 2001 From: jomo Date: Sun, 13 Dec 2015 14:08:59 +0100 Subject: [PATCH 10/19] add support for slim renders, fixes #125, adjust tests --- lib/cache.js | 51 +++++++++++++++------------ lib/helpers.js | 70 ++++++++++++++++++------------------ lib/networking.js | 82 +++++++++++++++++++++++++------------------ lib/object-patch.js | 22 ++++++++++++ lib/renders.js | 4 +-- lib/routes/renders.js | 2 +- test/test.js | 52 +++++++++++++-------------- 7 files changed, 162 insertions(+), 121 deletions(-) create mode 100644 lib/object-patch.js diff --git a/lib/cache.js b/lib/cache.js index 1f7a568..b996d05 100644 --- a/lib/cache.js +++ b/lib/cache.js @@ -67,15 +67,14 @@ exp.get_redis = function() { // updates the redis instance's server_info object // callback: error, info object exp.info = function(callback) { - redis.info(function (err, res) { - + redis.info(function(err, res) { // parse the info command and store it in redis.server_info // this code block was taken from mranney/node_redis#on_info_cmd // http://git.io/LBUNbg var lines = res.toString().split("\r\n"); var obj = {}; - lines.forEach(function (line) { + lines.forEach(function(line) { var parts = line.split(":"); if (parts[1]) { obj[parts[0]] = parts[1]; @@ -109,31 +108,36 @@ exp.update_timestamp = function(rid, userId, hash, temp, callback) { update_file_date(rid, hash); }; -// 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) { +exp.save_hash = function(rid, userId, skin_hash, cape_hash, slim, callback) { logging.debug(rid, "caching skin:" + skin_hash + " cape:" + cape_hash); - var time = Date.now(); - // store shorter null byte instead of "null" - skin_hash = (skin_hash === null ? "" : skin_hash); - cape_hash = (cape_hash === null ? "" : cape_hash); + + // 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 @@ -155,6 +159,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) }; } diff --git a/lib/helpers.js b/lib/helpers.js index 549e35a..4fcd367 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -21,12 +21,12 @@ function get_hash(url) { // face and face+helm images are extracted and stored to files // callback: error, skin hash 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 && url) { var skin_hash = get_hash(url); if (cache_details && cache_details.skin === skin_hash) { cache.update_timestamp(rid, userId, skin_hash, 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,27 +36,27 @@ 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) { if (skin_err) { logging.error(rid, skin_err); - callback(skin_err, null); + callback(skin_err, null, slim); } else { skins.extract_face(img, facepath, function(err2) { if (err2) { logging.error(rid, err2.stack); - 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); }); } }); @@ -129,7 +129,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 callbacks = requests[type][userId]; if (callbacks) { if (callbacks.length > 1) { @@ -138,7 +138,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--; @@ -167,34 +167,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 +212,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,12 +221,12 @@ 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) { @@ -234,18 +234,18 @@ exp.get_image_hash = function(rid, userId, type, callback) { } 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, cached_hash, 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 +259,7 @@ exp.get_image_hash = function(rid, userId, type, callback) { // image is the user's face+helm when helm is true, or the face otherwise // for status, see get_image_hash exp.get_avatar = function(rid, userId, helm, 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(__dirname, "..", config.directories.faces, skin_hash + ".png"); var helmpath = path.join(__dirname, "..", config.directories.helms, skin_hash + ".png"); @@ -286,23 +286,23 @@ exp.get_avatar = function(rid, userId, helm, size, callback) { // handles requests for +userId+ skins // callback: error, skin hash, status, image buffer 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(__dirname, "..", 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 +318,12 @@ function get_type(helm, body) { // handles creations of 3D renders // callback: error, skin hash, image buffer exp.get_render = function(rid, userId, scale, helm, 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(__dirname, "..", config.directories.renders, [skin_hash, scale, get_type(helm, body)].join("-") + ".png"); + var renderpath = path.join(__dirname, "..", config.directories.renders, [skin_hash, scale, get_type(helm, 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 +335,7 @@ exp.get_render = function(rid, userId, scale, helm, body, callback) { callback(err, 0, skin_hash, null); return; } - renders.draw_model(rid, img, scale, helm, body, function(draw_err, drawn_img) { + renders.draw_model(rid, img, scale, helm, body, slim, function(draw_err, drawn_img) { if (draw_err) { callback(draw_err, -1, skin_hash, null); } else if (!drawn_img) { @@ -357,7 +357,7 @@ exp.get_render = function(rid, userId, scale, helm, 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; diff --git a/lib/networking.js b/lib/networking.js index 5e387cc..0aca57a 100644 --- a/lib/networking.js +++ b/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/"; @@ -22,40 +23,46 @@ function extract_url(profile, type) { 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; + url = Object.get(props, "textures." + type + ".url") || null; } }); } return url; } -// helper method that calls `get_username_url` or `get_uuid_url` based on the +usedId+ +// extracts the +type+ [SKIN|CAPE] URL +// from the nested & encoded +profile+ object +// returns the if the model is "slim" +function extract_model(profile) { + var slim = 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); + slim = Object.get(props, "textures.SKIN.metadata.model"); + } + }); + } + return slim === "slim"; +} + +// 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 @@ -112,6 +119,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) { @@ -126,15 +134,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"); } - callback(url || null); + + callback(url || null, !!slim); }; // make a request to sessionserver for +uuid+ @@ -149,20 +166,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 diff --git a/lib/object-patch.js b/lib/object-patch.js new file mode 100644 index 0000000..261460e --- /dev/null +++ b/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; +}; \ No newline at end of file diff --git a/lib/renders.js b/lib/renders.js index d72ccd6..f30f321 100644 --- a/lib/renders.js +++ b/lib/renders.js @@ -78,7 +78,7 @@ function flip(src) { 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, callback) { +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); @@ -88,7 +88,7 @@ exp.draw_model = function(rid, img, scale, overlay, is_body, callback) { skin.onload = function() { var old_skin = skin.height === 32; - var arm_width = 4; + var arm_width = slim ? 3 : 4; /* eslint-disable no-multi-spaces */ var head_top = resize(removeTransparency(getPart(skin, 8, 0, 8, 8, 1)), scale); diff --git a/lib/routes/renders.js b/lib/routes/renders.js index 65b2214..0df771f 100644 --- a/lib/routes/renders.js +++ b/lib/routes/renders.js @@ -36,7 +36,7 @@ function handle_default(rid, scale, helm, body, img_status, userId, size, def, r // handle steve and alex 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, helm, body, function(render_err, def_img) { + renders.draw_model(rid, buf, scale, helm, body, def === "alex", function(render_err, def_img) { callback({ status: img_status, body: def_img, diff --git a/test/test.js b/test/test.js index 8bfd808..424ba46 100644 --- a/test/test.js +++ b/test/test.js @@ -139,7 +139,7 @@ describe("Crafatar", function() { 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(); }); @@ -548,17 +548,17 @@ describe("Crafatar", function() { "head render with existing username": { url: "http://localhost:3000/renders/head/jeb_?scale=2", etag: '"a846b82963"', - crc32: [353633671, 370672768] + crc32: [3487896679, 3001090792] }, "head render with non-existent username": { url: "http://localhost:3000/renders/head/0?scale=2", etag: '"steve"', - crc32: [883439147, 433083528] + crc32: [3257141069, 214248305] }, "head render with non-existent username defaulting to alex": { url: "http://localhost:3000/renders/head/0?scale=2&default=alex", etag: '"alex"', - crc32: [1240086237, 1108800327] + crc32: [263450586, 3116770561] }, "head render with non-existent username defaulting to username": { url: "http://localhost:3000/avatars/0?scale=2&default=jeb_", @@ -578,17 +578,17 @@ describe("Crafatar", function() { "helm head render with existing username": { url: "http://localhost:3000/renders/head/jeb_?scale=2&helm", etag: '"a846b82963"', - crc32: [3456497067, 3490318764] + crc32: [762377383, 1726474987] }, "helm head render with non-existent username": { url: "http://localhost:3000/renders/head/0?scale=2&helm", etag: '"steve"', - crc32: [1858563554, 2647471936] + crc32: [3257141069, 214248305] }, "helm head render with non-existent username defaulting to alex": { url: "http://localhost:3000/renders/head/0?scale=2&helm&default=alex", etag: '"alex"', - crc32: [2963161105, 1769904825] + crc32: [263450586, 3116770561] }, "helm head render with non-existent username defaulting to username": { url: "http://localhost:3000/renders/head/0?scale=2&helm&default=jeb_", @@ -608,17 +608,17 @@ describe("Crafatar", function() { "head render with existing uuid": { url: "http://localhost:3000/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2", etag: '"a846b82963"', - crc32: [353633671, 370672768] + crc32: [3487896679] }, "head render with non-existent uuid": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2", etag: '"steve"', - crc32: [883439147, 433083528] + crc32: [3257141069, 214248305] }, "head render with non-existent uuid defaulting to alex": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=alex", etag: '"alex"', - crc32: [1240086237, 1108800327] + crc32: [263450586, 3116770561] }, "head render with non-existent uuid defaulting to username": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&default=jeb_", @@ -638,17 +638,17 @@ describe("Crafatar", function() { "helm head render with existing uuid": { url: "http://localhost:3000/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&helm", etag: '"a846b82963"', - crc32: [3456497067, 3490318764] + crc32: [762377383] }, "helm head render with non-existent uuid": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm", etag: '"steve"', - crc32: [1858563554, 2647471936] + crc32: [3257141069, 214248305] }, "helm head render with non-existent uuid defaulting to alex": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm&default=alex", etag: '"alex"', - crc32: [2963161105, 1769904825] + crc32: [263450586, 3116770561] }, "helm head with non-existent uuid defaulting to username": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&helm&default=jeb_", @@ -668,17 +668,17 @@ describe("Crafatar", function() { "body render with existing username": { url: "http://localhost:3000/renders/body/jeb_?scale=2", etag: '"a846b82963"', - crc32: [1291941229, 2628108474] + crc32: [3127075871, 2595192206] }, "body render with non-existent username": { url: "http://localhost:3000/renders/body/0?scale=2", etag: '"steve"', - crc32: [2652947188, 2115706574] + crc32: [1046655221, 1620063267] }, "body render with non-existent username defaulting to alex": { url: "http://localhost:3000/renders/body/0?scale=2&default=alex", etag: '"alex"', - crc32: [407932087, 2516216042] + crc32: [549240598, 3952648540] }, "body render with non-existent username defaulting to username": { url: "http://localhost:3000/renders/body/0?scale=2&default=jeb_", @@ -698,17 +698,17 @@ describe("Crafatar", function() { "helm body render with existing username": { url: "http://localhost:3000/renders/body/jeb_?scale=2&helm", etag: '"a846b82963"', - crc32: [3556188297, 4269754007] + crc32: [699892097, 2732138694] }, "helm body render with non-existent username": { url: "http://localhost:3000/renders/body/0?scale=2&helm", etag: '"steve"', - crc32: [272191039, 542896675] + crc32: [1046655221, 1620063267] }, "helm body render with non-existent username defaulting to alex": { url: "http://localhost:3000/renders/body/0?scale=2&helm&default=alex", etag: '"alex"', - crc32: [737759773, 66512449] + crc32: [549240598, 3952648540] }, "helm body render with non-existent username defaulting to username": { url: "http://localhost:3000/renders/body/0?scale=2&helm&default=jeb_", @@ -728,17 +728,17 @@ describe("Crafatar", function() { "body render with existing uuid": { url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2", etag: '"a846b82963"', - crc32: [1291941229, 2628108474] + crc32: [3127075871] }, "body render with non-existent uuid": { url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2", etag: '"steve"', - crc32: [2652947188, 2115706574] + crc32: [1046655221, 1620063267] }, "body render with non-existent uuid defaulting to alex": { url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&default=alex", etag: '"alex"', - crc32: [407932087, 2516216042] + crc32: [549240598, 3952648540] }, "body render with non-existent uuid defaulting to username": { url: "http://localhost:3000/renders/body/0?scale=2&default=jeb_", @@ -758,17 +758,17 @@ describe("Crafatar", function() { "helm body render with existing uuid": { url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2&helm", etag: '"a846b82963"', - crc32: [3556188297, 4269754007] + crc32: [699892097] }, "helm body render with non-existent uuid": { url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&helm", etag: '"steve"', - crc32: [272191039, 542896675] + crc32: [1046655221, 1620063267] }, "helm body render with non-existent uuid defaulting to alex": { url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&helm&default=alex", etag: '"alex"', - crc32: [737759773, 66512449] + crc32: [549240598, 3952648540] }, "helm body render with non-existent uuid defaulting to url": { url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&helm&default=http%3A%2F%2Fexample.com", @@ -795,7 +795,7 @@ describe("Crafatar", function() { } } } else { - matches = (location.crc32 === crc(body)); + matches = location.crc32 === crc(body); } try { assert.ok(matches); From 5cb20b11051c24f9c717fcabad391aa4683765d2 Mon Sep 17 00:00:00 2001 From: jomo Date: Mon, 14 Dec 2015 01:59:32 +0100 Subject: [PATCH 11/19] add more crc values to tests --- test/test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test.js b/test/test.js index 40d0772..1e312f6 100644 --- a/test/test.js +++ b/test/test.js @@ -610,7 +610,7 @@ describe("Crafatar", function() { "head render with existing uuid": { url: "http://localhost:3000/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2", etag: '"a846b82963"', - crc32: [3487896679] + crc32: [3487896679, 3001090792] }, "head render with non-existent uuid": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2", @@ -640,7 +640,7 @@ describe("Crafatar", function() { "overlay head render with existing uuid": { url: "http://localhost:3000/renders/head/853c80ef3c3749fdaa49938b674adae6?scale=2&overlay", etag: '"a846b82963"', - crc32: [762377383] + crc32: [762377383, 1726474987] }, "overlay head render with non-existent uuid": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&overlay", @@ -730,7 +730,7 @@ describe("Crafatar", function() { "body render with existing uuid": { url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2", etag: '"a846b82963"', - crc32: [3127075871] + crc32: [3127075871, 2595192206] }, "body render with non-existent uuid": { url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2", @@ -760,7 +760,7 @@ describe("Crafatar", function() { "overlay body render with existing uuid": { url: "http://localhost:3000/renders/body/853c80ef3c3749fdaa49938b674adae6?scale=2&overlay", etag: '"a846b82963"', - crc32: [699892097] + crc32: [699892097, 2732138694] }, "overlay body render with non-existent uuid": { url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&overlay", From caeb9a52fe86ee703a26cb983316788dee3d7917 Mon Sep 17 00:00:00 2001 From: jomo Date: Tue, 15 Dec 2015 21:09:13 +0100 Subject: [PATCH 12/19] verbose logging on travis --- test/test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test.js b/test/test.js index 1e312f6..dfc6657 100644 --- a/test/test.js +++ b/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() {}; } From 1144b6755a72f0e9efa95f16febcd687a34208e5 Mon Sep 17 00:00:00 2001 From: jomo Date: Wed, 16 Dec 2015 00:47:51 +0100 Subject: [PATCH 13/19] always use crc32 for etag, much more reliable than mojang skin hash had to make quite a few changes to tests to prevent them from failing also, etag is now only sent with a 200 response, as defined in RFC7232 --- lib/networking.js | 34 --------- lib/response.js | 32 ++++----- test/test.js | 177 +++++++++++++++++----------------------------- 3 files changed, 81 insertions(+), 162 deletions(-) diff --git a/lib/networking.js b/lib/networking.js index cbeba48..a9a622c 100644 --- a/lib/networking.js +++ b/lib/networking.js @@ -13,40 +13,6 @@ 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 = Object.get(props, "textures." + type + ".url") || null; - } - }); - } - return url; -} - -// extracts the +type+ [SKIN|CAPE] URL -// from the nested & encoded +profile+ object -// returns the if the model is "slim" -function extract_model(profile) { - var slim = 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); - slim = Object.get(props, "textures.SKIN.metadata.model"); - } - }); - } - return slim === "slim"; -} - // 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 // callback: error, url, slim diff --git a/lib/response.js b/lib/response.js index 561d58f..fbaf8f2 100644 --- a/lib/response.js +++ b/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); }; \ No newline at end of file diff --git a/test/test.js b/test/test.js index dfc6657..b3b18d1 100644 --- a/test/test.js +++ b/test/test.js @@ -236,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(); }); @@ -341,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 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 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 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 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 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 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: [3487896679, 3001090792] }, "head render with non-existent username": { url: "http://localhost:3000/renders/head/0?scale=2", - etag: '"mhf_steve"', crc32: [3257141069, 214248305] }, "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: [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: [762377383, 1726474987] }, "overlay head render with non-existent username": { url: "http://localhost:3000/renders/head/0?scale=2&overlay", - etag: '"mhf_steve"', crc32: [3257141069, 214248305] }, "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: [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: [3487896679, 3001090792] }, "head render with non-existent uuid": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2", - etag: '"mhf_steve"', crc32: [3257141069, 214248305] }, "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: [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: [762377383, 1726474987] }, "overlay head render with non-existent uuid": { url: "http://localhost:3000/renders/head/00000000000000000000000000000000?scale=2&overlay", - etag: '"mhf_steve"', crc32: [3257141069, 214248305] }, "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: [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: [3127075871, 2595192206] }, "body render with non-existent username": { url: "http://localhost:3000/renders/body/0?scale=2", - etag: '"mhf_steve"', crc32: [1046655221, 1620063267] }, "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: [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: [699892097, 2732138694] }, "overlay body render with non-existent username": { url: "http://localhost:3000/renders/body/0?scale=2&overlay", - etag: '"mhf_steve"', crc32: [1046655221, 1620063267] }, "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: [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: [3127075871, 2595192206] }, "body render with non-existent uuid": { url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2", - etag: '"mhf_steve"', crc32: [1046655221, 1620063267] }, "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: [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: [699892097, 2732138694] }, "overlay body render with non-existent uuid": { url: "http://localhost:3000/renders/body/00000000000000000000000000000000?scale=2&overlay", - etag: '"mhf_steve"', crc32: [1046655221, 1620063267] }, "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: [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" }, }; @@ -787,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(); }); From 1f0f696151c3f8503c585813eec41d82b173a73a Mon Sep 17 00:00:00 2001 From: jomo Date: Mon, 11 Jan 2016 07:37:28 +0100 Subject: [PATCH 14/19] Make sure 'slim' model is correctly checked The 'textures.SKIN.metadata.model' field seems to be only present if set to 'slim', so currently this change has no effect however, if the field is returned in other cases (in future), we need to make sure it acually reports 'slim' --- lib/networking.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/networking.js b/lib/networking.js index a9a622c..8cb5e4a 100644 --- a/lib/networking.js +++ b/lib/networking.js @@ -135,7 +135,7 @@ exp.get_uuid_info = function(profile, type, callback) { var url = Object.get(profile, "textures." + type + ".url"); var slim; if (type === "SKIN") { - slim = Object.get(profile, "textures.SKIN.metadata.model"); + slim = Object.get(profile, "textures.SKIN.metadata.model") === "slim"; } callback(url || null, !!slim); From fcdb03173a8888e12986c2583b35cfb213b709be Mon Sep 17 00:00:00 2001 From: jomo Date: Wed, 13 Jan 2016 02:15:47 +0100 Subject: [PATCH 15/19] Use UUID's `profileName` response to update username model type, see #125 --- lib/cache.js | 11 ++++++++++- lib/helpers.js | 7 ++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/cache.js b/lib/cache.js index 219e6af..7f81cd7 100644 --- a/lib/cache.js +++ b/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 @@ -134,7 +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, + slim: data.a === "1", time: Number(data.t) }; } diff --git a/lib/helpers.js b/lib/helpers.js index 5859b7a..67a0468 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -19,9 +19,14 @@ 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_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) { From 5b1ad851efbdc4daf8a27de563e8b6ef27f200e4 Mon Sep 17 00:00:00 2001 From: jomo Date: Tue, 19 Jan 2016 23:08:56 +0100 Subject: [PATCH 16/19] mhf_alex is always slim --- lib/helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/helpers.js b/lib/helpers.js index 67a0468..bb14c21 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -341,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, slim, 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) { From b9f6a219425817b4b4c01720b67d35745384ee54 Mon Sep 17 00:00:00 2001 From: jomo Date: Wed, 20 Jan 2016 00:12:53 +0100 Subject: [PATCH 17/19] ignore case when checking for mhf_alex or mhf_steve --- lib/routes/avatars.js | 5 +++-- lib/routes/renders.js | 5 +++-- lib/routes/skins.js | 5 +++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/routes/avatars.js b/lib/routes/avatars.js index 5146638..24d6497 100644 --- a/lib/routes/avatars.js +++ b/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; } diff --git a/lib/routes/renders.js b/lib/routes/renders.js index 6950a11..90ca267 100644 --- a/lib/routes/renders.js +++ b/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,7 +35,7 @@ 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; } diff --git a/lib/routes/skins.js b/lib/routes/skins.js index 19280e4..f71991a 100644 --- a/lib/routes/skins.js +++ b/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; } From 8367c1e5198bc75a8ab7849ffb19d0342b965f98 Mon Sep 17 00:00:00 2001 From: jomo Date: Wed, 20 Jan 2016 00:56:20 +0100 Subject: [PATCH 18/19] mhf_alex should default to mhf_alex if skin not accessible We only use the 'hard stored' mhf_alex skin when it's part of the 'default' query parameter. In the rare event that mhf_alex is requested but the skin is not accessible, we would fall back to 'mhf_steve' because it's the default for usernames. This commit adds the special case to use the stored version of the 'mhf_alex' skin when it's not accessible otherwise --- lib/skins.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/skins.js b/lib/skins.js index a6e1637..d2c7cda 100644 --- a/lib/skins.js +++ b/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 From 74ba82870151bf9138330a2012b2d5dbb517d4ff Mon Sep 17 00:00:00 2001 From: jomo Date: Wed, 20 Jan 2016 01:17:36 +0100 Subject: [PATCH 19/19] add test for uuid -> username skin type update --- test/test.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/test.js b/test/test.js index b3b18d1..d8ae20c 100644 --- a/test/test.js +++ b/test/test.js @@ -775,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) {