diff --git a/CMakeLists.txt b/CMakeLists.txt index fe56393..fa7d657 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,6 +79,8 @@ file(GLOB SERVER_SOURCES "src/network/command/CommandServer.cpp" "src/platform/CThread.cpp" + "src/platform/HttpClient.cpp" + "src/platform/PngLoader.cpp" "src/platform/time.cpp" "src/platform/input/Controller.cpp" diff --git a/data/images/mob/char.png b/data/images/mob/char.png index 7cfa08a..d507d75 100755 Binary files a/data/images/mob/char.png and b/data/images/mob/char.png differ diff --git a/src/AppPlatform.h b/src/AppPlatform.h index 14a4448..84163b1 100755 --- a/src/AppPlatform.h +++ b/src/AppPlatform.h @@ -71,6 +71,7 @@ public: virtual void saveScreenshot(const std::string& filename, int glWidth, int glHeight) {} virtual TextureData loadTexture(const std::string& filename_, bool textureFolder) { return TextureData(); } + virtual TextureData loadTextureFromMemory(const unsigned char* data, size_t size) { return TextureData(); } virtual void playSound(const std::string& fn, float volume, float pitch) {} diff --git a/src/AppPlatform_glfw.h b/src/AppPlatform_glfw.h index ecfb490..265af68 100755 --- a/src/AppPlatform_glfw.h +++ b/src/AppPlatform_glfw.h @@ -3,6 +3,8 @@ #include "AppPlatform.h" #include "platform/log.h" +#include "platform/HttpClient.h" +#include "platform/PngLoader.h" #include "client/renderer/gles.h" #include "world/level/storage/FolderMethods.h" #include @@ -11,6 +13,7 @@ #include #include #include +#include "util/StringUtils.h" #ifdef _WIN32 #include @@ -55,10 +58,19 @@ public: TextureData loadTexture(const std::string& filename_, bool textureFolder) { + // Support fetching PNG textures via HTTP/HTTPS (for skins, etc) + if (Util::startsWith(filename_, "http://") || Util::startsWith(filename_, "https://")) { + std::vector body; + if (HttpClient::download(filename_, body) && !body.empty()) { + return loadTextureFromMemory(body.data(), body.size()); + } + return TextureData(); + } + TextureData out; std::string filename = textureFolder? "data/images/" + filename_ - : filename_; + : filename_; std::ifstream source(filename.c_str(), std::ios::binary); if (source) { @@ -107,7 +119,11 @@ public: } } - std::string getDateString(int s) { + TextureData loadTextureFromMemory(const unsigned char* data, size_t size) override { + return loadPngFromMemory(data, size); + } + + virtual std::string getDateString(int s) override { time_t tm = s; char mbstr[100]; diff --git a/src/AppPlatform_win32.h b/src/AppPlatform_win32.h index 61c3ab2..c3623b3 100755 --- a/src/AppPlatform_win32.h +++ b/src/AppPlatform_win32.h @@ -3,8 +3,11 @@ #include "AppPlatform.h" #include "platform/log.h" +#include "platform/HttpClient.h" +#include "platform/PngLoader.h" #include "client/renderer/gles.h" #include "world/level/storage/FolderMethods.h" +#include "util/StringUtils.h" #include #include #include @@ -50,10 +53,19 @@ public: TextureData loadTexture(const std::string& filename_, bool textureFolder) { + // Support fetching PNG textures via HTTP/HTTPS (for skins, etc). + if (Util::startsWith(filename_, "http://") || Util::startsWith(filename_, "https://")) { + std::vector body; + if (HttpClient::download(filename_, body) && !body.empty()) { + return loadTextureFromMemory(body.data(), body.size()); + } + return TextureData(); + } + TextureData out; std::string filename = textureFolder? "data/images/" + filename_ - : filename_; + : filename_; std::ifstream source(filename.c_str(), std::ios::binary); if (source) { @@ -102,7 +114,9 @@ public: } } - std::string getDateString(int s) { + TextureData loadTextureFromMemory(const unsigned char* data, size_t size) override { + return loadPngFromMemory(data, size); + } time_t tm = s; char mbstr[100]; diff --git a/src/client/model/HumanoidModel.cpp b/src/client/model/HumanoidModel.cpp index 985019c..4b6d998 100755 --- a/src/client/model/HumanoidModel.cpp +++ b/src/client/model/HumanoidModel.cpp @@ -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); diff --git a/src/client/model/HumanoidModel.h b/src/client/model/HumanoidModel.h index 14dc60e..21edf32 100755 --- a/src/client/model/HumanoidModel.h +++ b/src/client/model/HumanoidModel.h @@ -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); diff --git a/src/client/model/geom/Cube.cpp b/src/client/model/geom/Cube.cpp index e1da77d..bb3c030 100755 --- a/src/client/model/geom/Cube.cpp +++ b/src/client/model/geom/Cube.cpp @@ -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++) diff --git a/src/client/model/geom/Polygon.cpp b/src/client/model/geom/Polygon.cpp index db531f2..bfb1467 100755 --- a/src/client/model/geom/Polygon.cpp +++ b/src/client/model/geom/Polygon.cpp @@ -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, diff --git a/src/client/model/geom/Polygon.h b/src/client/model/geom/Polygon.h index 875b0a6..7731f9c 100755 --- a/src/client/model/geom/Polygon.h +++ b/src/client/model/geom/Polygon.h @@ -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(); diff --git a/src/client/player/LocalPlayer.cpp b/src/client/player/LocalPlayer.cpp index fbc52c7..2990b61 100755 --- a/src/client/player/LocalPlayer.cpp +++ b/src/client/player/LocalPlayer.cpp @@ -18,6 +18,20 @@ #include "../../network/packet/SendInventoryPacket.h" #include "../../network/packet/EntityEventPacket.h" #include "../../network/packet/PlayerActionPacket.h" +#include +#include +#include "../../platform/log.h" +#include "../../platform/HttpClient.h" +#include "../../platform/CThread.h" +#include "../../util/StringUtils.h" + +#if defined(_WIN32) +#include +#else +#include +#include +#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 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 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); } } diff --git a/src/client/renderer/TextureData.h b/src/client/renderer/TextureData.h index ea2a6e8..2c97797 100755 --- a/src/client/renderer/TextureData.h +++ b/src/client/renderer/TextureData.h @@ -16,7 +16,7 @@ typedef struct TextureData { TextureData() : w(0), h(0), - data(NULL), + data(nullptr), numBytes(0), transparent(true), memoryHandledExternally(false), diff --git a/src/client/renderer/Textures.cpp b/src/client/renderer/Textures.cpp index 6be65b9..9fce411 100755 --- a/src/client/renderer/Textures.cpp +++ b/src/client/renderer/Textures.cpp @@ -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) { diff --git a/src/client/renderer/entity/EntityRenderDispatcher.cpp b/src/client/renderer/entity/EntityRenderDispatcher.cpp index 6c0e72e..a3148fc 100755 --- a/src/client/renderer/entity/EntityRenderDispatcher.cpp +++ b/src/client/renderer/entity/EntityRenderDispatcher.cpp @@ -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()); diff --git a/src/client/renderer/entity/PlayerRenderer.cpp b/src/client/renderer/entity/PlayerRenderer.cpp index 590cbc8..b184bcb 100755 --- a/src/client/renderer/entity/PlayerRenderer.cpp +++ b/src/client/renderer/entity/PlayerRenderer.cpp @@ -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)) { } diff --git a/src/platform/HttpClient.cpp b/src/platform/HttpClient.cpp new file mode 100644 index 0000000..a5bd252 --- /dev/null +++ b/src/platform/HttpClient.cpp @@ -0,0 +1,335 @@ +#include "HttpClient.h" +#include "log.h" + +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +#pragma comment(lib, "ws2_32.lib") +#pragma comment(lib, "winhttp.lib") +#else +#include +#include +#include +#include +#include +#endif + +namespace { + +bool startsWith(const std::string& s, const std::string& prefix) { + return s.size() >= prefix.size() && std::equal(prefix.begin(), prefix.end(), s.begin()); +} + +bool parseUrl(const std::string& url, std::string& scheme, std::string& host, int& port, std::string& path); + +#if defined(_WIN32) + +bool downloadHttpsWinHttp(const std::string& url, std::vector& outBody) { + outBody.clear(); + + std::string scheme, host, path; + int port = 0; + if (!parseUrl(url, scheme, host, port, path)) + return false; + + // WinHTTP expects the path to include the leading '/'. + HINTERNET hSession = WinHttpOpen(L"MinecraftPE/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); + if (!hSession) + return false; + + HINTERNET hConnect = WinHttpConnect(hSession, std::wstring(host.begin(), host.end()).c_str(), port, 0); + if (!hConnect) { + WinHttpCloseHandle(hSession); + return false; + } + + HINTERNET hRequest = WinHttpOpenRequest( + hConnect, + L"GET", + std::wstring(path.begin(), path.end()).c_str(), + NULL, + WINHTTP_NO_REFERER, + WINHTTP_DEFAULT_ACCEPT_TYPES, + WINHTTP_FLAG_SECURE + ); + + if (!hRequest) { + WinHttpCloseHandle(hConnect); + WinHttpCloseHandle(hSession); + return false; + } + + DWORD redirectPolicy = WINHTTP_OPTION_REDIRECT_POLICY_ALWAYS; + WinHttpSetOption(hRequest, WINHTTP_OPTION_REDIRECT_POLICY, &redirectPolicy, sizeof(redirectPolicy)); + + BOOL result = WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0); + if (!result) { + WinHttpCloseHandle(hRequest); + WinHttpCloseHandle(hConnect); + WinHttpCloseHandle(hSession); + return false; + } + + result = WinHttpReceiveResponse(hRequest, NULL); + if (!result) { + WinHttpCloseHandle(hRequest); + WinHttpCloseHandle(hConnect); + WinHttpCloseHandle(hSession); + return false; + } + + DWORD bytesAvailable = 0; + while (WinHttpQueryDataAvailable(hRequest, &bytesAvailable) && bytesAvailable > 0) { + std::vector buffer(bytesAvailable); + DWORD bytesRead = 0; + if (!WinHttpReadData(hRequest, buffer.data(), bytesAvailable, &bytesRead) || bytesRead == 0) + break; + outBody.insert(outBody.end(), buffer.begin(), buffer.begin() + bytesRead); + } + + WinHttpCloseHandle(hRequest); + WinHttpCloseHandle(hConnect); + WinHttpCloseHandle(hSession); + + return !outBody.empty(); +} + +#endif + +std::string toLower(const std::string& s) { + std::string out = s; + std::transform(out.begin(), out.end(), out.begin(), ::tolower); + return out; +} + +bool parseUrl(const std::string& url, std::string& scheme, std::string& host, int& port, std::string& path) { + scheme.clear(); + host.clear(); + path.clear(); + port = 0; + + // Very simple URL parser. + // url format: scheme://host[:port]/path + auto pos = url.find("://"); + if (pos == std::string::npos) return false; + scheme = toLower(url.substr(0, pos)); + size_t start = pos + 3; + + size_t slash = url.find('/', start); + std::string hostPort = (slash == std::string::npos) ? url.substr(start) : url.substr(start, slash - start); + path = (slash == std::string::npos) ? "/" : url.substr(slash); + + size_t colon = hostPort.find(':'); + if (colon != std::string::npos) { + host = hostPort.substr(0, colon); + port = atoi(hostPort.c_str() + colon + 1); + } else { + host = hostPort; + } + + if (scheme == "http") { + if (port == 0) port = 80; + } else if (scheme == "https") { + if (port == 0) port = 443; + } + + return !host.empty() && !scheme.empty(); +} + +bool readAll(int sockfd, std::vector& out) { + const int BUF_SIZE = 4096; + unsigned char buffer[BUF_SIZE]; + + while (true) { + int received = recv(sockfd, (char*)buffer, BUF_SIZE, 0); + if (received <= 0) + break; + out.insert(out.end(), buffer, buffer + received); + } + return true; +} + +bool resolveAndConnect(const std::string& host, int port, int& outSock) { +#if defined(_WIN32) + static bool initialized = false; + if (!initialized) { + WSADATA wsaData; + WSAStartup(MAKEWORD(2, 2), &wsaData); + initialized = true; + } +#endif + + struct addrinfo hints; + struct addrinfo* result = NULL; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + + const std::string portStr = std::to_string(port); + if (getaddrinfo(host.c_str(), portStr.c_str(), &hints, &result) != 0) + return false; + + int sock = -1; + for (struct addrinfo* rp = result; rp != NULL; rp = rp->ai_next) { + sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (sock < 0) continue; + if (connect(sock, rp->ai_addr, (int)rp->ai_addrlen) == 0) + break; // success + +#if defined(_WIN32) + closesocket(sock); +#else + close(sock); +#endif + sock = -1; + } + + freeaddrinfo(result); + + if (sock < 0) + return false; + + outSock = sock; + return true; +} + +std::string getHeaderValue(const std::string& headers, const std::string& key) { + std::string lower = toLower(headers); + std::string lowerKey = toLower(key); + size_t pos = lower.find(lowerKey); + if (pos == std::string::npos) return ""; + size_t colon = lower.find(':', pos + lowerKey.size()); + if (colon == std::string::npos) return ""; + size_t start = colon + 1; + while (start < lower.size() && (lower[start] == ' ' || lower[start] == '\t')) start++; + size_t end = lower.find('\r', start); + if (end == std::string::npos) end = lower.find('\n', start); + if (end == std::string::npos) end = lower.size(); + return headers.substr(start, end - start); +} + +bool extractStatusCode(const std::string& headers, int& outStatus) { + size_t pos = headers.find(" "); + if (pos == std::string::npos) return false; + size_t pos2 = headers.find(" ", pos + 1); + if (pos2 == std::string::npos) return false; + std::string code = headers.substr(pos + 1, pos2 - pos - 1); + outStatus = atoi(code.c_str()); + return true; +} + +} // anonymous namespace + +namespace HttpClient { + +bool download(const std::string& url, std::vector& outBody) { + outBody.clear(); + + std::string currentUrl = url; + for (int redirect = 0; redirect < 3; ++redirect) { + std::string scheme, host, path; + int port = 0; + if (!parseUrl(currentUrl, scheme, host, port, path)) { + LOGW("[HttpClient] parseUrl failed for '%s'\n", currentUrl.c_str()); + return false; + } + + if (scheme == "https") { +#if defined(_WIN32) + LOGI("[HttpClient] using WinHTTP for HTTPS URL %s\n", currentUrl.c_str()); + return downloadHttpsWinHttp(currentUrl, outBody); +#else + LOGW("[HttpClient] HTTPS not supported on this platform: %s\n", currentUrl.c_str()); + return false; +#endif + } + + if (scheme != "http") { + LOGW("[HttpClient] unsupported scheme '%s' for URL '%s'\n", scheme.c_str(), currentUrl.c_str()); + return false; + } + + int sock = -1; + if (!resolveAndConnect(host, port, sock)) { + LOGW("[HttpClient] resolve/connect failed for %s:%d\n", host.c_str(), port); + return false; + } + + std::string request = "GET " + path + " HTTP/1.1\r\n"; + request += "Host: " + host + "\r\n"; + request += "User-Agent: MinecraftPE\r\n"; + request += "Connection: close\r\n"; + request += "\r\n"; + + send(sock, request.c_str(), (int)request.size(), 0); + + std::vector raw; + readAll(sock, raw); + +#if defined(_WIN32) + closesocket(sock); +#else + close(sock); +#endif + + if (raw.empty()) { + LOGW("[HttpClient] no response data from %s\n", currentUrl.c_str()); + return false; + } + + // split headers and body + const std::string delim = "\r\n\r\n"; + auto it = std::search(raw.begin(), raw.end(), delim.begin(), delim.end()); + if (it == raw.end()) + return false; + + size_t headerLen = it - raw.begin(); + std::string headers(reinterpret_cast(raw.data()), headerLen); + size_t bodyStart = headerLen + delim.size(); + + int status = 0; + if (!extractStatusCode(headers, status)) + return false; + + if (status == 301 || status == 302 || status == 307 || status == 308) { + std::string location = getHeaderValue(headers, "Location"); + if (location.empty()) { + LOGW("[HttpClient] redirect without Location header for %s\n", currentUrl.c_str()); + return false; + } + LOGI("[HttpClient] redirect %s -> %s\n", currentUrl.c_str(), location.c_str()); + currentUrl = location; + continue; + } + + if (status != 200) { + std::string bodySnippet; + if (!outBody.empty()) { + size_t len = std::min(outBody.size(), (size_t)1024); + bodySnippet.assign(outBody.begin(), outBody.begin() + len); + } + LOGW("[HttpClient] HTTP status %d for %s\n", status, currentUrl.c_str()); + LOGW("[HttpClient] Headers:\n%s\n", headers.c_str()); + LOGW("[HttpClient] Body (up to 1024 bytes):\n%s\n", bodySnippet.c_str()); + return false; + } + + outBody.assign(raw.begin() + bodyStart, raw.end()); + return true; + } + + return false; +} + +} // namespace HttpClient diff --git a/src/platform/HttpClient.h b/src/platform/HttpClient.h new file mode 100644 index 0000000..a0f828e --- /dev/null +++ b/src/platform/HttpClient.h @@ -0,0 +1,16 @@ +#ifndef HTTPCLIENT_H__ +#define HTTPCLIENT_H__ + +#include +#include + +namespace HttpClient { + +/// Download the given URL into "outBody". +/// Returns true if the download completed successfully (HTTP 200) and the body is in outBody. +/// This function supports plain HTTP only (no TLS). It will follow up to 3 redirects. +bool download(const std::string& url, std::vector& outBody); + +} // namespace HttpClient + +#endif /* HTTPCLIENT_H__ */ diff --git a/src/platform/PngLoader.cpp b/src/platform/PngLoader.cpp new file mode 100644 index 0000000..06569fd --- /dev/null +++ b/src/platform/PngLoader.cpp @@ -0,0 +1,94 @@ +#include "PngLoader.h" + +#include +#include + +struct MemoryReader { + const unsigned char* data; + size_t size; + size_t pos; +}; + +static void pngMemoryRead(png_structp pngPtr, png_bytep outBytes, png_size_t byteCountToRead) { + MemoryReader* reader = (MemoryReader*)png_get_io_ptr(pngPtr); + if (!reader) + return; + + if (reader->pos + byteCountToRead > reader->size) { + png_error(pngPtr, "Read past end of buffer"); + return; + } + + memcpy(outBytes, reader->data + reader->pos, byteCountToRead); + reader->pos += byteCountToRead; +} + +TextureData loadPngFromMemory(const unsigned char* data, size_t size) { + TextureData out; + if (!data || size == 0) return out; + + png_structp pngPtr = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + if (!pngPtr) return out; + + png_infop infoPtr = png_create_info_struct(pngPtr); + if (!infoPtr) { + png_destroy_read_struct(&pngPtr, NULL, NULL); + return out; + } + + if (setjmp(png_jmpbuf(pngPtr))) { + png_destroy_read_struct(&pngPtr, &infoPtr, NULL); + return out; + } + + MemoryReader reader; + reader.data = data; + reader.size = size; + reader.pos = 0; + + png_set_read_fn(pngPtr, &reader, pngMemoryRead); + png_read_info(pngPtr, infoPtr); + + // Convert any color type to 8-bit RGBA + if (png_get_color_type(pngPtr, infoPtr) == PNG_COLOR_TYPE_PALETTE) + png_set_palette_to_rgb(pngPtr); + if (png_get_color_type(pngPtr, infoPtr) == PNG_COLOR_TYPE_GRAY && png_get_bit_depth(pngPtr, infoPtr) < 8) + png_set_expand_gray_1_2_4_to_8(pngPtr); + if (png_get_valid(pngPtr, infoPtr, PNG_INFO_tRNS)) + png_set_tRNS_to_alpha(pngPtr); + if (png_get_bit_depth(pngPtr, infoPtr) == 16) + png_set_strip_16(pngPtr); + + // Ensure we always have RGBA (4 bytes per pixel) + // Only add alpha if the image lacks it (e.g., RGB skin files). + png_set_gray_to_rgb(pngPtr); + + // Handle interlaced PNGs properly + int number_passes = png_set_interlace_handling(pngPtr); + + png_read_update_info(pngPtr, infoPtr); + + int colorType = png_get_color_type(pngPtr, infoPtr); + if (colorType == PNG_COLOR_TYPE_RGB) { + png_set_filler(pngPtr, 0xFF, PNG_FILLER_AFTER); + } + + out.w = png_get_image_width(pngPtr, infoPtr); + out.h = png_get_image_height(pngPtr, infoPtr); + + png_bytep* rowPtrs = new png_bytep[out.h]; + out.data = new unsigned char[4 * out.w * out.h]; + out.memoryHandledExternally = false; + + int rowStrideBytes = 4 * out.w; + for (int i = 0; i < out.h; i++) { + rowPtrs[i] = (png_bytep)&out.data[i*rowStrideBytes]; + } + + png_read_image(pngPtr, rowPtrs); + + png_destroy_read_struct(&pngPtr, &infoPtr, NULL); + delete[] rowPtrs; + + return out; +} diff --git a/src/platform/PngLoader.h b/src/platform/PngLoader.h new file mode 100644 index 0000000..cc1d0a4 --- /dev/null +++ b/src/platform/PngLoader.h @@ -0,0 +1,12 @@ +#ifndef PNGLOADER_H__ +#define PNGLOADER_H__ + +#include "../client/renderer/TextureData.h" + +#include + +/// Decode a PNG (from memory) into a TextureData. +/// Returns an empty TextureData on failure. +TextureData loadPngFromMemory(const unsigned char* data, size_t size); + +#endif // PNGLOADER_H__ diff --git a/src/world/entity/Mob.cpp b/src/world/entity/Mob.cpp index 8e7fec6..3b4bc87 100755 --- a/src/world/entity/Mob.cpp +++ b/src/world/entity/Mob.cpp @@ -110,6 +110,11 @@ std::string Mob::getTexture() return textureName; } +void Mob::setTextureName(const std::string& name) +{ + textureName = name; +} + bool Mob::isPickable() { return !removed; diff --git a/src/world/entity/Mob.h b/src/world/entity/Mob.h index f9c901f..efb92a2 100755 --- a/src/world/entity/Mob.h +++ b/src/world/entity/Mob.h @@ -42,6 +42,7 @@ public: virtual void spawnAnim(); virtual std::string getTexture(); + virtual void setTextureName(const std::string& name); virtual bool isAlive(); virtual bool isPickable();