#include "GraphicsResources.h" #include #include #include "Alloc.h" #include "FileSystemUtils.h" #include "Graphics.h" #include "GraphicsUtil.h" #include "Localization.h" #include "Vlogging.h" #include "Screen.h" #include "XMLUtils.h" // Used to load PNG data extern "C" { extern unsigned lodepng_decode32( unsigned char** out, unsigned* w, unsigned* h, const unsigned char* in, size_t insize ); extern unsigned lodepng_encode24( unsigned char** out, size_t* outsize, const unsigned char* image, unsigned w, unsigned h ); extern const char* lodepng_error_text(unsigned code); } static SDL_Surface* LoadImageRaw(const char* filename, unsigned char** data) { *data = NULL; // Temporary storage for the image that's loaded SDL_Surface* loadedImage = NULL; unsigned int width, height; unsigned int error; unsigned char* fileIn; size_t length; FILESYSTEM_loadAssetToMemory(filename, &fileIn, &length); if (fileIn == NULL) { SDL_assert(0 && "Image file missing!"); return NULL; } error = lodepng_decode32(data, &width, &height, fileIn, length); VVV_free(fileIn); if (error != 0) { vlog_error("Could not load %s: %s", filename, lodepng_error_text(error)); return NULL; } loadedImage = SDL_CreateRGBSurfaceWithFormatFrom( *data, width, height, 32, width * 4, #if SDL_BYTEORDER == SDL_BIG_ENDIAN SDL_PIXELFORMAT_RGBA8888 #else SDL_PIXELFORMAT_ABGR8888 #endif ); return loadedImage; } static SDL_Surface* LoadSurfaceFromRaw(SDL_Surface* loadedImage) { SDL_Surface* optimizedImage = SDL_ConvertSurfaceFormat( loadedImage, SDL_PIXELFORMAT_ARGB8888, 0 ); SDL_SetSurfaceBlendMode(optimizedImage, SDL_BLENDMODE_BLEND); return optimizedImage; } SDL_Surface* LoadImageSurface(const char* filename) { unsigned char* data; SDL_Surface* loadedImage = LoadImageRaw(filename, &data); SDL_Surface* optimizedImage = LoadSurfaceFromRaw(loadedImage); if (loadedImage != NULL) { VVV_freefunc(SDL_FreeSurface, loadedImage); } VVV_free(data); if (optimizedImage == NULL) { vlog_error("Image not found: %s", filename); SDL_assert(0 && "Image not found! See stderr."); } return optimizedImage; } static SDL_Texture* LoadTextureFromRaw(const char* filename, SDL_Surface* loadedImage, const TextureLoadType loadtype) { if (loadedImage == NULL) { return NULL; } // Modify the surface with the load type. // This could be done in LoadImageRaw, however currently, surfaces are only used for // pixel perfect collision (which will be changed later) and the window icon. switch (loadtype) { case TEX_WHITE: SDL_LockSurface(loadedImage); for (int y = 0; y < loadedImage->h; y++) { for (int x = 0; x < loadedImage->w; x++) { SDL_Color color = ReadPixel(loadedImage, x, y); color.r = 255; color.g = 255; color.b = 255; DrawPixel(loadedImage, x, y, color); } } SDL_UnlockSurface(loadedImage); break; case TEX_GRAYSCALE: SDL_LockSurface(loadedImage); for (int y = 0; y < loadedImage->h; y++) { for (int x = 0; x < loadedImage->w; x++) { SDL_Color color = ReadPixel(loadedImage, x, y); // Magic numbers used for grayscaling (eyes perceive certain colors brighter than others) Uint8 r = color.r * 0.299; Uint8 g = color.g * 0.587; Uint8 b = color.b * 0.114; const double gray = SDL_floor(r + g + b + 0.5); color.r = gray; color.g = gray; color.b = gray; DrawPixel(loadedImage, x, y, color); } } SDL_UnlockSurface(loadedImage); break; default: break; } //Create texture from surface pixels SDL_Texture* texture = SDL_CreateTextureFromSurface(gameScreen.m_renderer, loadedImage); if (texture == NULL) { vlog_error("Failed creating texture: %s. SDL error: %s\n", filename, SDL_GetError()); } return texture; } SDL_Texture* LoadImage(const char *filename, const TextureLoadType loadtype) { unsigned char* data; SDL_Surface* loadedImage = LoadImageRaw(filename, &data); SDL_Texture* texture = LoadTextureFromRaw(filename, loadedImage, loadtype); if (loadedImage != NULL) { VVV_freefunc(SDL_FreeSurface, loadedImage); } VVV_free(data); if (texture == NULL) { vlog_error("Image not found: %s", filename); SDL_assert(0 && "Image not found! See stderr."); } return texture; } static SDL_Texture* LoadImage(const char* filename) { return LoadImage(filename, TEX_COLOR); } /* Any unneeded variants can be NULL */ static void LoadVariants(const char* filename, SDL_Texture** colored, SDL_Texture** white, SDL_Texture** grayscale) { unsigned char* data; SDL_Surface* loadedImage = LoadImageRaw(filename, &data); if (colored != NULL) { *colored = LoadTextureFromRaw(filename, loadedImage, TEX_COLOR); if (*colored == NULL) { vlog_error("Image not found: %s", filename); SDL_assert(0 && "Image not found! See stderr."); } } if (grayscale != NULL) { *grayscale = LoadTextureFromRaw(filename, loadedImage, TEX_GRAYSCALE); if (*grayscale == NULL) { vlog_error("Image not found: %s", filename); SDL_assert(0 && "Image not found! See stderr."); } } if (white != NULL) { *white = LoadTextureFromRaw(filename, loadedImage, TEX_WHITE); if (*white == NULL) { vlog_error("Image not found: %s", filename); SDL_assert(0 && "Image not found! See stderr."); } } if (loadedImage != NULL) { VVV_freefunc(SDL_FreeSurface, loadedImage); } VVV_free(data); } /* The pointers `texture` and `surface` cannot be NULL */ static void LoadSprites(const char* filename, SDL_Texture** texture, SDL_Surface** surface) { unsigned char* data; SDL_Surface* loadedImage = LoadImageRaw(filename, &data); *surface = LoadSurfaceFromRaw(loadedImage); if (*surface == NULL) { vlog_error("Image not found: %s", filename); SDL_assert(0 && "Image not found! See stderr."); } *texture = LoadTextureFromRaw(filename, loadedImage, TEX_WHITE); if (*texture == NULL) { vlog_error("Image not found: %s", filename); SDL_assert(0 && "Image not found! See stderr."); } if (loadedImage != NULL) { VVV_freefunc(SDL_FreeSurface, loadedImage); } VVV_free(data); } static void LoadSpritesTranslation( const char* filename, tinyxml2::XMLDocument* mask, SDL_Surface* surface_english, SDL_Texture** texture ) { /* Create a sprites texture for display in another language. * surface_english is used as a base. Parts of the translation (filename) * will replace parts of the base, as instructed in the mask XML. */ if (surface_english == NULL) { vlog_error("LoadSpritesTranslation: English surface is NULL!"); return; } // Make a copy of the English sprites, for working with SDL_Surface* working = GetSubSurface( surface_english, 0, 0, surface_english->w, surface_english->h ); if (working == NULL) { return; } SDL_Surface* translated; { unsigned char* data; SDL_Surface* loaded_image = LoadImageRaw(filename, &data); translated = LoadSurfaceFromRaw(loaded_image); VVV_freefunc(SDL_FreeSurface, loaded_image); VVV_free(data); } SDL_SetSurfaceBlendMode(translated, SDL_BLENDMODE_NONE); tinyxml2::XMLHandle hMask(mask); tinyxml2::XMLElement* pElem; int sprite_w = 1, sprite_h = 1; if ((pElem = mask->FirstChildElement()) != NULL) { sprite_w = pElem->IntAttribute("sprite_w", 1); sprite_h = pElem->IntAttribute("sprite_h", 1); } FOR_EACH_XML_ELEMENT(hMask, pElem) { EXPECT_ELEM(pElem, "sprite"); int x = pElem->IntAttribute("x", 0); int y = pElem->IntAttribute("y", 0); SDL_Rect src; src.x = x * sprite_w; src.y = y * sprite_h; src.w = pElem->IntAttribute("w", 1) * sprite_w; src.h = pElem->IntAttribute("h", 1) * sprite_h; SDL_Rect dst; dst.x = pElem->IntAttribute("dx", x) * sprite_w; dst.y = pElem->IntAttribute("dy", y) * sprite_h; SDL_BlitSurface(translated, &src, working, &dst); } *texture = LoadTextureFromRaw(filename, working, TEX_WHITE); VVV_freefunc(SDL_FreeSurface, translated); VVV_freefunc(SDL_FreeSurface, working); } void GraphicsResources::init_translations(void) { VVV_freefunc(SDL_DestroyTexture, im_sprites_translated); VVV_freefunc(SDL_DestroyTexture, im_flipsprites_translated); if (loc::english_sprites) { return; } const char* langcode = loc::lang.c_str(); const char* path_template = "lang/%s/graphics/%s"; char path_xml[256]; char path_sprites[256]; char path_flipsprites[256]; SDL_snprintf(path_xml, sizeof(path_xml), path_template, langcode, "spritesmask.xml"); SDL_snprintf(path_sprites, sizeof(path_sprites), path_template, langcode, "sprites.png"); SDL_snprintf(path_flipsprites, sizeof(path_flipsprites), path_template, langcode, "flipsprites.png"); /* We don't want to apply main-game translations to level-specific (custom) sprites. * Either sprites and translations are BOTH main-game, or BOTH level-specific. * Our pivots are the XML (it _has_ to exist for translated sprites to work) and * graphics/sprites.png (what sense does it make to have only flipsprites). */ if (FILESYSTEM_isAssetMounted(path_xml) != FILESYSTEM_isAssetMounted("graphics/sprites.png")) { return; } tinyxml2::XMLDocument doc_mask; if (!FILESYSTEM_loadAssetTiXml2Document(path_xml, doc_mask)) { // Only try to load the images if the XML document exists return; } if (FILESYSTEM_areAssetsInSameRealDir(path_xml, path_sprites)) { LoadSpritesTranslation( path_sprites, &doc_mask, im_sprites_surf, &im_sprites_translated ); } if (FILESYSTEM_areAssetsInSameRealDir(path_xml, path_flipsprites)) { LoadSpritesTranslation( path_flipsprites, &doc_mask, im_flipsprites_surf, &im_flipsprites_translated ); } } void GraphicsResources::init(void) { LoadVariants("graphics/tiles.png", &im_tiles, &im_tiles_white, &im_tiles_tint); LoadVariants("graphics/tiles2.png", &im_tiles2, NULL, &im_tiles2_tint); LoadVariants("graphics/entcolours.png", &im_entcolours, NULL, &im_entcolours_tint); LoadSprites("graphics/sprites.png", &im_sprites, &im_sprites_surf); LoadSprites("graphics/flipsprites.png", &im_flipsprites, &im_flipsprites_surf); im_tiles3 = LoadImage("graphics/tiles3.png"); im_teleporter = LoadImage("graphics/teleporter.png", TEX_WHITE); im_image0 = LoadImage("graphics/levelcomplete.png"); im_image1 = LoadImage("graphics/minimap.png"); im_image2 = LoadImage("graphics/covered.png"); im_image3 = LoadImage("graphics/elephant.png", TEX_WHITE); im_image4 = LoadImage("graphics/gamecomplete.png"); im_image5 = LoadImage("graphics/fliplevelcomplete.png"); im_image6 = LoadImage("graphics/flipgamecomplete.png"); im_image7 = LoadImage("graphics/site.png", TEX_WHITE); im_image8 = LoadImage("graphics/site2.png", TEX_WHITE); im_image9 = LoadImage("graphics/site3.png", TEX_WHITE); im_image10 = LoadImage("graphics/ending.png"); im_image11 = LoadImage("graphics/site4.png", TEX_WHITE); im_button_left = LoadImage("graphics/buttons/button_left.png"); im_button_right = LoadImage("graphics/buttons/button_right.png"); im_button_map = LoadImage("graphics/buttons/button_map.png"); im_sprites_translated = NULL; im_flipsprites_translated = NULL; init_translations(); im_image12 = SDL_CreateTexture(gameScreen.m_renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_TARGET, 240, 180); if (im_image12 == NULL) { vlog_error("Failed to create minimap texture: %s", SDL_GetError()); SDL_assert(0 && "Failed to create minimap texture! See stderr."); return; } } void GraphicsResources::destroy(void) { #define CLEAR(img) VVV_freefunc(SDL_DestroyTexture, img) CLEAR(im_tiles); CLEAR(im_tiles_white); CLEAR(im_tiles_tint); CLEAR(im_tiles2); CLEAR(im_tiles2_tint); CLEAR(im_tiles3); CLEAR(im_entcolours); CLEAR(im_entcolours_tint); CLEAR(im_sprites); CLEAR(im_flipsprites); CLEAR(im_teleporter); CLEAR(im_image0); CLEAR(im_image1); CLEAR(im_image2); CLEAR(im_image3); CLEAR(im_image4); CLEAR(im_image5); CLEAR(im_image6); CLEAR(im_image7); CLEAR(im_image8); CLEAR(im_image9); CLEAR(im_image10); CLEAR(im_image11); CLEAR(im_image12); CLEAR(im_sprites_translated); CLEAR(im_flipsprites_translated); CLEAR(im_button_left); CLEAR(im_button_right); CLEAR(im_button_map); #undef CLEAR VVV_freefunc(SDL_FreeSurface, im_sprites_surf); VVV_freefunc(SDL_FreeSurface, im_flipsprites_surf); } bool SaveImage(const SDL_Surface* surface, const char* filename) { unsigned char* out; size_t outsize; unsigned int error; bool success; error = lodepng_encode24( &out, &outsize, (const unsigned char*) surface->pixels, surface->w, surface->h ); if (error != 0) { vlog_error("Could not save image: %s", lodepng_error_text(error)); return false; } success = FILESYSTEM_saveFile(filename, out, outsize); SDL_free(out); if (!success) { vlog_error("Could not save image"); } return success; } bool SaveScreenshot(void) { static time_t last_time = 0; static int subsecond_counter = 0; bool success = TakeScreenshot(&graphics.tempScreenshot); if (!success) { vlog_error("Could not take screenshot"); return false; } const time_t now = time(NULL); const tm* date = localtime(&now); if (now != last_time) { last_time = now; subsecond_counter = 0; } subsecond_counter++; char timestamp[32]; strftime(timestamp, sizeof(timestamp), "%Y-%m-%d_%H-%M-%S", date); char name[32]; if (subsecond_counter > 1) { SDL_snprintf(name, sizeof(name), "%s_%i", timestamp, subsecond_counter); } else { SDL_strlcpy(name, timestamp, sizeof(name)); } char filename[64]; SDL_snprintf(filename, sizeof(filename), "screenshots/1x/%s_1x.png", name); success = SaveImage(graphics.tempScreenshot, filename); if (!success) { return false; } success = UpscaleScreenshot2x(graphics.tempScreenshot, &graphics.tempScreenshot2x); if (!success) { vlog_error("Could not upscale screenshot to 2x"); return false; } SDL_snprintf(filename, sizeof(filename), "screenshots/2x/%s_2x.png", name); success = SaveImage(graphics.tempScreenshot2x, filename); if (!success) { return false; } vlog_info("Saved screenshot %s", name); return true; }