From 97daa795fb87d473f23e8c02ee6d33639af4e565 Mon Sep 17 00:00:00 2001 From: mschiller890 Date: Fri, 13 Mar 2026 23:56:49 +0100 Subject: [PATCH] Added capes (needs improvement) --- src/client/player/LocalPlayer.cpp | 94 +++++++++--- .../renderer/entity/HumanoidMobRenderer.cpp | 142 +++++++++++++++++- .../renderer/entity/HumanoidMobRenderer.h | 4 + src/world/entity/Mob.cpp | 36 +++++ src/world/entity/Mob.h | 22 ++- 5 files changed, 277 insertions(+), 21 deletions(-) diff --git a/src/client/player/LocalPlayer.cpp b/src/client/player/LocalPlayer.cpp index ea510d1..8e76115 100755 --- a/src/client/player/LocalPlayer.cpp +++ b/src/client/player/LocalPlayer.cpp @@ -107,64 +107,74 @@ static std::string extractJsonString(const std::string& json, const std::string& return json.substr(pos, end - pos); } -static std::string getSkinUrlForUsername(const std::string& username) { +static std::string getTextureUrlForUsername(const std::string& username, const std::string& textureKey) { if (username.empty()) { - LOGI("[Skin] username empty\n"); + LOGI("[%s] username empty\n", textureKey.c_str()); return ""; } - LOGI("[Skin] resolving UUID for user '%s'...\n", username.c_str()); + LOGI("[%s] resolving UUID for user '%s'...\n", textureKey.c_str(), username.c_str()); std::vector 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()); + LOGW("[%s] failed to download UUID for %s\n", textureKey.c_str(), 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()); + LOGW("[%s] no UUID found in Mojang response for %s\n", textureKey.c_str(), username.c_str()); return ""; } - LOGI("[Skin] UUID=%s for user %s\n", uuid.c_str(), username.c_str()); + LOGI("[%s] UUID=%s for user %s\n", textureKey.c_str(), 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()); + LOGW("[%s] failed to download profile for UUID %s\n", textureKey.c_str(), 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()); + LOGW("[%s] no value field in profile response for UUID %s\n", textureKey.c_str(), 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()); + + std::string searchKey = "\"" + textureKey + "\""; + size_t texturePos = decoded.find(searchKey); + if (texturePos == std::string::npos) { + LOGW("[%s] no %s entry in decoded profile for UUID %s\n", textureKey.c_str(), textureKey.c_str(), uuid.c_str()); return ""; } - size_t urlPos = decoded.find("\"url\"", skinPos); + size_t urlPos = decoded.find("\"url\"", texturePos); if (urlPos == std::string::npos) { - LOGW("[Skin] no url field under SKIN for UUID %s\n", uuid.c_str()); + LOGW("[%s] no url field under %s for UUID %s\n", textureKey.c_str(), textureKey.c_str(), 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()); + std::string textureUrl = extractJsonString(urlFragment, "url"); + if (textureUrl.empty()) { + LOGW("[%s] failed to parse %s URL for UUID %s\n", textureKey.c_str(), textureKey.c_str(), uuid.c_str()); return ""; } - LOGI("[Skin] skin URL for %s: %s\n", username.c_str(), skinUrl.c_str()); - return skinUrl; + LOGI("[%s] %s URL for %s: %s\n", textureKey.c_str(), textureKey.c_str(), username.c_str(), textureUrl.c_str()); + return textureUrl; +} + +static std::string getSkinUrlForUsername(const std::string& username) { + return getTextureUrlForUsername(username, "SKIN"); +} + +static std::string getCapeUrlForUsername(const std::string& username) { + return getTextureUrlForUsername(username, "CAPE"); } static bool ensureDirectoryExists(const std::string& path) { @@ -235,6 +245,51 @@ static void* fetchSkinForPlayer(void* param) { return NULL; } +static void* fetchCapeForPlayer(void* param) { + LocalPlayer* player = (LocalPlayer*)param; + if (!player) return NULL; + + LOGI("[Cape] starting cape download for %s\n", player->name.c_str()); + + const std::string cacheDir = "data/images/capes"; + if (!ensureDirectoryExists(cacheDir)) { + LOGW("[Cape] failed to create cache directory %s\n", cacheDir.c_str()); + } + + std::string cacheFile = cacheDir + "/" + player->name + ".png"; + if (fileExists(cacheFile)) { + LOGI("[Cape] using cached cape for %s\n", player->name.c_str()); + player->setCapeTextureName("capes/" + player->name + ".png"); + return NULL; + } + + std::string capeUrl = getCapeUrlForUsername(player->name); + if (capeUrl.empty()) { + LOGW("[Cape] cape URL lookup failed for %s\n", player->name.c_str()); + return NULL; + } + + LOGI("[Cape] downloading cape from %s\n", capeUrl.c_str()); + std::vector capeData; + if (!HttpClient::download(capeUrl, capeData) || capeData.empty()) { + LOGW("[Cape] download failed for %s\n", capeUrl.c_str()); + return NULL; + } + + // Save to cache + FILE* fp = fopen(cacheFile.c_str(), "wb"); + if (fp) { + fwrite(capeData.data(), 1, capeData.size(), fp); + fclose(fp); + LOGI("[Cape] cached cape to %s\n", cacheFile.c_str()); + } else { + LOGW("[Cape] failed to write cape cache %s\n", cacheFile.c_str()); + } + + player->setCapeTextureName("capes/" + 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 @@ -265,8 +320,9 @@ LocalPlayer::LocalPlayer(Minecraft* minecraft, Level* level, User* user, int dim if (user != NULL && !user->name.empty()) { this->name = user->name; - // Fetch user skin from Mojang servers in the background (avoids blocking the main thread) + // Fetch user skin and cape from Mojang servers in the background (avoids blocking the main thread) new CThread(fetchSkinForPlayer, this); + new CThread(fetchCapeForPlayer, this); } } diff --git a/src/client/renderer/entity/HumanoidMobRenderer.cpp b/src/client/renderer/entity/HumanoidMobRenderer.cpp index b9efe21..2b49aba 100755 --- a/src/client/renderer/entity/HumanoidMobRenderer.cpp +++ b/src/client/renderer/entity/HumanoidMobRenderer.cpp @@ -2,6 +2,7 @@ #include "EntityRenderDispatcher.h" #include "../ItemInHandRenderer.h" #include "../TileRenderer.h" +#include "../Tesselator.h" #include "../../model/HumanoidModel.h" #include "../../../world/level/tile/Tile.h" #include "../../../world/entity/player/Player.h" @@ -12,7 +13,9 @@ HumanoidMobRenderer::HumanoidMobRenderer(HumanoidModel* humanoidModel, float shadow) : super(humanoidModel, shadow), - humanoidModel(humanoidModel) + humanoidModel(humanoidModel), + lastCapeXRot(0), + lastCapeZRot(0) { } @@ -68,6 +71,143 @@ void HumanoidMobRenderer::additionalRendering(Mob* mob, float a) { entityRenderDispatcher->itemInHandRenderer->renderItem(mob, item); glPopMatrix2(); } + + // Render player cape if available +{ + Player* player = dynamic_cast(mob); + if (player) { + const std::string capeTex = player->getCapeTexture(); + if (!capeTex.empty()) { + + bindTexture(capeTex); + + glPushMatrix2(); + + // Attach to player body + humanoidModel->body.translateTo(1 / 16.0f); + + // Convert model units (pixels) to world units + glScalef2(1.0f / 16.0f, 1.0f / 16.0f, 1.0f / 16.0f); + + // Position cape slightly down and behind the shoulders + glTranslatef2(0.0f, 1.0f, 2.0f); + + // Java-like cape physics (interpolated inertia + body motion) + float pt = a; + + double capeX = player->getCapePrevX() + (player->getCapeX() - player->getCapePrevX()) * pt; + double capeY = player->getCapePrevY() + (player->getCapeY() - player->getCapePrevY()) * pt; + double capeZ = player->getCapePrevZ() + (player->getCapeZ() - player->getCapePrevZ()) * pt; + + double px = player->xo + (player->x - player->xo) * pt; + double py = player->yo + (player->y - player->yo) * pt; + double pz = player->zo + (player->z - player->zo) * pt; + + double dx = capeX - px; + double dy = capeY - py; + double dz = capeZ - pz; + + float bodyYaw = player->yBodyRotO + (player->yBodyRot - player->yBodyRotO) * pt; + + float rad = bodyYaw * Mth::PI / 180.0f; + double sinYaw = Mth::sin(rad); + double cosYaw = -Mth::cos(rad); + + float forward = (float)(dx * sinYaw + dz * cosYaw) * 100.0f; + float sideways = (float)(dx * cosYaw - dz * sinYaw) * 100.0f; + if (forward < 0.0f) forward = 0.0f; + + float lift = (float)dy * 10.0f; + if (lift < -6.0f) lift = -6.0f; + if (lift > 32.0f) lift = 32.0f; + + float walk = + Mth::sin((player->walkAnimPos + player->walkAnimSpeed) * 6.0f) * + 32.0f * + player->walkAnimSpeed; + + float capeXRot = 6.0f + forward / 2.0f + lift + walk; + float capeZRot = sideways / 2.0f; + + // Smooth out jitter by lerping from the previous frame + const float smooth = 0.3f; + capeXRot = lastCapeXRot + (capeXRot - lastCapeXRot) * smooth; + capeZRot = lastCapeZRot + (capeZRot - lastCapeZRot) * smooth; + + lastCapeXRot = capeXRot; + lastCapeZRot = capeZRot; + + glRotatef2(capeXRot, 1.0f, 0.0f, 0.0f); + glRotatef2(capeZRot, 0.0f, 0.0f, 1.0f); + + Tesselator& t = Tesselator::instance; + t.begin(); + + // UV coordinates (64x32 skin layout) + const float u0 = 1.0f / 64.0f; + const float u1 = 11.0f / 64.0f; + const float u2 = 12.0f / 64.0f; + const float u3 = 22.0f / 64.0f; + + const float uL0 = 0.0f / 64.0f; + const float uL1 = 1.0f / 64.0f; + + const float uR0 = 11.0f / 64.0f; + const float uR1 = 12.0f / 64.0f; + + const float v0 = 0.0f / 32.0f; + const float v1 = 1.0f / 32.0f; + + const float vTop = 1.0f / 32.0f; + const float vBottom = 17.0f / 32.0f; + + // Cape size (10x16x1 pixels) + const float halfW = 5.0f; + const float height = 16.0f; + const float depth = 1.0f; + + // Front + t.tex(u0, vTop); t.vertex(-halfW, 0.0f, 0.0f); + t.tex(u1, vTop); t.vertex(halfW, 0.0f, 0.0f); + t.tex(u1, vBottom); t.vertex(halfW, height, 0.0f); + t.tex(u0, vBottom); t.vertex(-halfW, height, 0.0f); + + // Back + t.tex(u2, vTop); t.vertex(halfW, 0.0f, depth); + t.tex(u3, vTop); t.vertex(-halfW, 0.0f, depth); + t.tex(u3, vBottom); t.vertex(-halfW, height, depth); + t.tex(u2, vBottom); t.vertex(halfW, height, depth); + + // Left + t.tex(uL0, vTop); t.vertex(-halfW, 0.0f, depth); + t.tex(uL1, vTop); t.vertex(-halfW, 0.0f, 0.0f); + t.tex(uL1, vBottom); t.vertex(-halfW, height, 0.0f); + t.tex(uL0, vBottom); t.vertex(-halfW, height, depth); + + // Right + t.tex(uR0, vTop); t.vertex(halfW, 0.0f, 0.0f); + t.tex(uR1, vTop); t.vertex(halfW, 0.0f, depth); + t.tex(uR1, vBottom); t.vertex(halfW, height, depth); + t.tex(uR0, vBottom); t.vertex(halfW, height, 0.0f); + + // Top + t.tex(u0, v0); t.vertex(-halfW, 0.0f, depth); + t.tex(u1, v0); t.vertex(halfW, 0.0f, depth); + t.tex(u1, v1); t.vertex(halfW, 0.0f, 0.0f); + t.tex(u0, v1); t.vertex(-halfW, 0.0f, 0.0f); + + // Bottom + t.tex(u2, v0); t.vertex(halfW, height, 0.0f); + t.tex(u3, v0); t.vertex(-halfW, height, 0.0f); + t.tex(u3, v1); t.vertex(-halfW, height, depth); + t.tex(u2, v1); t.vertex(halfW, height, depth); + + t.draw(); + + glPopMatrix2(); + } + } +} } void HumanoidMobRenderer::render( Entity* mob_, float x, float y, float z, float rot, float a ) { diff --git a/src/client/renderer/entity/HumanoidMobRenderer.h b/src/client/renderer/entity/HumanoidMobRenderer.h index 59c6b0c..e10c27c 100755 --- a/src/client/renderer/entity/HumanoidMobRenderer.h +++ b/src/client/renderer/entity/HumanoidMobRenderer.h @@ -21,6 +21,10 @@ protected: private: HumanoidModel* humanoidModel; + + // Last rotation values for cape smoothing (reduces jitter) + float lastCapeXRot; + float lastCapeZRot; }; #endif /*NET_MINECRAFT_CLIENT_RENDERER_ENTITY__HumanoidMobRenderer_H__*/ diff --git a/src/world/entity/Mob.cpp b/src/world/entity/Mob.cpp index 3b4bc87..cc784f0 100755 --- a/src/world/entity/Mob.cpp +++ b/src/world/entity/Mob.cpp @@ -29,6 +29,7 @@ Mob::Mob(Level* level) invulnerableDuration(20), //hasHair(false), textureName("mob/char.png"), + capeTextureName(""), allowAlpha(true), modelName(""), bobStrength(1), @@ -82,6 +83,15 @@ Mob::Mob(Level* level) yRot = (float) (Mth::random() * Mth::PI * 2); this->footSize = 0.5f; + + // Initialize cape inertia positions + xCape = x; + yCape = y; + zCape = z; + + xc = xCape; + yc = yCape; + zc = zCape; } Mob::~Mob() { @@ -115,6 +125,16 @@ void Mob::setTextureName(const std::string& name) textureName = name; } +std::string Mob::getCapeTexture() +{ + return capeTextureName; +} + +void Mob::setCapeTextureName(const std::string& name) +{ + capeTextureName = name; +} + bool Mob::isPickable() { return !removed; @@ -274,6 +294,10 @@ void Mob::superTick() void Mob::tick() { + xc = xCape; + yc = yCape; + zc = zCape; + super::tick(); if (arrowCount > 0) { @@ -378,6 +402,18 @@ void Mob::tick() while (xRot - xRotO >= 180) xRotO += 360; animStep += walkSpeed; + + // Reduce jitter by using a smaller interpolation factor (more lag, smoother motion) + double dxCape = x - xCape; + double dyCape = y - yCape; + double dzCape = z - zCape; + + const double interp = 0.15; // small value for smoother cape motion + const double interpY = 0.12; // extra smoothing on vertical movement + + xCape += dxCape * interp; + yCape += dyCape * interpY; + zCape += dzCape * interp; } void Mob::setSize( float w, float h ) diff --git a/src/world/entity/Mob.h b/src/world/entity/Mob.h index efb92a2..7e90196 100755 --- a/src/world/entity/Mob.h +++ b/src/world/entity/Mob.h @@ -44,9 +44,14 @@ public: virtual std::string getTexture(); virtual void setTextureName(const std::string& name); - virtual bool isAlive(); + // Optional player cape texture (non-null on clients when available) + virtual std::string getCapeTexture(); + virtual void setCapeTextureName(const std::string& name); + + virtual bool isAlive(); virtual bool isPickable(); virtual bool isPushable(); + virtual bool isShootable(); MoveControl* getMoveControl(); @@ -213,7 +218,22 @@ protected: float walkingSpeed; float flyingSpeed; + // Cape inertia positions + double xCape, yCape, zCape; + double xc, yc, zc; + +public: + // Cape position accessors (for renderers) + double getCapeX() const { return xCape; } + double getCapeY() const { return yCape; } + double getCapeZ() const { return zCape; } + + double getCapePrevX() const { return xc; } + double getCapePrevY() const { return yc; } + double getCapePrevZ() const { return zc; } + std::string textureName; + std::string capeTextureName; std::string modelName; int deathScore; float oRun, run;