]> git.lizzy.rs Git - dragonfireclient.git/blobdiff - src/client/tile.cpp
Edited .gitignore properly; fixed armor invulnarability in the server code.
[dragonfireclient.git] / src / client / tile.cpp
index 315c2fa2951336d7bee057472403a1a8ec13f903..d03588b2b727f765143b42ded8755433c20077e4 100644 (file)
@@ -19,25 +19,28 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 
 #include "tile.h"
 
+#include <algorithm>
 #include <ICameraSceneNode.h>
+#include <IrrCompileConfig.h>
 #include "util/string.h"
 #include "util/container.h"
 #include "util/thread.h"
-#include "util/numeric.h"
-#include "irrlichttypes_extrabloated.h"
-#include "debug.h"
 #include "filesys.h"
 #include "settings.h"
 #include "mesh.h"
-#include "log.h"
 #include "gamedef.h"
-#include "strfnd.h"
-#include "util/string.h" // for parseColorString()
+#include "util/strfnd.h"
 #include "imagefilters.h"
 #include "guiscalingfilter.h"
+#include "renderingengine.h"
 
-#ifdef __ANDROID__
+
+#if ENABLE_GLES
+#ifdef _IRR_COMPILE_WITH_OGLES1_
 #include <GLES/gl.h>
+#else
+#include <GLES2/gl2.h>
+#endif
 #endif
 
 /*
@@ -93,13 +96,13 @@ std::string getImagePath(std::string path)
                NULL
        };
        // If there is no extension, add one
-       if (removeStringEnd(path, extensions) == "")
+       if (removeStringEnd(path, extensions).empty())
                path = path + ".png";
        // Check paths until something is found to exist
        const char **ext = extensions;
        do{
                bool r = replace_ext(path, *ext);
-               if (r == false)
+               if (!r)
                        return "";
                if (fs::PathExists(path))
                        return path;
@@ -119,9 +122,14 @@ std::string getImagePath(std::string path)
 
        Utilizes a thread-safe cache.
 */
-std::string getTexturePath(const std::string &filename)
+std::string getTexturePath(const std::string &filename, bool *is_base_pack)
 {
-       std::string fullpath = "";
+       std::string fullpath;
+
+       // This can set a wrong value on cached textures, but is irrelevant because
+       // is_base_pack is only passed when initializing the textures the first time
+       if (is_base_pack)
+               *is_base_pack = false;
        /*
                Check from cache
        */
@@ -132,24 +140,27 @@ std::string getTexturePath(const std::string &filename)
        /*
                Check from texture_path
        */
-       std::string texture_path = g_settings->get("texture_path");
-       if (texture_path != "")
-       {
-               std::string testpath = texture_path + DIR_DELIM + filename;
+       for (const auto &path : getTextureDirs()) {
+               std::string testpath = path + DIR_DELIM;
+               testpath.append(filename);
                // Check all filename extensions. Returns "" if not found.
                fullpath = getImagePath(testpath);
+               if (!fullpath.empty())
+                       break;
        }
 
        /*
                Check from default data directory
        */
-       if (fullpath == "")
+       if (fullpath.empty())
        {
                std::string base_path = porting::path_share + DIR_DELIM + "textures"
                                + DIR_DELIM + "base" + DIR_DELIM + "pack";
                std::string testpath = base_path + DIR_DELIM + filename;
                // Check all filename extensions. Returns "" if not found.
                fullpath = getImagePath(testpath);
+               if (is_base_pack && !fullpath.empty())
+                       *is_base_pack = true;
        }
 
        // Add to cache (also an empty result is cached)
@@ -191,14 +202,12 @@ class SourceImageCache
 {
 public:
        ~SourceImageCache() {
-               for (std::map<std::string, video::IImage*>::iterator iter = m_images.begin();
-                               iter != m_images.end(); iter++) {
-                       iter->second->drop();
+               for (auto &m_image : m_images) {
+                       m_image.second->drop();
                }
                m_images.clear();
        }
-       void insert(const std::string &name, video::IImage *img,
-                       bool prefer_local, video::IVideoDriver *driver)
+       void insert(const std::string &name, video::IImage *img, bool prefer_local)
        {
                assert(img); // Pre-condition
                // Remove old image
@@ -213,10 +222,13 @@ class SourceImageCache
                bool need_to_grab = true;
 
                // Try to use local texture instead if asked to
-               if (prefer_local){
-                       std::string path = getTexturePath(name);
-                       if (path != ""){
-                               video::IImage *img2 = driver->createImageFromFile(path.c_str());
+               if (prefer_local) {
+                       bool is_base_pack;
+                       std::string path = getTexturePath(name, &is_base_pack);
+                       // Ignore base pack
+                       if (!path.empty() && !is_base_pack) {
+                               video::IImage *img2 = RenderingEngine::get_video_driver()->
+                                       createImageFromFile(path.c_str());
                                if (img2){
                                        toadd = img2;
                                        need_to_grab = false;
@@ -224,10 +236,6 @@ class SourceImageCache
                        }
                }
 
-               // Apply the "clean transparent" filter, if configured.
-               if (g_settings->getBool("texture_clean_transparent"))
-                       imageCleanTransparent(toadd, 127);
-
                if (need_to_grab)
                        toadd->grab();
                m_images[name] = toadd;
@@ -241,7 +249,7 @@ class SourceImageCache
                return NULL;
        }
        // Primarily fetches from cache, secondarily tries to read from filesystem
-       video::IImage* getOrLoad(const std::string &name, IrrlichtDevice *device)
+       video::IImage *getOrLoad(const std::string &name)
        {
                std::map<std::string, video::IImage*>::iterator n;
                n = m_images.find(name);
@@ -249,9 +257,9 @@ class SourceImageCache
                        n->second->grab(); // Grab for caller
                        return n->second;
                }
-               video::IVideoDriver* driver = device->getVideoDriver();
+               video::IVideoDriver *driver = RenderingEngine::get_video_driver();
                std::string path = getTexturePath(name);
-               if (path == ""){
+               if (path.empty()) {
                        infostream<<"SourceImageCache::getOrLoad(): No path found for \""
                                        <<name<<"\""<<std::endl;
                        return NULL;
@@ -277,7 +285,7 @@ class SourceImageCache
 class TextureSource : public IWritableTextureSource
 {
 public:
-       TextureSource(IrrlichtDevice *device);
+       TextureSource();
        virtual ~TextureSource();
 
        /*
@@ -334,7 +342,7 @@ class TextureSource : public IWritableTextureSource
        */
        video::ITexture* getTexture(u32 id);
 
-       video::ITexture* getTexture(const std::string &name, u32 *id);
+       video::ITexture* getTexture(const std::string &name, u32 *id = NULL);
 
        /*
                Get a texture specifically intended for mesh
@@ -344,11 +352,7 @@ class TextureSource : public IWritableTextureSource
        */
        video::ITexture* getTextureForMesh(const std::string &name, u32 *id);
 
-       // Returns a pointer to the irrlicht device
-       virtual IrrlichtDevice* getDevice()
-       {
-               return m_device;
-       }
+       virtual Palette* getPalette(const std::string &name);
 
        bool isKnownSourceImage(const std::string &name)
        {
@@ -357,7 +361,7 @@ class TextureSource : public IWritableTextureSource
                if (cache_found)
                        return is_known;
                // Not found in cache; find out if a local file exists
-               is_known = (getTexturePath(name) != "");
+               is_known = (!getTexturePath(name).empty());
                m_source_image_existence.set(name, is_known);
                return is_known;
        }
@@ -374,24 +378,14 @@ class TextureSource : public IWritableTextureSource
        // Shall be called from the main thread.
        void rebuildImagesAndTextures();
 
-       // Render a mesh to a texture.
-       // Returns NULL if render-to-texture failed.
-       // Shall be called from the main thread.
-       video::ITexture* generateTextureFromMesh(
-                       const TextureFromMeshParams &params);
-
-       // Generates an image from a full string like
-       // "stone.png^mineral_coal.png^[crack:1:0".
-       // Shall be called from the main thread.
-       video::IImage* generateImage(const std::string &name);
-
        video::ITexture* getNormalTexture(const std::string &name);
+       video::SColor getTextureAverageColor(const std::string &name);
+       video::ITexture *getShaderFlagsTexture(bool normamap_present);
+
 private:
 
        // The id of the thread that is allowed to use irrlicht directly
-       threadid_t m_main_thread;
-       // The irrlicht device
-       IrrlichtDevice *m_device;
+       std::thread::id m_main_thread;
 
        // Cache of source images
        // This should be only accessed from the main thread
@@ -404,6 +398,13 @@ class TextureSource : public IWritableTextureSource
        // if baseimg is NULL, it is created. Otherwise stuff is made on it.
        bool generateImagePart(std::string part_of_name, video::IImage *& baseimg);
 
+       /*! Generates an image from a full string like
+        * "stone.png^mineral_coal.png^[crack:1:0".
+        * Shall be called from the main thread.
+        * The returned Image should be dropped.
+        */
+       video::IImage* generateImage(const std::string &name);
+
        // Thread-safe cache of what source images are known (true = known)
        MutexedMap<std::string, bool> m_source_image_existence;
 
@@ -413,7 +414,7 @@ class TextureSource : public IWritableTextureSource
        // Maps a texture name to an index in the former.
        std::map<std::string, u32> m_name_to_id;
        // The two former containers are behind this mutex
-       JMutex m_textureinfo_cache_mutex;
+       std::mutex m_textureinfo_cache_mutex;
 
        // Queued texture fetches (to be processed by the main thread)
        RequestQueue<std::string, u32, u8, u8> m_get_texture_queue;
@@ -422,26 +423,26 @@ class TextureSource : public IWritableTextureSource
        // but can't be deleted because the ITexture* might still be used
        std::vector<video::ITexture*> m_texture_trash;
 
+       // Maps image file names to loaded palettes.
+       std::unordered_map<std::string, Palette> m_palettes;
+
        // Cached settings needed for making textures from meshes
        bool m_setting_trilinear_filter;
        bool m_setting_bilinear_filter;
        bool m_setting_anisotropic_filter;
 };
 
-IWritableTextureSource* createTextureSource(IrrlichtDevice *device)
+IWritableTextureSource *createTextureSource()
 {
-       return new TextureSource(device);
+       return new TextureSource();
 }
 
-TextureSource::TextureSource(IrrlichtDevice *device):
-               m_device(device)
+TextureSource::TextureSource()
 {
-       assert(m_device); // Pre-condition
-
-       m_main_thread = get_current_thread_id();
+       m_main_thread = std::this_thread::get_id();
 
        // Add a NULL TextureInfo as the first index, named ""
-       m_textureinfo_cache.push_back(TextureInfo(""));
+       m_textureinfo_cache.emplace_back("");
        m_name_to_id[""] = 0;
 
        // Cache some settings
@@ -454,31 +455,24 @@ TextureSource::TextureSource(IrrlichtDevice *device):
 
 TextureSource::~TextureSource()
 {
-       video::IVideoDriver* driver = m_device->getVideoDriver();
+       video::IVideoDriver *driver = RenderingEngine::get_video_driver();
 
        unsigned int textures_before = driver->getTextureCount();
 
-       for (std::vector<TextureInfo>::iterator iter =
-                       m_textureinfo_cache.begin();
-                       iter != m_textureinfo_cache.end(); iter++)
-       {
+       for (const auto &iter : m_textureinfo_cache) {
                //cleanup texture
-               if (iter->texture)
-                       driver->removeTexture(iter->texture);
+               if (iter.texture)
+                       driver->removeTexture(iter.texture);
        }
        m_textureinfo_cache.clear();
 
-       for (std::vector<video::ITexture*>::iterator iter =
-                       m_texture_trash.begin(); iter != m_texture_trash.end();
-                       iter++) {
-               video::ITexture *t = *iter;
-
+       for (auto t : m_texture_trash) {
                //cleanup trashed texture
                driver->removeTexture(t);
        }
 
-       infostream << "~TextureSource() "<< textures_before << "/"
-                       << driver->getTextureCount() << std::endl;
+       infostream << "~TextureSource() before cleanup: "<< textures_before
+                       << " after: " << driver->getTextureCount() << std::endl;
 }
 
 u32 TextureSource::getTextureId(const std::string &name)
@@ -489,7 +483,7 @@ u32 TextureSource::getTextureId(const std::string &name)
                /*
                        See if texture already exists
                */
-               JMutexAutoLock lock(m_textureinfo_cache_mutex);
+               MutexAutoLock lock(m_textureinfo_cache_mutex);
                std::map<std::string, u32>::iterator n;
                n = m_name_to_id.find(name);
                if (n != m_name_to_id.end())
@@ -501,43 +495,35 @@ u32 TextureSource::getTextureId(const std::string &name)
        /*
                Get texture
        */
-       if (get_current_thread_id() == m_main_thread)
-       {
+       if (std::this_thread::get_id() == m_main_thread) {
                return generateTexture(name);
        }
-       else
-       {
-               infostream<<"getTextureId(): Queued: name=\""<<name<<"\""<<std::endl;
 
-               // We're gonna ask the result to be put into here
-               static ResultQueue<std::string, u32, u8, u8> result_queue;
 
-               // Throw a request in
-               m_get_texture_queue.add(name, 0, 0, &result_queue);
+       infostream<<"getTextureId(): Queued: name=\""<<name<<"\""<<std::endl;
 
-               /*infostream<<"Waiting for texture from main thread, name=\""
-                               <<name<<"\""<<std::endl;*/
+       // We're gonna ask the result to be put into here
+       static ResultQueue<std::string, u32, u8, u8> result_queue;
 
-               try
-               {
-                       while(true) {
-                               // Wait result for a second
-                               GetResult<std::string, u32, u8, u8>
-                                       result = result_queue.pop_front(1000);
+       // Throw a request in
+       m_get_texture_queue.add(name, 0, 0, &result_queue);
 
-                               if (result.key == name) {
-                                       return result.item;
-                               }
+       try {
+               while(true) {
+                       // Wait result for a second
+                       GetResult<std::string, u32, u8, u8>
+                               result = result_queue.pop_front(1000);
+
+                       if (result.key == name) {
+                               return result.item;
                        }
                }
-               catch(ItemNotFoundException &e)
-               {
-                       errorstream<<"Waiting for texture " << name << " timed out."<<std::endl;
-                       return 0;
-               }
+       } catch(ItemNotFoundException &e) {
+               errorstream << "Waiting for texture " << name << " timed out." << std::endl;
+               return 0;
        }
 
-       infostream<<"getTextureId(): Failed"<<std::endl;
+       infostream << "getTextureId(): Failed" << std::endl;
 
        return 0;
 }
@@ -552,10 +538,16 @@ static void blit_with_alpha(video::IImage *src, video::IImage *dst,
 static void blit_with_alpha_overlay(video::IImage *src, video::IImage *dst,
                v2s32 src_pos, v2s32 dst_pos, v2u32 size);
 
-// Like blit_with_alpha overlay, but uses an int to calculate the ratio
-// and modifies any destination pixels that are not fully transparent
-static void blit_with_interpolate_overlay(video::IImage *src, video::IImage *dst,
-               v2s32 src_pos, v2s32 dst_pos, v2u32 size, int ratio);
+// Apply a color to an image.  Uses an int (0-255) to calculate the ratio.
+// If the ratio is 255 or -1 and keep_alpha is true, then it multiples the
+// color alpha with the destination alpha.
+// Otherwise, any pixels that are not fully transparent get the color alpha.
+static void apply_colorize(video::IImage *dst, v2u32 dst_pos, v2u32 size,
+               const video::SColor &color, int ratio, bool keep_alpha);
+
+// paint a texture using the given color
+static void apply_multiplication(video::IImage *dst, v2u32 dst_pos, v2u32 size,
+               const video::SColor &color);
 
 // Apply a mask to an image
 static void apply_mask(video::IImage *mask, video::IImage *dst,
@@ -564,7 +556,7 @@ static void apply_mask(video::IImage *mask, video::IImage *dst,
 // Draw or overlay a crack
 static void draw_crack(video::IImage *crack, video::IImage *dst,
                bool use_overlay, s32 frame_count, s32 progression,
-               video::IVideoDriver *driver);
+               video::IVideoDriver *driver, u8 tiles = 1);
 
 // Brighten image
 void brighten(video::IImage *image);
@@ -583,7 +575,7 @@ u32 TextureSource::generateTexture(const std::string &name)
        //infostream << "generateTexture(): name=\"" << name << "\"" << std::endl;
 
        // Empty name means texture 0
-       if (name == "") {
+       if (name.empty()) {
                infostream<<"generateTexture(): name is empty"<<std::endl;
                return 0;
        }
@@ -592,7 +584,7 @@ u32 TextureSource::generateTexture(const std::string &name)
                /*
                        See if texture already exists
                */
-               JMutexAutoLock lock(m_textureinfo_cache_mutex);
+               MutexAutoLock lock(m_textureinfo_cache_mutex);
                std::map<std::string, u32>::iterator n;
                n = m_name_to_id.find(name);
                if (n != m_name_to_id.end()) {
@@ -603,13 +595,13 @@ u32 TextureSource::generateTexture(const std::string &name)
        /*
                Calling only allowed from main thread
        */
-       if (get_current_thread_id() != m_main_thread) {
+       if (std::this_thread::get_id() != m_main_thread) {
                errorstream<<"TextureSource::generateTexture() "
                                "called not from main thread"<<std::endl;
                return 0;
        }
 
-       video::IVideoDriver *driver = m_device->getVideoDriver();
+       video::IVideoDriver *driver = RenderingEngine::get_video_driver();
        sanity_check(driver);
 
        video::IImage *img = generateImage(name);
@@ -617,7 +609,7 @@ u32 TextureSource::generateTexture(const std::string &name)
        video::ITexture *tex = NULL;
 
        if (img != NULL) {
-#ifdef __ANDROID__
+#if ENABLE_GLES
                img = Align2Npot2(img, driver);
 #endif
                // Create texture from resulting image
@@ -630,7 +622,7 @@ u32 TextureSource::generateTexture(const std::string &name)
                Add texture to caches (add NULL textures too)
        */
 
-       JMutexAutoLock lock(m_textureinfo_cache_mutex);
+       MutexAutoLock lock(m_textureinfo_cache_mutex);
 
        u32 id = m_textureinfo_cache.size();
        TextureInfo ti(name, tex);
@@ -642,7 +634,7 @@ u32 TextureSource::generateTexture(const std::string &name)
 
 std::string TextureSource::getTextureName(u32 id)
 {
-       JMutexAutoLock lock(m_textureinfo_cache_mutex);
+       MutexAutoLock lock(m_textureinfo_cache_mutex);
 
        if (id >= m_textureinfo_cache.size())
        {
@@ -657,7 +649,7 @@ std::string TextureSource::getTextureName(u32 id)
 
 video::ITexture* TextureSource::getTexture(u32 id)
 {
-       JMutexAutoLock lock(m_textureinfo_cache_mutex);
+       MutexAutoLock lock(m_textureinfo_cache_mutex);
 
        if (id >= m_textureinfo_cache.size())
                return NULL;
@@ -676,7 +668,69 @@ video::ITexture* TextureSource::getTexture(const std::string &name, u32 *id)
 
 video::ITexture* TextureSource::getTextureForMesh(const std::string &name, u32 *id)
 {
-       return getTexture(name + "^[autoupscaleformesh", id);
+       static thread_local bool filter_needed =
+               g_settings->getBool("texture_clean_transparent") ||
+               ((m_setting_trilinear_filter || m_setting_bilinear_filter) &&
+               g_settings->getS32("texture_min_size") > 1);
+       // Avoid duplicating texture if it won't actually change
+       if (filter_needed)
+               return getTexture(name + "^[applyfiltersformesh", id);
+       return getTexture(name, id);
+}
+
+Palette* TextureSource::getPalette(const std::string &name)
+{
+       // Only the main thread may load images
+       sanity_check(std::this_thread::get_id() == m_main_thread);
+
+       if (name.empty())
+               return NULL;
+
+       auto it = m_palettes.find(name);
+       if (it == m_palettes.end()) {
+               // Create palette
+               video::IImage *img = generateImage(name);
+               if (!img) {
+                       warningstream << "TextureSource::getPalette(): palette \"" << name
+                               << "\" could not be loaded." << std::endl;
+                       return NULL;
+               }
+               Palette new_palette;
+               u32 w = img->getDimension().Width;
+               u32 h = img->getDimension().Height;
+               // Real area of the image
+               u32 area = h * w;
+               if (area == 0)
+                       return NULL;
+               if (area > 256) {
+                       warningstream << "TextureSource::getPalette(): the specified"
+                               << " palette image \"" << name << "\" is larger than 256"
+                               << " pixels, using the first 256." << std::endl;
+                       area = 256;
+               } else if (256 % area != 0)
+                       warningstream << "TextureSource::getPalette(): the "
+                               << "specified palette image \"" << name << "\" does not "
+                               << "contain power of two pixels." << std::endl;
+               // We stretch the palette so it will fit 256 values
+               // This many param2 values will have the same color
+               u32 step = 256 / area;
+               // For each pixel in the image
+               for (u32 i = 0; i < area; i++) {
+                       video::SColor c = img->getPixel(i % w, i / w);
+                       // Fill in palette with 'step' colors
+                       for (u32 j = 0; j < step; j++)
+                               new_palette.push_back(c);
+               }
+               img->drop();
+               // Fill in remaining elements
+               while (new_palette.size() < 256)
+                       new_palette.emplace_back(0xFFFFFFFF);
+               m_palettes[name] = new_palette;
+               it = m_palettes.find(name);
+       }
+       if (it != m_palettes.end())
+               return &((*it).second);
+       return NULL;
 }
 
 void TextureSource::processQueue()
@@ -703,253 +757,167 @@ void TextureSource::insertSourceImage(const std::string &name, video::IImage *im
 {
        //infostream<<"TextureSource::insertSourceImage(): name="<<name<<std::endl;
 
-       sanity_check(get_current_thread_id() == m_main_thread);
+       sanity_check(std::this_thread::get_id() == m_main_thread);
 
-       m_sourcecache.insert(name, img, true, m_device->getVideoDriver());
+       m_sourcecache.insert(name, img, true);
        m_source_image_existence.set(name, true);
 }
 
 void TextureSource::rebuildImagesAndTextures()
 {
-       JMutexAutoLock lock(m_textureinfo_cache_mutex);
+       MutexAutoLock lock(m_textureinfo_cache_mutex);
 
-       video::IVideoDriver* driver = m_device->getVideoDriver();
+       video::IVideoDriver *driver = RenderingEngine::get_video_driver();
        sanity_check(driver);
 
+       infostream << "TextureSource: recreating " << m_textureinfo_cache.size()
+               << " textures" << std::endl;
+
        // Recreate textures
-       for (u32 i=0; i<m_textureinfo_cache.size(); i++){
-               TextureInfo *ti = &m_textureinfo_cache[i];
-               video::IImage *img = generateImage(ti->name);
-#ifdef __ANDROID__
+       for (TextureInfo &ti : m_textureinfo_cache) {
+               video::IImage *img = generateImage(ti.name);
+#if ENABLE_GLES
                img = Align2Npot2(img, driver);
-               sanity_check(img->getDimension().Height == npot2(img->getDimension().Height));
-               sanity_check(img->getDimension().Width == npot2(img->getDimension().Width));
 #endif
                // Create texture from resulting image
                video::ITexture *t = NULL;
                if (img) {
-                       t = driver->addTexture(ti->name.c_str(), img);
-                       guiScalingCache(io::path(ti->name.c_str()), driver, img);
+                       t = driver->addTexture(ti.name.c_str(), img);
+                       guiScalingCache(io::path(ti.name.c_str()), driver, img);
                        img->drop();
                }
-               video::ITexture *t_old = ti->texture;
+               video::ITexture *t_old = ti.texture;
                // Replace texture
-               ti->texture = t;
+               ti.texture = t;
 
                if (t_old)
                        m_texture_trash.push_back(t_old);
        }
 }
 
-video::ITexture* TextureSource::generateTextureFromMesh(
-               const TextureFromMeshParams &params)
+inline static void applyShadeFactor(video::SColor &color, u32 factor)
 {
-       video::IVideoDriver *driver = m_device->getVideoDriver();
-       sanity_check(driver);
-
-#ifdef __ANDROID__
-       const GLubyte* renderstr = glGetString(GL_RENDERER);
-       std::string renderer((char*) renderstr);
-
-       // use no render to texture hack
-       if (
-               (renderer.find("Adreno") != std::string::npos) ||
-               (renderer.find("Mali") != std::string::npos) ||
-               (renderer.find("Immersion") != std::string::npos) ||
-               (renderer.find("Tegra") != std::string::npos) ||
-               g_settings->getBool("inventory_image_hack")
-               ) {
-               // Get a scene manager
-               scene::ISceneManager *smgr_main = m_device->getSceneManager();
-               sanity_check(smgr_main);
-               scene::ISceneManager *smgr = smgr_main->createNewSceneManager();
-               sanity_check(smgr);
-
-               const float scaling = 0.2;
-
-               scene::IMeshSceneNode* meshnode =
-                               smgr->addMeshSceneNode(params.mesh, NULL,
-                                               -1, v3f(0,0,0), v3f(0,0,0),
-                                               v3f(1.0 * scaling,1.0 * scaling,1.0 * scaling), true);
-               meshnode->setMaterialFlag(video::EMF_LIGHTING, true);
-               meshnode->setMaterialFlag(video::EMF_ANTI_ALIASING, true);
-               meshnode->setMaterialFlag(video::EMF_TRILINEAR_FILTER, m_setting_trilinear_filter);
-               meshnode->setMaterialFlag(video::EMF_BILINEAR_FILTER, m_setting_bilinear_filter);
-               meshnode->setMaterialFlag(video::EMF_ANISOTROPIC_FILTER, m_setting_anisotropic_filter);
-
-               scene::ICameraSceneNode* camera = smgr->addCameraSceneNode(0,
-                               params.camera_position, params.camera_lookat);
-               // second parameter of setProjectionMatrix (isOrthogonal) is ignored
-               camera->setProjectionMatrix(params.camera_projection_matrix, false);
-
-               smgr->setAmbientLight(params.ambient_light);
-               smgr->addLightSceneNode(0,
-                               params.light_position,
-                               params.light_color,
-                               params.light_radius*scaling);
-
-               core::dimension2d<u32> screen = driver->getScreenSize();
-
-               // Render scene
-               driver->beginScene(true, true, video::SColor(0,0,0,0));
-               driver->clearZBuffer();
-               smgr->drawAll();
-
-               core::dimension2d<u32> partsize(screen.Width * scaling,screen.Height * scaling);
-
-               irr::video::IImage* rawImage =
-                               driver->createImage(irr::video::ECF_A8R8G8B8, partsize);
-
-               u8* pixels = static_cast<u8*>(rawImage->lock());
-               if (!pixels)
-               {
-                       rawImage->drop();
-                       return NULL;
-               }
-
-               core::rect<s32> source(
-                               screen.Width /2 - (screen.Width  * (scaling / 2)),
-                               screen.Height/2 - (screen.Height * (scaling / 2)),
-                               screen.Width /2 + (screen.Width  * (scaling / 2)),
-                               screen.Height/2 + (screen.Height * (scaling / 2))
-                       );
-
-               glReadPixels(source.UpperLeftCorner.X, source.UpperLeftCorner.Y,
-                               partsize.Width, partsize.Height, GL_RGBA,
-                               GL_UNSIGNED_BYTE, pixels);
-
-               driver->endScene();
-
-               // Drop scene manager
-               smgr->drop();
-
-               unsigned int pixelcount = partsize.Width*partsize.Height;
-
-               u8* runptr = pixels;
-               for (unsigned int i=0; i < pixelcount; i++) {
-
-                       u8 B = *runptr;
-                       u8 G = *(runptr+1);
-                       u8 R = *(runptr+2);
-                       u8 A = *(runptr+3);
-
-                       //BGRA -> RGBA
-                       *runptr = R;
-                       runptr ++;
-                       *runptr = G;
-                       runptr ++;
-                       *runptr = B;
-                       runptr ++;
-                       *runptr = A;
-                       runptr ++;
-               }
-
-               video::IImage* inventory_image =
-                               driver->createImage(irr::video::ECF_A8R8G8B8, params.dim);
-
-               rawImage->copyToScaling(inventory_image);
-               rawImage->drop();
-
-               guiScalingCache(io::path(params.rtt_texture_name.c_str()), driver, inventory_image);
-
-               video::ITexture *rtt = driver->addTexture(params.rtt_texture_name.c_str(), inventory_image);
-               inventory_image->drop();
+       u32 f = core::clamp<u32>(factor, 0, 256);
+       color.setRed(color.getRed() * f / 256);
+       color.setGreen(color.getGreen() * f / 256);
+       color.setBlue(color.getBlue() * f / 256);
+}
 
-               if (rtt == NULL) {
-                       errorstream << "TextureSource::generateTextureFromMesh(): failed to recreate texture from image: " << params.rtt_texture_name << std::endl;
-                       return NULL;
+static video::IImage *createInventoryCubeImage(
+       video::IImage *top, video::IImage *left, video::IImage *right)
+{
+       core::dimension2du size_top = top->getDimension();
+       core::dimension2du size_left = left->getDimension();
+       core::dimension2du size_right = right->getDimension();
+
+       u32 size = npot2(std::max({
+                       size_top.Width, size_top.Height,
+                       size_left.Width, size_left.Height,
+                       size_right.Width, size_right.Height,
+       }));
+
+       // It must be divisible by 4, to let everything work correctly.
+       // But it is a power of 2, so being at least 4 is the same.
+       // And the resulting texture should't be too large as well.
+       size = core::clamp<u32>(size, 4, 64);
+
+       // With such parameters, the cube fits exactly, touching each image line
+       // from `0` to `cube_size - 1`. (Note that division is exact here).
+       u32 cube_size = 9 * size;
+       u32 offset = size / 2;
+
+       video::IVideoDriver *driver = RenderingEngine::get_video_driver();
+
+       auto lock_image = [size, driver] (video::IImage *&image) -> const u32 * {
+               image->grab();
+               core::dimension2du dim = image->getDimension();
+               video::ECOLOR_FORMAT format = image->getColorFormat();
+               if (dim.Width != size || dim.Height != size || format != video::ECF_A8R8G8B8) {
+                       video::IImage *scaled = driver->createImage(video::ECF_A8R8G8B8, {size, size});
+                       image->copyToScaling(scaled);
+                       image->drop();
+                       image = scaled;
                }
+               sanity_check(image->getPitch() == 4 * size);
+               return reinterpret_cast<u32 *>(image->lock());
+       };
+       auto free_image = [] (video::IImage *image) -> void {
+               image->unlock();
+               image->drop();
+       };
 
-               driver->makeColorKeyTexture(rtt, v2s32(0,0));
-
-               if (params.delete_texture_on_shutdown)
-                       m_texture_trash.push_back(rtt);
-
-               return rtt;
-       }
-#endif
-
-       if (driver->queryFeature(video::EVDF_RENDER_TO_TARGET) == false)
-       {
-               static bool warned = false;
-               if (!warned)
-               {
-                       errorstream<<"TextureSource::generateTextureFromMesh(): "
-                               <<"EVDF_RENDER_TO_TARGET not supported."<<std::endl;
-                       warned = true;
+       video::IImage *result = driver->createImage(video::ECF_A8R8G8B8, {cube_size, cube_size});
+       sanity_check(result->getPitch() == 4 * cube_size);
+       result->fill(video::SColor(0x00000000u));
+       u32 *target = reinterpret_cast<u32 *>(result->lock());
+
+       // Draws single cube face
+       // `shade_factor` is face brightness, in range [0.0, 1.0]
+       // (xu, xv, x1; yu, yv, y1) form coordinate transformation matrix
+       // `offsets` list pixels to be drawn for single source pixel
+       auto draw_image = [=] (video::IImage *image, float shade_factor,
+                       s16 xu, s16 xv, s16 x1,
+                       s16 yu, s16 yv, s16 y1,
+                       std::initializer_list<v2s16> offsets) -> void {
+               u32 brightness = core::clamp<u32>(256 * shade_factor, 0, 256);
+               const u32 *source = lock_image(image);
+               for (u16 v = 0; v < size; v++) {
+                       for (u16 u = 0; u < size; u++) {
+                               video::SColor pixel(*source);
+                               applyShadeFactor(pixel, brightness);
+                               s16 x = xu * u + xv * v + x1;
+                               s16 y = yu * u + yv * v + y1;
+                               for (const auto &off : offsets)
+                                       target[(y + off.Y) * cube_size + (x + off.X) + offset] = pixel.color;
+                               source++;
+                       }
                }
-               return NULL;
-       }
-
-       // Create render target texture
-       video::ITexture *rtt = driver->addRenderTargetTexture(
-                       params.dim, params.rtt_texture_name.c_str(),
-                       video::ECF_A8R8G8B8);
-       if (rtt == NULL)
-       {
-               errorstream<<"TextureSource::generateTextureFromMesh(): "
-                       <<"addRenderTargetTexture returned NULL."<<std::endl;
-               return NULL;
-       }
-
-       // Set render target
-       if (!driver->setRenderTarget(rtt, false, true, video::SColor(0,0,0,0))) {
-               driver->removeTexture(rtt);
-               errorstream<<"TextureSource::generateTextureFromMesh(): "
-                       <<"failed to set render target"<<std::endl;
-               return NULL;
-       }
+               free_image(image);
+       };
 
-       // Get a scene manager
-       scene::ISceneManager *smgr_main = m_device->getSceneManager();
-       assert(smgr_main);
-       scene::ISceneManager *smgr = smgr_main->createNewSceneManager();
-       assert(smgr);
-
-       scene::IMeshSceneNode* meshnode =
-                       smgr->addMeshSceneNode(params.mesh, NULL,
-                                       -1, v3f(0,0,0), v3f(0,0,0), v3f(1,1,1), true);
-       meshnode->setMaterialFlag(video::EMF_LIGHTING, true);
-       meshnode->setMaterialFlag(video::EMF_ANTI_ALIASING, true);
-       meshnode->setMaterialFlag(video::EMF_TRILINEAR_FILTER, m_setting_trilinear_filter);
-       meshnode->setMaterialFlag(video::EMF_BILINEAR_FILTER, m_setting_bilinear_filter);
-       meshnode->setMaterialFlag(video::EMF_ANISOTROPIC_FILTER, m_setting_anisotropic_filter);
-
-       scene::ICameraSceneNode* camera = smgr->addCameraSceneNode(0,
-                       params.camera_position, params.camera_lookat);
-       // second parameter of setProjectionMatrix (isOrthogonal) is ignored
-       camera->setProjectionMatrix(params.camera_projection_matrix, false);
-
-       smgr->setAmbientLight(params.ambient_light);
-       smgr->addLightSceneNode(0,
-                       params.light_position,
-                       params.light_color,
-                       params.light_radius);
-
-       // Render scene
-       driver->beginScene(true, true, video::SColor(0,0,0,0));
-       smgr->drawAll();
-       driver->endScene();
-
-       // Drop scene manager
-       smgr->drop();
-
-       // Unset render target
-       driver->setRenderTarget(0, false, true, 0);
-
-       if (params.delete_texture_on_shutdown)
-               m_texture_trash.push_back(rtt);
-
-       return rtt;
+       draw_image(top, 1.000000f,
+                       4, -4, 4 * (size - 1),
+                       2, 2, 0,
+                       {
+                                               {2, 0}, {3, 0}, {4, 0}, {5, 0},
+                               {0, 1}, {1, 1}, {2, 1}, {3, 1}, {4, 1}, {5, 1}, {6, 1}, {7, 1},
+                                               {2, 2}, {3, 2}, {4, 2}, {5, 2},
+                       });
+
+       draw_image(left, 0.836660f,
+                       4, 0, 0,
+                       2, 5, 2 * size,
+                       {
+                               {0, 0}, {1, 0},
+                               {0, 1}, {1, 1}, {2, 1}, {3, 1},
+                               {0, 2}, {1, 2}, {2, 2}, {3, 2},
+                               {0, 3}, {1, 3}, {2, 3}, {3, 3},
+                               {0, 4}, {1, 4}, {2, 4}, {3, 4},
+                                               {2, 5}, {3, 5},
+                       });
+
+       draw_image(right, 0.670820f,
+                       4, 0, 4 * size,
+                       -2, 5, 4 * size - 2,
+                       {
+                                               {2, 0}, {3, 0},
+                               {0, 1}, {1, 1}, {2, 1}, {3, 1},
+                               {0, 2}, {1, 2}, {2, 2}, {3, 2},
+                               {0, 3}, {1, 3}, {2, 3}, {3, 3},
+                               {0, 4}, {1, 4}, {2, 4}, {3, 4},
+                               {0, 5}, {1, 5},
+                       });
+
+       result->unlock();
+       return result;
 }
 
 video::IImage* TextureSource::generateImage(const std::string &name)
 {
-       /*
-               Get the base image
-       */
+       // Get the base image
 
        const char separator = '^';
+       const char escape = '\\';
        const char paren_open = '(';
        const char paren_close = ')';
 
@@ -957,7 +925,9 @@ video::IImage* TextureSource::generateImage(const std::string &name)
        s32 last_separator_pos = -1;
        u8 paren_bal = 0;
        for (s32 i = name.size() - 1; i >= 0; i--) {
-               switch(name[i]) {
+               if (i > 0 && name[i-1] == escape)
+                       continue;
+               switch (name[i]) {
                case separator:
                        if (paren_bal == 0) {
                                last_separator_pos = i;
@@ -998,10 +968,6 @@ video::IImage* TextureSource::generateImage(const std::string &name)
                baseimg = generateImage(name.substr(0, last_separator_pos));
        }
 
-
-       video::IVideoDriver* driver = m_device->getVideoDriver();
-       sanity_check(driver);
-
        /*
                Parse out the last part of the name of the image and act
                according to it
@@ -1009,7 +975,7 @@ video::IImage* TextureSource::generateImage(const std::string &name)
 
        std::string last_part_of_name = name.substr(last_separator_pos + 1);
 
-       /* 
+       /*
                If this name is enclosed in parentheses, generate it
                and blit it onto the base image
        */
@@ -1025,10 +991,12 @@ video::IImage* TextureSource::generateImage(const std::string &name)
                        return NULL;
                }
                core::dimension2d<u32> dim = tmp->getDimension();
-               if (!baseimg)
-                       baseimg = driver->createImage(video::ECF_A8R8G8B8, dim);
-               blit_with_alpha(tmp, baseimg, v2s32(0, 0), v2s32(0, 0), dim);
-               tmp->drop();
+               if (baseimg) {
+                       blit_with_alpha(tmp, baseimg, v2s32(0, 0), v2s32(0, 0), dim);
+                       tmp->drop();
+               } else {
+                       baseimg = tmp;
+               }
        } else if (!generateImagePart(last_part_of_name, baseimg)) {
                // Generate image according to part of name
                errorstream << "generateImage(): "
@@ -1045,83 +1013,113 @@ video::IImage* TextureSource::generateImage(const std::string &name)
        return baseimg;
 }
 
-#ifdef __ANDROID__
-#include <GLES/gl.h>
+#if ENABLE_GLES
+
+
+static inline u16 get_GL_major_version()
+{
+       const GLubyte *gl_version = glGetString(GL_VERSION);
+       return (u16) (gl_version[0] - '0');
+}
+
+/**
+ * Check if hardware requires npot2 aligned textures
+ * @return true if alignment NOT(!) requires, false otherwise
+ */
+
+bool hasNPotSupport()
+{
+       // Only GLES2 is trusted to correctly report npot support
+       // Note: we cache the boolean result, the GL context will never change.
+       static const bool supported = get_GL_major_version() > 1 &&
+               glGetString(GL_EXTENSIONS) &&
+               strstr((char *)glGetString(GL_EXTENSIONS), "GL_OES_texture_npot");
+       return supported;
+}
+
 /**
  * Check and align image to npot2 if required by hardware
  * @param image image to check for npot2 alignment
  * @param driver driver to use for image operations
  * @return image or copy of image aligned to npot2
  */
+
 video::IImage * Align2Npot2(video::IImage * image,
                video::IVideoDriver* driver)
 {
-       if (image == NULL) {
+       if (image == NULL)
                return image;
-       }
-
-       core::dimension2d<u32> dim = image->getDimension();
 
-       std::string extensions = (char*) glGetString(GL_EXTENSIONS);
-       if (extensions.find("GL_OES_texture_npot") != std::string::npos) {
+       if (hasNPotSupport())
                return image;
-       }
 
+       core::dimension2d<u32> dim = image->getDimension();
        unsigned int height = npot2(dim.Height);
        unsigned int width  = npot2(dim.Width);
 
-       if ((dim.Height == height) &&
-                       (dim.Width == width)) {
+       if (dim.Height == height && dim.Width == width)
                return image;
-       }
 
-       if (dim.Height > height) {
+       if (dim.Height > height)
                height *= 2;
-       }
-
-       if (dim.Width > width) {
+       if (dim.Width > width)
                width *= 2;
-       }
 
        video::IImage *targetimage =
                        driver->createImage(video::ECF_A8R8G8B8,
                                        core::dimension2d<u32>(width, height));
 
-       if (targetimage != NULL) {
+       if (targetimage != NULL)
                image->copyToScaling(targetimage);
-       }
        image->drop();
        return targetimage;
 }
 
 #endif
 
+static std::string unescape_string(const std::string &str, const char esc = '\\')
+{
+       std::string out;
+       size_t pos = 0, cpos;
+       out.reserve(str.size());
+       while (1) {
+               cpos = str.find_first_of(esc, pos);
+               if (cpos == std::string::npos) {
+                       out += str.substr(pos);
+                       break;
+               }
+               out += str.substr(pos, cpos - pos) + str[cpos + 1];
+               pos = cpos + 2;
+       }
+       return out;
+}
+
 bool TextureSource::generateImagePart(std::string part_of_name,
                video::IImage *& baseimg)
 {
-       video::IVideoDriver* driver = m_device->getVideoDriver();
+       const char escape = '\\'; // same as in generateImage()
+       video::IVideoDriver *driver = RenderingEngine::get_video_driver();
        sanity_check(driver);
 
        // Stuff starting with [ are special commands
-       if (part_of_name.size() == 0 || part_of_name[0] != '[')
-       {
-               video::IImage *image = m_sourcecache.getOrLoad(part_of_name, m_device);
-#ifdef __ANDROID__
+       if (part_of_name.empty() || part_of_name[0] != '[') {
+               video::IImage *image = m_sourcecache.getOrLoad(part_of_name);
+#if ENABLE_GLES
                image = Align2Npot2(image, driver);
 #endif
                if (image == NULL) {
-                       if (part_of_name != "") {
-                               if (part_of_name.find("_normal.png") == std::string::npos){
-                                       errorstream<<"generateImage(): Could not load image \""
-                                               <<part_of_name<<"\""<<" while building texture"<<std::endl;
-                                       errorstream<<"generateImage(): Creating a dummy"
-                                               <<" image for \""<<part_of_name<<"\""<<std::endl;
-                               } else {
-                                       infostream<<"generateImage(): Could not load normal map \""
-                                               <<part_of_name<<"\""<<std::endl;
-                                       infostream<<"generateImage(): Creating a dummy"
-                                               <<" normal map for \""<<part_of_name<<"\""<<std::endl;
+                       if (!part_of_name.empty()) {
+
+                               // Do not create normalmap dummies
+                               if (part_of_name.find("_normal.png") != std::string::npos) {
+                                       warningstream << "generateImage(): Could not load normal map \""
+                                               << part_of_name << "\"" << std::endl;
+                                       return true;
                                }
+
+                               errorstream << "generateImage(): Could not load image \""
+                                       << part_of_name << "\" while building texture; "
+                                       "Creating a dummy image" << std::endl;
                        }
 
                        // Just create a dummy image
@@ -1172,7 +1170,28 @@ bool TextureSource::generateImagePart(std::string part_of_name,
                                        core::rect<s32>(pos_from, dim),
                                        video::SColor(255,255,255,255),
                                        NULL);*/
-                       blit_with_alpha(image, baseimg, pos_from, pos_to, dim);
+
+                       core::dimension2d<u32> dim_dst = baseimg->getDimension();
+                       if (dim == dim_dst) {
+                               blit_with_alpha(image, baseimg, pos_from, pos_to, dim);
+                       } else if (dim.Width * dim.Height < dim_dst.Width * dim_dst.Height) {
+                               // Upscale overlying image
+                               video::IImage *scaled_image = RenderingEngine::get_video_driver()->
+                                       createImage(video::ECF_A8R8G8B8, dim_dst);
+                               image->copyToScaling(scaled_image);
+
+                               blit_with_alpha(scaled_image, baseimg, pos_from, pos_to, dim_dst);
+                               scaled_image->drop();
+                       } else {
+                               // Upscale base image
+                               video::IImage *scaled_base = RenderingEngine::get_video_driver()->
+                                       createImage(video::ECF_A8R8G8B8, dim);
+                               baseimg->copyToScaling(scaled_base);
+                               baseimg->drop();
+                               baseimg = scaled_base;
+
+                               blit_with_alpha(image, baseimg, pos_from, pos_to, dim);
+                       }
                }
                //cleanup
                image->drop();
@@ -1191,7 +1210,7 @@ bool TextureSource::generateImagePart(std::string part_of_name,
                        Adds a cracking texture
                        N = animation frame count, P = crack progression
                */
-               if (part_of_name.substr(0,6) == "[crack")
+               if (str_starts_with(part_of_name, "[crack"))
                {
                        if (baseimg == NULL) {
                                errorstream<<"generateImagePart(): baseimg == NULL "
@@ -1201,57 +1220,66 @@ bool TextureSource::generateImagePart(std::string part_of_name,
                        }
 
                        // Crack image number and overlay option
+                       // Format: crack[o][:<tiles>]:<frame_count>:<frame>
                        bool use_overlay = (part_of_name[6] == 'o');
                        Strfnd sf(part_of_name);
                        sf.next(":");
                        s32 frame_count = stoi(sf.next(":"));
                        s32 progression = stoi(sf.next(":"));
+                       s32 tiles = 1;
+                       // Check whether there is the <tiles> argument, that is,
+                       // whether there are 3 arguments. If so, shift values
+                       // as the first and not the last argument is optional.
+                       auto s = sf.next(":");
+                       if (!s.empty()) {
+                               tiles = frame_count;
+                               frame_count = progression;
+                               progression = stoi(s);
+                       }
 
-                       /*
-                               Load crack image.
+                       if (progression >= 0) {
+                               /*
+                                       Load crack image.
 
-                               It is an image with a number of cracking stages
-                               horizontally tiled.
-                       */
-                       video::IImage *img_crack = m_sourcecache.getOrLoad(
-                                       "crack_anylength.png", m_device);
+                                       It is an image with a number of cracking stages
+                                       horizontally tiled.
+                               */
+                               video::IImage *img_crack = m_sourcecache.getOrLoad(
+                                       "crack_anylength.png");
 
-                       if (img_crack && progression >= 0)
-                       {
-                               draw_crack(img_crack, baseimg,
+                               if (img_crack) {
+                                       draw_crack(img_crack, baseimg,
                                                use_overlay, frame_count,
-                                               progression, driver);
-                               img_crack->drop();
+                                               progression, driver, tiles);
+                                       img_crack->drop();
+                               }
                        }
                }
                /*
                        [combine:WxH:X,Y=filename:X,Y=filename2
-                       Creates a bigger texture from an amount of smaller ones
+                       Creates a bigger texture from any amount of smaller ones
                */
-               else if (part_of_name.substr(0,8) == "[combine")
+               else if (str_starts_with(part_of_name, "[combine"))
                {
                        Strfnd sf(part_of_name);
                        sf.next(":");
                        u32 w0 = stoi(sf.next("x"));
                        u32 h0 = stoi(sf.next(":"));
-                       //infostream<<"combined w="<<w0<<" h="<<h0<<std::endl;
                        core::dimension2d<u32> dim(w0,h0);
                        if (baseimg == NULL) {
                                baseimg = driver->createImage(video::ECF_A8R8G8B8, dim);
                                baseimg->fill(video::SColor(0,0,0,0));
                        }
-                       while (sf.atend() == false) {
+                       while (!sf.at_end()) {
                                u32 x = stoi(sf.next(","));
                                u32 y = stoi(sf.next("="));
-                               std::string filename = sf.next(":");
+                               std::string filename = unescape_string(sf.next_esc(":", escape), escape);
                                infostream<<"Adding \""<<filename
                                                <<"\" to combined ("<<x<<","<<y<<")"
                                                <<std::endl;
-                               video::IImage *img = m_sourcecache.getOrLoad(filename, m_device);
+                               video::IImage *img = generateImage(filename);
                                if (img) {
                                        core::dimension2d<u32> dim = img->getDimension();
-                                       infostream<<"Size "<<dim.Width
-                                                       <<"x"<<dim.Height<<std::endl;
                                        core::position2d<s32> pos_base(x, y);
                                        video::IImage *img2 =
                                                        driver->createImage(video::ECF_A8R8G8B8, dim);
@@ -1270,9 +1298,9 @@ bool TextureSource::generateImagePart(std::string part_of_name,
                        }
                }
                /*
-                       "[brighten"
+                       [brighten
                */
-               else if (part_of_name.substr(0,9) == "[brighten")
+               else if (str_starts_with(part_of_name, "[brighten"))
                {
                        if (baseimg == NULL) {
                                errorstream<<"generateImagePart(): baseimg==NULL "
@@ -1284,13 +1312,13 @@ bool TextureSource::generateImagePart(std::string part_of_name,
                        brighten(baseimg);
                }
                /*
-                       "[noalpha"
+                       [noalpha
                        Make image completely opaque.
                        Used for the leaves texture when in old leaves mode, so
                        that the transparent parts don't look completely black
                        when simple alpha channel is used for rendering.
                */
-               else if (part_of_name.substr(0,8) == "[noalpha")
+               else if (str_starts_with(part_of_name, "[noalpha"))
                {
                        if (baseimg == NULL){
                                errorstream<<"generateImagePart(): baseimg==NULL "
@@ -1311,10 +1339,10 @@ bool TextureSource::generateImagePart(std::string part_of_name,
                        }
                }
                /*
-                       "[makealpha:R,G,B"
+                       [makealpha:R,G,B
                        Convert one color to transparent.
                */
-               else if (part_of_name.substr(0,11) == "[makealpha:")
+               else if (str_starts_with(part_of_name, "[makealpha:"))
                {
                        if (baseimg == NULL) {
                                errorstream<<"generateImagePart(): baseimg == NULL "
@@ -1327,7 +1355,6 @@ bool TextureSource::generateImagePart(std::string part_of_name,
                        u32 r1 = stoi(sf.next(","));
                        u32 g1 = stoi(sf.next(","));
                        u32 b1 = stoi(sf.next(""));
-                       std::string filename = sf.next("");
 
                        core::dimension2d<u32> dim = baseimg->getDimension();
 
@@ -1351,7 +1378,7 @@ bool TextureSource::generateImagePart(std::string part_of_name,
                        }
                }
                /*
-                       "[transformN"
+                       [transformN
                        Rotates and/or flips the image.
 
                        N can be a number (between 0 and 7) or a transform name.
@@ -1370,7 +1397,7 @@ bool TextureSource::generateImagePart(std::string part_of_name,
                        The resulting transform will be equivalent to one of the
                        eight existing ones, though (see: dihedral group).
                */
-               else if (part_of_name.substr(0,10) == "[transform")
+               else if (str_starts_with(part_of_name, "[transform"))
                {
                        if (baseimg == NULL) {
                                errorstream<<"generateImagePart(): baseimg == NULL "
@@ -1397,7 +1424,7 @@ bool TextureSource::generateImagePart(std::string part_of_name,
                        Example (a grass block (not actually used in game):
                        "[inventorycube{grass.png{mud.png&grass_side.png{mud.png&grass_side.png"
                */
-               else if (part_of_name.substr(0,14) == "[inventorycube")
+               else if (str_starts_with(part_of_name, "[inventorycube"))
                {
                        if (baseimg != NULL){
                                errorstream<<"generateImagePart(): baseimg != NULL "
@@ -1426,105 +1453,29 @@ bool TextureSource::generateImagePart(std::string part_of_name,
                                return true;
                        }
 
-#ifdef __ANDROID__
-                       assert(img_top->getDimension().Height == npot2(img_top->getDimension().Height));
-                       assert(img_top->getDimension().Width == npot2(img_top->getDimension().Width));
+                       baseimg = createInventoryCubeImage(img_top, img_left, img_right);
 
-                       assert(img_left->getDimension().Height == npot2(img_left->getDimension().Height));
-                       assert(img_left->getDimension().Width == npot2(img_left->getDimension().Width));
-
-                       assert(img_right->getDimension().Height == npot2(img_right->getDimension().Height));
-                       assert(img_right->getDimension().Width == npot2(img_right->getDimension().Width));
-#endif
-
-                       // Create textures from images
-                       video::ITexture *texture_top = driver->addTexture(
-                                       (imagename_top + "__temp__").c_str(), img_top);
-                       video::ITexture *texture_left = driver->addTexture(
-                                       (imagename_left + "__temp__").c_str(), img_left);
-                       video::ITexture *texture_right = driver->addTexture(
-                                       (imagename_right + "__temp__").c_str(), img_right);
-                       FATAL_ERROR_IF(!(texture_top && texture_left && texture_right), "");
-
-                       // Drop images
+                       // Face images are not needed anymore
                        img_top->drop();
                        img_left->drop();
                        img_right->drop();
 
-                       /*
-                               Draw a cube mesh into a render target texture
-                       */
-                       scene::IMesh* cube = createCubeMesh(v3f(1, 1, 1));
-                       setMeshColor(cube, video::SColor(255, 255, 255, 255));
-                       cube->getMeshBuffer(0)->getMaterial().setTexture(0, texture_top);
-                       cube->getMeshBuffer(1)->getMaterial().setTexture(0, texture_top);
-                       cube->getMeshBuffer(2)->getMaterial().setTexture(0, texture_right);
-                       cube->getMeshBuffer(3)->getMaterial().setTexture(0, texture_right);
-                       cube->getMeshBuffer(4)->getMaterial().setTexture(0, texture_left);
-                       cube->getMeshBuffer(5)->getMaterial().setTexture(0, texture_left);
-
-                       TextureFromMeshParams params;
-                       params.mesh = cube;
-                       params.dim.set(64, 64);
-                       params.rtt_texture_name = part_of_name + "_RTT";
-                       // We will delete the rtt texture ourselves
-                       params.delete_texture_on_shutdown = false;
-                       params.camera_position.set(0, 1.0, -1.5);
-                       params.camera_position.rotateXZBy(45);
-                       params.camera_lookat.set(0, 0, 0);
-                       // Set orthogonal projection
-                       params.camera_projection_matrix.buildProjectionMatrixOrthoLH(
-                                       1.65, 1.65, 0, 100);
-
-                       params.ambient_light.set(1.0, 0.2, 0.2, 0.2);
-                       params.light_position.set(10, 100, -50);
-                       params.light_color.set(1.0, 0.5, 0.5, 0.5);
-                       params.light_radius = 1000;
-
-                       video::ITexture *rtt = generateTextureFromMesh(params);
-
-                       // Drop mesh
-                       cube->drop();
-
-                       // Free textures
-                       driver->removeTexture(texture_top);
-                       driver->removeTexture(texture_left);
-                       driver->removeTexture(texture_right);
-
-                       if (rtt == NULL) {
-                               baseimg = generateImage(imagename_top);
-                               return true;
-                       }
-
-                       // Create image of render target
-                       video::IImage *image = driver->createImage(rtt, v2s32(0, 0), params.dim);
-                       FATAL_ERROR_IF(!image, "Could not create image of render target");
-
-                       // Cleanup texture
-                       driver->removeTexture(rtt);
-
-                       baseimg = driver->createImage(video::ECF_A8R8G8B8, params.dim);
-
-                       if (image) {
-                               image->copyTo(baseimg);
-                               image->drop();
-                       }
+                       return true;
                }
                /*
                        [lowpart:percent:filename
                        Adds the lower part of a texture
                */
-               else if (part_of_name.substr(0,9) == "[lowpart:")
+               else if (str_starts_with(part_of_name, "[lowpart:"))
                {
                        Strfnd sf(part_of_name);
                        sf.next(":");
                        u32 percent = stoi(sf.next(":"));
-                       std::string filename = sf.next(":");
-                       //infostream<<"power part "<<percent<<"%% of "<<filename<<std::endl;
+                       std::string filename = unescape_string(sf.next_esc(":", escape), escape);
 
                        if (baseimg == NULL)
                                baseimg = driver->createImage(video::ECF_A8R8G8B8, v2u32(16,16));
-                       video::IImage *img = m_sourcecache.getOrLoad(filename, m_device);
+                       video::IImage *img = generateImage(filename);
                        if (img)
                        {
                                core::dimension2d<u32> dim = img->getDimension();
@@ -1550,7 +1501,7 @@ bool TextureSource::generateImagePart(std::string part_of_name,
                        Crops a frame of a vertical animation.
                        N = frame count, I = frame index
                */
-               else if (part_of_name.substr(0,15) == "[verticalframe:")
+               else if (str_starts_with(part_of_name, "[verticalframe:"))
                {
                        Strfnd sf(part_of_name);
                        sf.next(":");
@@ -1594,7 +1545,7 @@ bool TextureSource::generateImagePart(std::string part_of_name,
                        [mask:filename
                        Applies a mask to an image
                */
-               else if (part_of_name.substr(0,6) == "[mask:")
+               else if (str_starts_with(part_of_name, "[mask:"))
                {
                        if (baseimg == NULL) {
                                errorstream << "generateImage(): baseimg == NULL "
@@ -1604,23 +1555,49 @@ bool TextureSource::generateImagePart(std::string part_of_name,
                        }
                        Strfnd sf(part_of_name);
                        sf.next(":");
-                       std::string filename = sf.next(":");
+                       std::string filename = unescape_string(sf.next_esc(":", escape), escape);
 
-                       video::IImage *img = m_sourcecache.getOrLoad(filename, m_device);
+                       video::IImage *img = generateImage(filename);
                        if (img) {
                                apply_mask(img, baseimg, v2s32(0, 0), v2s32(0, 0),
                                                img->getDimension());
+                               img->drop();
                        } else {
                                errorstream << "generateImage(): Failed to load \""
                                                << filename << "\".";
                        }
                }
+               /*
+               [multiply:color
+                       multiplys a given color to any pixel of an image
+                       color = color as ColorString
+               */
+               else if (str_starts_with(part_of_name, "[multiply:")) {
+                       Strfnd sf(part_of_name);
+                       sf.next(":");
+                       std::string color_str = sf.next(":");
+
+                       if (baseimg == NULL) {
+                               errorstream << "generateImagePart(): baseimg != NULL "
+                                               << "for part_of_name=\"" << part_of_name
+                                               << "\", cancelling." << std::endl;
+                               return false;
+                       }
+
+                       video::SColor color;
+
+                       if (!parseColorString(color_str, color, false))
+                               return false;
+
+                       apply_multiplication(baseimg, v2u32(0, 0), baseimg->getDimension(), color);
+               }
                /*
                        [colorize:color
                        Overlays image with given color
                        color = color as ColorString
                */
-               else if (part_of_name.substr(0,10) == "[colorize:") {
+               else if (str_starts_with(part_of_name, "[colorize:"))
+               {
                        Strfnd sf(part_of_name);
                        sf.next(":");
                        std::string color_str = sf.next(":");
@@ -1635,36 +1612,39 @@ bool TextureSource::generateImagePart(std::string part_of_name,
 
                        video::SColor color;
                        int ratio = -1;
+                       bool keep_alpha = false;
 
                        if (!parseColorString(color_str, color, false))
                                return false;
 
                        if (is_number(ratio_str))
                                ratio = mystoi(ratio_str, 0, 255);
+                       else if (ratio_str == "alpha")
+                               keep_alpha = true;
 
-                       core::dimension2d<u32> dim = baseimg->getDimension();
-                       video::IImage *img = driver->createImage(video::ECF_A8R8G8B8, dim);
+                       apply_colorize(baseimg, v2u32(0, 0), baseimg->getDimension(), color, ratio, keep_alpha);
+               }
+               /*
+                       [applyfiltersformesh
+                       Internal modifier
+               */
+               else if (str_starts_with(part_of_name, "[applyfiltersformesh"))
+               {
+                       /* IMPORTANT: When changing this, getTextureForMesh() needs to be
+                        * updated too. */
 
-                       if (!img) {
-                               errorstream << "generateImagePart(): Could not create image "
-                                               << "for part_of_name=\"" << part_of_name
-                                               << "\", cancelling." << std::endl;
-                               return false;
-                       }
+                       // Apply the "clean transparent" filter, if configured.
+                       if (g_settings->getBool("texture_clean_transparent"))
+                               imageCleanTransparent(baseimg, 127);
 
-                       img->fill(video::SColor(color));
-                       // Overlay the colored image
-                       blit_with_interpolate_overlay(img, baseimg, v2s32(0,0), v2s32(0,0), dim, ratio);
-                       img->drop();
-               }
-               else if (part_of_name.substr(0,19) == "[autoupscaleformesh") {
                        /* Upscale textures to user's requested minimum size.  This is a trick to make
                         * filters look as good on low-res textures as on high-res ones, by making
                         * low-res textures BECOME high-res ones.  This is helpful for worlds that
                         * mix high- and low-res textures, or for mods with least-common-denominator
                         * textures that don't have the resources to offer high-res alternatives.
                         */
-                       s32 scaleto = g_settings->getS32("texture_min_size");
+                       const bool filter = m_setting_trilinear_filter || m_setting_bilinear_filter;
+                       const s32 scaleto = filter ? g_settings->getS32("texture_min_size") : 1;
                        if (scaleto > 1) {
                                const core::dimension2d<u32> dim = baseimg->getDimension();
 
@@ -1672,6 +1652,12 @@ bool TextureSource::generateImagePart(std::string part_of_name,
                                 * equal to the target minimum.  If e.g. this is a vertical frames
                                 * animation, the short dimension will be the real size.
                                 */
+                               if ((dim.Width == 0) || (dim.Height == 0)) {
+                                       errorstream << "generateImagePart(): Illegal 0 dimension "
+                                               << "for part_of_name=\""<< part_of_name
+                                               << "\", cancelling." << std::endl;
+                                       return false;
+                               }
                                u32 xscale = scaleto / dim.Width;
                                u32 yscale = scaleto / dim.Height;
                                u32 scale = (xscale > yscale) ? xscale : yscale;
@@ -1689,6 +1675,143 @@ bool TextureSource::generateImagePart(std::string part_of_name,
                                }
                        }
                }
+               /*
+                       [resize:WxH
+                       Resizes the base image to the given dimensions
+               */
+               else if (str_starts_with(part_of_name, "[resize"))
+               {
+                       if (baseimg == NULL) {
+                               errorstream << "generateImagePart(): baseimg == NULL "
+                                               << "for part_of_name=\""<< part_of_name
+                                               << "\", cancelling." << std::endl;
+                               return false;
+                       }
+
+                       Strfnd sf(part_of_name);
+                       sf.next(":");
+                       u32 width = stoi(sf.next("x"));
+                       u32 height = stoi(sf.next(""));
+                       core::dimension2d<u32> dim(width, height);
+
+                       video::IImage *image = RenderingEngine::get_video_driver()->
+                               createImage(video::ECF_A8R8G8B8, dim);
+                       baseimg->copyToScaling(image);
+                       baseimg->drop();
+                       baseimg = image;
+               }
+               /*
+                       [opacity:R
+                       Makes the base image transparent according to the given ratio.
+                       R must be between 0 and 255.
+                       0 means totally transparent.
+                       255 means totally opaque.
+               */
+               else if (str_starts_with(part_of_name, "[opacity:")) {
+                       if (baseimg == NULL) {
+                               errorstream << "generateImagePart(): baseimg == NULL "
+                                               << "for part_of_name=\"" << part_of_name
+                                               << "\", cancelling." << std::endl;
+                               return false;
+                       }
+
+                       Strfnd sf(part_of_name);
+                       sf.next(":");
+
+                       u32 ratio = mystoi(sf.next(""), 0, 255);
+
+                       core::dimension2d<u32> dim = baseimg->getDimension();
+
+                       for (u32 y = 0; y < dim.Height; y++)
+                       for (u32 x = 0; x < dim.Width; x++)
+                       {
+                               video::SColor c = baseimg->getPixel(x, y);
+                               c.setAlpha(floor((c.getAlpha() * ratio) / 255 + 0.5));
+                               baseimg->setPixel(x, y, c);
+                       }
+               }
+               /*
+                       [invert:mode
+                       Inverts the given channels of the base image.
+                       Mode may contain the characters "r", "g", "b", "a".
+                       Only the channels that are mentioned in the mode string
+                       will be inverted.
+               */
+               else if (str_starts_with(part_of_name, "[invert:")) {
+                       if (baseimg == NULL) {
+                               errorstream << "generateImagePart(): baseimg == NULL "
+                                               << "for part_of_name=\"" << part_of_name
+                                               << "\", cancelling." << std::endl;
+                               return false;
+                       }
+
+                       Strfnd sf(part_of_name);
+                       sf.next(":");
+
+                       std::string mode = sf.next("");
+                       u32 mask = 0;
+                       if (mode.find('a') != std::string::npos)
+                               mask |= 0xff000000UL;
+                       if (mode.find('r') != std::string::npos)
+                               mask |= 0x00ff0000UL;
+                       if (mode.find('g') != std::string::npos)
+                               mask |= 0x0000ff00UL;
+                       if (mode.find('b') != std::string::npos)
+                               mask |= 0x000000ffUL;
+
+                       core::dimension2d<u32> dim = baseimg->getDimension();
+
+                       for (u32 y = 0; y < dim.Height; y++)
+                       for (u32 x = 0; x < dim.Width; x++)
+                       {
+                               video::SColor c = baseimg->getPixel(x, y);
+                               c.color ^= mask;
+                               baseimg->setPixel(x, y, c);
+                       }
+               }
+               /*
+                       [sheet:WxH:X,Y
+                       Retrieves a tile at position X,Y (in tiles)
+                       from the base image it assumes to be a
+                       tilesheet with dimensions W,H (in tiles).
+               */
+               else if (part_of_name.substr(0,7) == "[sheet:") {
+                       if (baseimg == NULL) {
+                               errorstream << "generateImagePart(): baseimg != NULL "
+                                               << "for part_of_name=\"" << part_of_name
+                                               << "\", cancelling." << std::endl;
+                               return false;
+                       }
+
+                       Strfnd sf(part_of_name);
+                       sf.next(":");
+                       u32 w0 = stoi(sf.next("x"));
+                       u32 h0 = stoi(sf.next(":"));
+                       u32 x0 = stoi(sf.next(","));
+                       u32 y0 = stoi(sf.next(":"));
+
+                       core::dimension2d<u32> img_dim = baseimg->getDimension();
+                       core::dimension2d<u32> tile_dim(v2u32(img_dim) / v2u32(w0, h0));
+
+                       video::IImage *img = driver->createImage(
+                                       video::ECF_A8R8G8B8, tile_dim);
+                       if (!img) {
+                               errorstream << "generateImagePart(): Could not create image "
+                                               << "for part_of_name=\"" << part_of_name
+                                               << "\", cancelling." << std::endl;
+                               return false;
+                       }
+
+                       img->fill(video::SColor(0,0,0,0));
+                       v2u32 vdim(tile_dim);
+                       core::rect<s32> rect(v2s32(x0 * vdim.X, y0 * vdim.Y), tile_dim);
+                       baseimg->copyToWithAlpha(img, v2s32(0), rect,
+                                       video::SColor(255,255,255,255), NULL);
+
+                       // Replace baseimg
+                       baseimg->drop();
+                       baseimg = img;
+               }
                else
                {
                        errorstream << "generateImagePart(): Invalid "
@@ -1699,6 +1822,24 @@ bool TextureSource::generateImagePart(std::string part_of_name,
        return true;
 }
 
+/*
+       Calculate the color of a single pixel drawn on top of another pixel.
+
+       This is a little more complicated than just video::SColor::getInterpolated
+       because getInterpolated does not handle alpha correctly.  For example, a
+       pixel with alpha=64 drawn atop a pixel with alpha=128 should yield a
+       pixel with alpha=160, while getInterpolated would yield alpha=96.
+*/
+static inline video::SColor blitPixel(const video::SColor &src_c, const video::SColor &dst_c, u32 ratio)
+{
+       if (dst_c.getAlpha() == 0)
+               return src_c;
+       video::SColor out_c = src_c.getInterpolated(dst_c, (float)ratio / 255.0f);
+       out_c.setAlpha(dst_c.getAlpha() + (255 - dst_c.getAlpha()) *
+               src_c.getAlpha() * ratio / (255 * 255));
+       return out_c;
+}
+
 /*
        Draw an image on top of an another one, using the alpha channel of the
        source image
@@ -1718,7 +1859,7 @@ static void blit_with_alpha(video::IImage *src, video::IImage *dst,
                s32 dst_y = dst_pos.Y + y0;
                video::SColor src_c = src->getPixel(src_x, src_y);
                video::SColor dst_c = dst->getPixel(dst_x, dst_y);
-               dst_c = src_c.getInterpolated(dst_c, (float)src_c.getAlpha()/255.0f);
+               dst_c = blitPixel(src_c, dst_c, src_c.getAlpha());
                dst->setPixel(dst_x, dst_y, dst_c);
        }
 }
@@ -1741,12 +1882,15 @@ static void blit_with_alpha_overlay(video::IImage *src, video::IImage *dst,
                video::SColor dst_c = dst->getPixel(dst_x, dst_y);
                if (dst_c.getAlpha() == 255 && src_c.getAlpha() != 0)
                {
-                       dst_c = src_c.getInterpolated(dst_c, (float)src_c.getAlpha()/255.0f);
+                       dst_c = blitPixel(src_c, dst_c, src_c.getAlpha());
                        dst->setPixel(dst_x, dst_y, dst_c);
                }
        }
 }
 
+// This function has been disabled because it is currently unused.
+// Feel free to re-enable if you find it handy.
+#if 0
 /*
        Draw an image on top of an another one, using the specified ratio
        modify all partially-opaque pixels in the destination.
@@ -1773,6 +1917,66 @@ static void blit_with_interpolate_overlay(video::IImage *src, video::IImage *dst
                }
        }
 }
+#endif
+
+/*
+       Apply color to destination
+*/
+static void apply_colorize(video::IImage *dst, v2u32 dst_pos, v2u32 size,
+               const video::SColor &color, int ratio, bool keep_alpha)
+{
+       u32 alpha = color.getAlpha();
+       video::SColor dst_c;
+       if ((ratio == -1 && alpha == 255) || ratio == 255) { // full replacement of color
+               if (keep_alpha) { // replace the color with alpha = dest alpha * color alpha
+                       dst_c = color;
+                       for (u32 y = dst_pos.Y; y < dst_pos.Y + size.Y; y++)
+                       for (u32 x = dst_pos.X; x < dst_pos.X + size.X; x++) {
+                               u32 dst_alpha = dst->getPixel(x, y).getAlpha();
+                               if (dst_alpha > 0) {
+                                       dst_c.setAlpha(dst_alpha * alpha / 255);
+                                       dst->setPixel(x, y, dst_c);
+                               }
+                       }
+               } else { // replace the color including the alpha
+                       for (u32 y = dst_pos.Y; y < dst_pos.Y + size.Y; y++)
+                       for (u32 x = dst_pos.X; x < dst_pos.X + size.X; x++)
+                               if (dst->getPixel(x, y).getAlpha() > 0)
+                                       dst->setPixel(x, y, color);
+               }
+       } else {  // interpolate between the color and destination
+               float interp = (ratio == -1 ? color.getAlpha() / 255.0f : ratio / 255.0f);
+               for (u32 y = dst_pos.Y; y < dst_pos.Y + size.Y; y++)
+               for (u32 x = dst_pos.X; x < dst_pos.X + size.X; x++) {
+                       dst_c = dst->getPixel(x, y);
+                       if (dst_c.getAlpha() > 0) {
+                               dst_c = color.getInterpolated(dst_c, interp);
+                               dst->setPixel(x, y, dst_c);
+                       }
+               }
+       }
+}
+
+/*
+       Apply color to destination
+*/
+static void apply_multiplication(video::IImage *dst, v2u32 dst_pos, v2u32 size,
+               const video::SColor &color)
+{
+       video::SColor dst_c;
+
+       for (u32 y = dst_pos.Y; y < dst_pos.Y + size.Y; y++)
+       for (u32 x = dst_pos.X; x < dst_pos.X + size.X; x++) {
+               dst_c = dst->getPixel(x, y);
+               dst_c.set(
+                               dst_c.getAlpha(),
+                               (dst_c.getRed() * color.getRed()) / 255,
+                               (dst_c.getGreen() * color.getGreen()) / 255,
+                               (dst_c.getBlue() * color.getBlue()) / 255
+                               );
+               dst->setPixel(x, y, dst_c);
+       }
+}
 
 /*
        Apply mask to destination
@@ -1794,74 +1998,78 @@ static void apply_mask(video::IImage *mask, video::IImage *dst,
        }
 }
 
+video::IImage *create_crack_image(video::IImage *crack, s32 frame_index,
+               core::dimension2d<u32> size, u8 tiles, video::IVideoDriver *driver)
+{
+       core::dimension2d<u32> strip_size = crack->getDimension();
+       core::dimension2d<u32> frame_size(strip_size.Width, strip_size.Width);
+       core::dimension2d<u32> tile_size(size / tiles);
+       s32 frame_count = strip_size.Height / strip_size.Width;
+       if (frame_index >= frame_count)
+               frame_index = frame_count - 1;
+       core::rect<s32> frame(v2s32(0, frame_index * frame_size.Height), frame_size);
+       video::IImage *result = nullptr;
+
+// extract crack frame
+       video::IImage *crack_tile = driver->createImage(video::ECF_A8R8G8B8, tile_size);
+       if (!crack_tile)
+               return nullptr;
+       if (tile_size == frame_size) {
+               crack->copyTo(crack_tile, v2s32(0, 0), frame);
+       } else {
+               video::IImage *crack_frame = driver->createImage(video::ECF_A8R8G8B8, frame_size);
+               if (!crack_frame)
+                       goto exit__has_tile;
+               crack->copyTo(crack_frame, v2s32(0, 0), frame);
+               crack_frame->copyToScaling(crack_tile);
+               crack_frame->drop();
+       }
+       if (tiles == 1)
+               return crack_tile;
+
+// tile it
+       result = driver->createImage(video::ECF_A8R8G8B8, size);
+       if (!result)
+               goto exit__has_tile;
+       result->fill({});
+       for (u8 i = 0; i < tiles; i++)
+               for (u8 j = 0; j < tiles; j++)
+                       crack_tile->copyTo(result, v2s32(i * tile_size.Width, j * tile_size.Height));
+
+exit__has_tile:
+       crack_tile->drop();
+       return result;
+}
+
 static void draw_crack(video::IImage *crack, video::IImage *dst,
                bool use_overlay, s32 frame_count, s32 progression,
-               video::IVideoDriver *driver)
+               video::IVideoDriver *driver, u8 tiles)
 {
        // Dimension of destination image
        core::dimension2d<u32> dim_dst = dst->getDimension();
-       // Dimension of original image
-       core::dimension2d<u32> dim_crack = crack->getDimension();
-       // Count of crack stages
-       s32 crack_count = dim_crack.Height / dim_crack.Width;
        // Limit frame_count
        if (frame_count > (s32) dim_dst.Height)
                frame_count = dim_dst.Height;
        if (frame_count < 1)
                frame_count = 1;
-       // Limit progression
-       if (progression > crack_count-1)
-               progression = crack_count-1;
-       // Dimension of a single crack stage
-       core::dimension2d<u32> dim_crack_cropped(
-               dim_crack.Width,
-               dim_crack.Width
-       );
        // Dimension of the scaled crack stage,
        // which is the same as the dimension of a single destination frame
-       core::dimension2d<u32> dim_crack_scaled(
+       core::dimension2d<u32> frame_size(
                dim_dst.Width,
                dim_dst.Height / frame_count
        );
-       // Create cropped and scaled crack images
-       video::IImage *crack_cropped = driver->createImage(
-                       video::ECF_A8R8G8B8, dim_crack_cropped);
-       video::IImage *crack_scaled = driver->createImage(
-                       video::ECF_A8R8G8B8, dim_crack_scaled);
+       video::IImage *crack_scaled = create_crack_image(crack, progression,
+                       frame_size, tiles, driver);
+       if (!crack_scaled)
+               return;
 
-       if (crack_cropped && crack_scaled)
-       {
-               // Crop crack image
-               v2s32 pos_crack(0, progression*dim_crack.Width);
-               crack->copyTo(crack_cropped,
-                               v2s32(0,0),
-                               core::rect<s32>(pos_crack, dim_crack_cropped));
-               // Scale crack image by copying
-               crack_cropped->copyToScaling(crack_scaled);
-               // Copy or overlay crack image onto each frame
-               for (s32 i = 0; i < frame_count; ++i)
-               {
-                       v2s32 dst_pos(0, dim_crack_scaled.Height * i);
-                       if (use_overlay)
-                       {
-                               blit_with_alpha_overlay(crack_scaled, dst,
-                                               v2s32(0,0), dst_pos,
-                                               dim_crack_scaled);
-                       }
-                       else
-                       {
-                               blit_with_alpha(crack_scaled, dst,
-                                               v2s32(0,0), dst_pos,
-                                               dim_crack_scaled);
-                       }
-               }
+       auto blit = use_overlay ? blit_with_alpha_overlay : blit_with_alpha;
+       for (s32 i = 0; i < frame_count; ++i) {
+               v2s32 dst_pos(0, frame_size.Height * i);
+               blit(crack_scaled, dst, v2s32(0,0), dst_pos, frame_size);
        }
 
-       if (crack_scaled)
-               crack_scaled->drop();
-
-       if (crack_cropped)
-               crack_cropped->drop();
+       crack_scaled->drop();
 }
 
 void brighten(video::IImage *image)
@@ -1908,9 +2116,8 @@ u32 parseImageTransform(const std::string& s)
                                pos++;
                                break;
                        }
-                       else if (!(name_i.empty()) &&
-                               lowercase(s.substr(pos, name_i.size())) == name_i)
-                       {
+
+                       if (!(name_i.empty()) && lowercase(s.substr(pos, name_i.size())) == name_i) {
                                transform = i;
                                pos += name_i.size();
                                break;
@@ -1937,8 +2144,8 @@ core::dimension2d<u32> imageTransformDimension(u32 transform, core::dimension2d<
 {
        if (transform % 2 == 0)
                return dim;
-       else
-               return core::dimension2d<u32>(dim.Height, dim.Width);
+
+       return core::dimension2d<u32>(dim.Height, dim.Width);
 }
 
 void imageTransform(u32 transform, video::IImage *src, video::IImage *dst)
@@ -1988,21 +2195,85 @@ void imageTransform(u32 transform, video::IImage *src, video::IImage *dst)
 
 video::ITexture* TextureSource::getNormalTexture(const std::string &name)
 {
-       u32 id;
        if (isKnownSourceImage("override_normal.png"))
-               return getTexture("override_normal.png", &id);
+               return getTexture("override_normal.png");
        std::string fname_base = name;
-       std::string normal_ext = "_normal.png";
-       size_t pos = fname_base.find(".");
+       static const char *normal_ext = "_normal.png";
+       static const u32 normal_ext_size = strlen(normal_ext);
+       size_t pos = fname_base.find('.');
        std::string fname_normal = fname_base.substr(0, pos) + normal_ext;
        if (isKnownSourceImage(fname_normal)) {
                // look for image extension and replace it
                size_t i = 0;
-               while ((i = fname_base.find(".", i)) != std::string::npos) {
+               while ((i = fname_base.find('.', i)) != std::string::npos) {
                        fname_base.replace(i, 4, normal_ext);
-                       i += normal_ext.length();
-               }
-               return getTexture(fname_base, &id);
+                       i += normal_ext_size;
                }
+               return getTexture(fname_base);
+       }
        return NULL;
 }
+
+video::SColor TextureSource::getTextureAverageColor(const std::string &name)
+{
+       video::IVideoDriver *driver = RenderingEngine::get_video_driver();
+       video::SColor c(0, 0, 0, 0);
+       video::ITexture *texture = getTexture(name);
+       video::IImage *image = driver->createImage(texture,
+               core::position2d<s32>(0, 0),
+               texture->getOriginalSize());
+       u32 total = 0;
+       u32 tR = 0;
+       u32 tG = 0;
+       u32 tB = 0;
+       core::dimension2d<u32> dim = image->getDimension();
+       u16 step = 1;
+       if (dim.Width > 16)
+               step = dim.Width / 16;
+       for (u16 x = 0; x < dim.Width; x += step) {
+               for (u16 y = 0; y < dim.Width; y += step) {
+                       c = image->getPixel(x,y);
+                       if (c.getAlpha() > 0) {
+                               total++;
+                               tR += c.getRed();
+                               tG += c.getGreen();
+                               tB += c.getBlue();
+                       }
+               }
+       }
+       image->drop();
+       if (total > 0) {
+               c.setRed(tR / total);
+               c.setGreen(tG / total);
+               c.setBlue(tB / total);
+       }
+       c.setAlpha(255);
+       return c;
+}
+
+
+video::ITexture *TextureSource::getShaderFlagsTexture(bool normalmap_present)
+{
+       std::string tname = "__shaderFlagsTexture";
+       tname += normalmap_present ? "1" : "0";
+
+       if (isKnownSourceImage(tname)) {
+               return getTexture(tname);
+       }
+
+       video::IVideoDriver *driver = RenderingEngine::get_video_driver();
+       video::IImage *flags_image = driver->createImage(
+               video::ECF_A8R8G8B8, core::dimension2d<u32>(1, 1));
+       sanity_check(flags_image != NULL);
+       video::SColor c(255, normalmap_present ? 255 : 0, 0, 0);
+       flags_image->setPixel(0, 0, c);
+       insertSourceImage(tname, flags_image);
+       flags_image->drop();
+       return getTexture(tname);
+
+}
+
+std::vector<std::string> getTextureDirs()
+{
+       return fs::GetRecursiveDirs(g_settings->get("texture_path"));
+}