Watch out! Skins!

This commit is contained in:
mschiller890
2026-03-13 16:26:19 +01:00
parent 6f7812293e
commit 12d5835711
21 changed files with 737 additions and 31 deletions

View File

@@ -4,8 +4,8 @@
#include "../../world/entity/player/Player.h"
#include "../../world/entity/player/Inventory.h"
HumanoidModel::HumanoidModel( float g /*= 0*/, float yOffset /*= 0*/ )
: holdingLeftHand(false),
HumanoidModel::HumanoidModel( float g /*= 0*/, float yOffset /*= 0*/, int texW /*= 64*/, int texH /*= 32*/ )
: holdingLeftHand(false),
holdingRightHand(false),
sneaking(false),
bowAndArrow(false),
@@ -18,6 +18,9 @@ HumanoidModel::HumanoidModel( float g /*= 0*/, float yOffset /*= 0*/ )
leg0(0, 16),
leg1(0, 16)
{
texWidth = texW;
texHeight = texH;
head.setModel(this);
body.setModel(this);
arm0.setModel(this);

View File

@@ -9,7 +9,7 @@ class ItemInstance;
class HumanoidModel: public Model
{
public:
HumanoidModel(float g = 0, float yOffset = 0);
HumanoidModel(float g = 0, float yOffset = 0, int texW = 64, int texH = 32);
void setupAnim(float time, float r, float bob, float yRot, float xRot, float scale);

View File

@@ -50,12 +50,12 @@ Cube::Cube(ModelPart* modelPart, int xTexOffs, int yTexOffs, float x0, float y0,
VertexPT* l2 = ++ptr;
VertexPT* l3 = ++ptr;
polygons[0] = PolygonQuad(l1, u1, u2, l2, xTexOffs + d + w, yTexOffs + d, xTexOffs + d + w + d, yTexOffs + d + h); // Right
polygons[1] = PolygonQuad(u0, l0, l3, u3, xTexOffs + 0, yTexOffs + d, xTexOffs + d, yTexOffs + d + h); // Left
polygons[2] = PolygonQuad(l1, l0, u0, u1, xTexOffs + d, yTexOffs + 0, xTexOffs + d + w, yTexOffs + d); // Up
polygons[3] = PolygonQuad(u2, u3, l3, l2, xTexOffs + d + w, yTexOffs + d, xTexOffs + d + w + w, yTexOffs); // Down
polygons[4] = PolygonQuad(u1, u0, u3, u2, xTexOffs + d, yTexOffs + d, xTexOffs + d + w, yTexOffs + d + h); // Front
polygons[5] = PolygonQuad(l0, l1, l2, l3, xTexOffs + d + w + d, yTexOffs + d, xTexOffs + d + w + d + w, yTexOffs + d + h); // Back
polygons[0] = PolygonQuad(l1, u1, u2, l2, xTexOffs + d + w, yTexOffs + d, xTexOffs + d + w + d, yTexOffs + d + h, modelPart->xTexSize, modelPart->yTexSize); // Right
polygons[1] = PolygonQuad(u0, l0, l3, u3, xTexOffs + 0, yTexOffs + d, xTexOffs + d, yTexOffs + d + h, modelPart->xTexSize, modelPart->yTexSize); // Left
polygons[2] = PolygonQuad(l1, l0, u0, u1, xTexOffs + d, yTexOffs + 0, xTexOffs + d + w, yTexOffs + d, modelPart->xTexSize, modelPart->yTexSize); // Up
polygons[3] = PolygonQuad(u2, u3, l3, l2, xTexOffs + d + w, yTexOffs + d, xTexOffs + d + w + w, yTexOffs, modelPart->xTexSize, modelPart->yTexSize); // Down
polygons[4] = PolygonQuad(u1, u0, u3, u2, xTexOffs + d, yTexOffs + d, xTexOffs + d + w, yTexOffs + d + h, modelPart->xTexSize, modelPart->yTexSize); // Front
polygons[5] = PolygonQuad(l0, l1, l2, l3, xTexOffs + d + w + d, yTexOffs + d, xTexOffs + d + w + d + w, yTexOffs + d + h, modelPart->xTexSize, modelPart->yTexSize); // Back
if (modelPart->mirror) {
for (int i = 0; i < 6; i++)

View File

@@ -12,15 +12,15 @@ PolygonQuad::PolygonQuad(VertexPT* v0, VertexPT* v1, VertexPT* v2, VertexPT* v3)
}
PolygonQuad::PolygonQuad( VertexPT* v0, VertexPT* v1, VertexPT* v2, VertexPT* v3,
int uu0, int vv0, int uu1, int vv1)
: _flipNormal(false)
int uu0, int vv0, int uu1, int vv1, float texW, float texH)
: _flipNormal(false)
{
const float us = -0.002f / 64.0f;//0.1f / 64.0f;
const float vs = -0.002f / 32.0f;//0.1f / 32.0f;
vertices[0] = v0->remap(uu1 / 64.0f - us, vv0 / 32.0f + vs);
vertices[1] = v1->remap(uu0 / 64.0f + us, vv0 / 32.0f + vs);
vertices[2] = v2->remap(uu0 / 64.0f + us, vv1 / 32.0f - vs);
vertices[3] = v3->remap(uu1 / 64.0f - us, vv1 / 32.0f - vs);
const float us = -0.002f / texW;
const float vs = -0.002f / texH;
vertices[0] = v0->remap(uu1 / texW - us, vv0 / texH + vs);
vertices[1] = v1->remap(uu0 / texW + us, vv0 / texH + vs);
vertices[2] = v2->remap(uu0 / texW + us, vv1 / texH - vs);
vertices[3] = v3->remap(uu1 / texW - us, vv1 / texH - vs);
}
PolygonQuad::PolygonQuad( VertexPT* v0, VertexPT* v1, VertexPT* v2, VertexPT* v3,

View File

@@ -11,7 +11,7 @@ class PolygonQuad
public:
PolygonQuad() {}
PolygonQuad(VertexPT*,VertexPT*,VertexPT*,VertexPT*);
PolygonQuad(VertexPT*,VertexPT*,VertexPT*,VertexPT*, int u0, int v0, int u1, int v1);
PolygonQuad(VertexPT*,VertexPT*,VertexPT*,VertexPT*, int u0, int v0, int u1, int v1, float texW = 64.0f, float texH = 32.0f);
PolygonQuad(VertexPT*,VertexPT*,VertexPT*,VertexPT*, float u0, float v0, float u1, float v1);
void mirror();

View File

@@ -18,6 +18,20 @@
#include "../../network/packet/SendInventoryPacket.h"
#include "../../network/packet/EntityEventPacket.h"
#include "../../network/packet/PlayerActionPacket.h"
#include <vector>
#include <cctype>
#include "../../platform/log.h"
#include "../../platform/HttpClient.h"
#include "../../platform/CThread.h"
#include "../../util/StringUtils.h"
#if defined(_WIN32)
#include <direct.h>
#else
#include <sys/stat.h>
#include <sys/types.h>
#endif
#ifndef STANDALONE_SERVER
#include "../gui/Screen.h"
#include "../gui/screens/FurnaceScreen.h"
@@ -32,6 +46,195 @@
#include "../../world/item/ArmorItem.h"
#include "../../network/packet/PlayerArmorEquipmentPacket.h"
namespace {
static bool isBase64(unsigned char c) {
return (std::isalnum(c) || (c == '+') || (c == '/'));
}
static std::string base64Decode(const std::string& encoded) {
static const std::string base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string out;
int in_len = (int)encoded.size();
int i = 0;
int in_ = 0;
unsigned char char_array_4[4], char_array_3[3];
while (in_len-- && (encoded[in_] != '=') && isBase64(encoded[in_])) {
char_array_4[i++] = encoded[in_]; in_++;
if (i == 4) {
for (i = 0; i < 4; i++)
char_array_4[i] = (unsigned char)base64Chars.find(char_array_4[i]);
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
for (i = 0; i < 3; i++)
out += char_array_3[i];
i = 0;
}
}
if (i) {
for (int j = i; j < 4; j++)
char_array_4[j] = 0;
for (int j = 0; j < 4; j++)
char_array_4[j] = (unsigned char)base64Chars.find(char_array_4[j]);
char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];
for (int j = 0; (j < i - 1); j++)
out += char_array_3[j];
}
return out;
}
static std::string extractJsonString(const std::string& json, const std::string& key) {
std::string search = "\"" + key + "\"";
size_t pos = json.find(search);
if (pos == std::string::npos) return "";
pos = json.find(':', pos + search.size());
if (pos == std::string::npos) return "";
pos++;
while (pos < json.size() && std::isspace((unsigned char)json[pos])) pos++;
if (pos >= json.size() || json[pos] != '"') return "";
pos++;
size_t end = json.find('"', pos);
if (end == std::string::npos) return "";
return json.substr(pos, end - pos);
}
static std::string getSkinUrlForUsername(const std::string& username) {
if (username.empty()) {
LOGI("[Skin] username empty\n");
return "";
}
LOGI("[Skin] resolving UUID for user '%s'...\n", username.c_str());
std::vector<unsigned char> body;
std::string apiUrl = "http://api.mojang.com/users/profiles/minecraft/" + username;
if (!HttpClient::download(apiUrl, body)) {
LOGW("[Skin] failed to download UUID for %s\n", username.c_str());
return "";
}
std::string response(body.begin(), body.end());
std::string uuid = extractJsonString(response, "id");
if (uuid.empty()) {
LOGW("[Skin] no UUID found in Mojang response for %s\n", username.c_str());
return "";
}
LOGI("[Skin] UUID=%s for user %s\n", uuid.c_str(), username.c_str());
std::string profileUrl = "http://sessionserver.mojang.com/session/minecraft/profile/" + uuid;
if (!HttpClient::download(profileUrl, body)) {
LOGW("[Skin] failed to download profile for UUID %s\n", uuid.c_str());
return "";
}
response.assign(body.begin(), body.end());
std::string encoded = extractJsonString(response, "value");
if (encoded.empty()) {
LOGW("[Skin] no value field in profile response for UUID %s\n", uuid.c_str());
return "";
}
std::string decoded = base64Decode(encoded);
size_t skinPos = decoded.find("\"SKIN\"");
if (skinPos == std::string::npos) {
LOGW("[Skin] no SKIN entry in decoded profile for UUID %s\n", uuid.c_str());
return "";
}
size_t urlPos = decoded.find("\"url\"", skinPos);
if (urlPos == std::string::npos) {
LOGW("[Skin] no url field under SKIN for UUID %s\n", uuid.c_str());
return "";
}
// extract the URL value from the substring starting at urlPos
std::string urlFragment = decoded.substr(urlPos);
std::string skinUrl = extractJsonString(urlFragment, "url");
if (skinUrl.empty()) {
LOGW("[Skin] failed to parse skin URL for UUID %s\n", uuid.c_str());
return "";
}
LOGI("[Skin] skin URL for %s: %s\n", username.c_str(), skinUrl.c_str());
return skinUrl;
}
static bool ensureDirectoryExists(const std::string& path) {
#if defined(_WIN32)
return _mkdir(path.c_str()) == 0 || errno == EEXIST;
#else
struct stat st;
if (stat(path.c_str(), &st) == 0 && S_ISDIR(st.st_mode))
return true;
return mkdir(path.c_str(), 0755) == 0 || errno == EEXIST;
#endif
}
static bool fileExists(const std::string& path) {
struct stat st;
if (stat(path.c_str(), &st) != 0)
return false;
#if defined(_WIN32)
return (st.st_mode & _S_IFREG) != 0;
#else
return S_ISREG(st.st_mode);
#endif
}
static void* fetchSkinForPlayer(void* param) {
LocalPlayer* player = (LocalPlayer*)param;
if (!player) return NULL;
LOGI("[Skin] starting skin download for %s\n", player->name.c_str());
const std::string cacheDir = "data/images/skins";
if (!ensureDirectoryExists(cacheDir)) {
LOGW("[Skin] failed to create cache directory %s\n", cacheDir.c_str());
}
std::string cacheFile = cacheDir + "/" + player->name + ".png";
if (fileExists(cacheFile)) {
LOGI("[Skin] using cached skin for %s\n", player->name.c_str());
player->setTextureName("skins/" + player->name + ".png");
return NULL;
}
std::string skinUrl = getSkinUrlForUsername(player->name);
if (skinUrl.empty()) {
LOGW("[Skin] skin URL lookup failed for %s\n", player->name.c_str());
return NULL;
}
LOGI("[Skin] downloading skin from %s\n", skinUrl.c_str());
std::vector<unsigned char> skinData;
if (!HttpClient::download(skinUrl, skinData) || skinData.empty()) {
LOGW("[Skin] download failed for %s\n", skinUrl.c_str());
return NULL;
}
// Save to cache
FILE* fp = fopen(cacheFile.c_str(), "wb");
if (fp) {
fwrite(skinData.data(), 1, skinData.size(), fp);
fclose(fp);
LOGI("[Skin] cached skin to %s\n", cacheFile.c_str());
} else {
LOGW("[Skin] failed to write skin cache %s\n", cacheFile.c_str());
}
player->setTextureName("skins/" + player->name + ".png");
return NULL;
}
//@note: doesn't work completely, since it doesn't care about stairs rotation
static bool isJumpable(int tileId) {
return tileId != Tile::fence->id
@@ -43,6 +246,8 @@ static bool isJumpable(int tileId) {
&& (Tile::tiles[tileId] != NULL && Tile::tiles[tileId]->getRenderShape() != Tile::SHAPE_STAIRS);
}
} // anonymous namespace
LocalPlayer::LocalPlayer(Minecraft* minecraft, Level* level, User* user, int dimension, bool isCreative)
: Player(level, isCreative),
minecraft(minecraft),
@@ -58,10 +263,10 @@ LocalPlayer::LocalPlayer(Minecraft* minecraft, Level* level, User* user, int dim
this->dimension = dimension;
_init();
if (user != NULL) {
if (user->name.length() > 0)
//customTextureUrl = "http://s3.amazonaws.com/MinecraftSkins/" + user.name + ".png";
this->name = user->name;
if (user != NULL && !user->name.empty()) {
this->name = user->name;
// Fetch user skin from Mojang servers in the background (avoids blocking the main thread)
new CThread(fetchSkinForPlayer, this);
}
}

View File

@@ -16,7 +16,7 @@ typedef struct TextureData {
TextureData()
: w(0),
h(0),
data(NULL),
data(nullptr),
numBytes(0),
transparent(true),
memoryHandledExternally(false),

View File

@@ -5,6 +5,7 @@
#include "../Options.h"
#include "../../platform/time.h"
#include "../../AppPlatform.h"
#include "../../util/StringUtils.h"
/*static*/ int Textures::textureChanges = 0;
/*static*/ bool Textures::MIPMAP = false;
@@ -64,7 +65,8 @@ TextureId Textures::loadTexture( const std::string& resourceName, bool inTexture
if (it != idMap.end())
return it->second;
TextureData texdata = platform->loadTexture(resourceName, inTextureFolder);
bool isUrl = Util::startsWith(resourceName, "http://") || Util::startsWith(resourceName, "https://");
TextureData texdata = platform->loadTexture(resourceName, isUrl ? false : inTextureFolder);
if (texdata.data)
return assignTexture(resourceName, texdata);
else if (texdata.identifier != InvalidId) {

View File

@@ -51,7 +51,7 @@ EntityRenderDispatcher::EntityRenderDispatcher()
assign( ER_SPIDER_RENDERER, new SpiderRenderer());
assign( ER_TNT_RENDERER, new TntRenderer());
assign( ER_ARROW_RENDERER, new ArrowRenderer());
assign( ER_PLAYER_RENDERER, new PlayerRenderer(new HumanoidModel(), 0));
assign( ER_PLAYER_RENDERER, new PlayerRenderer(new HumanoidModel(0, 0, 64, 64), 0));
assign( ER_THROWNEGG_RENDERER, new ItemSpriteRenderer(Item::egg->getIcon(0)));
assign( ER_SNOWBALL_RENDERER, new ItemSpriteRenderer(Item::snowBall->getIcon(0)));
assign( ER_PAINTING_RENDERER, new PaintingRenderer());

View File

@@ -14,8 +14,8 @@ static const std::string armorFilenames[10] = {
PlayerRenderer::PlayerRenderer( HumanoidModel* humanoidModel, float shadow )
: super(humanoidModel, shadow),
armorParts1(new HumanoidModel(1.0f)),
armorParts2(new HumanoidModel(0.5f))
armorParts1(new HumanoidModel(1.0f, 0, 64, 64)),
armorParts2(new HumanoidModel(0.5f, 0, 64, 64))
{
}