summary refs log tree commit diff
path: root/src/TexturePack.c
diff options
context:
space:
mode:
authorWlodekM <[email protected]>2024-06-16 10:35:45 +0300
committerWlodekM <[email protected]>2024-06-16 10:35:45 +0300
commitabef6da56913f1c55528103e60a50451a39628b1 (patch)
treeb3c8092471ecbb73e568cd0d336efa0e7871ee8d /src/TexturePack.c
initial commit
Diffstat (limited to 'src/TexturePack.c')
-rw-r--r--src/TexturePack.c667
1 files changed, 667 insertions, 0 deletions
diff --git a/src/TexturePack.c b/src/TexturePack.c
new file mode 100644
index 0000000..c97cfc1
--- /dev/null
+++ b/src/TexturePack.c
@@ -0,0 +1,667 @@
+#include "TexturePack.h"
+#include "String.h"
+#include "Constants.h"
+#include "Stream.h"
+#include "World.h"
+#include "Graphics.h"
+#include "Event.h"
+#include "Game.h"
+#include "Http.h"
+#include "Platform.h"
+#include "Deflate.h"
+#include "Funcs.h"
+#include "ExtMath.h"
+#include "Options.h"
+#include "Logger.h"
+#include "Utils.h"
+#include "Chat.h" /* TODO avoid this include */
+#include "Errors.h"
+
+/* Simple fallback terrain for when no texture packs are available at all */
+static BitmapCol fallback_terrain[16 * 8] = {
+	BitmapColor_RGB( 96, 144,  85), BitmapColor_RGB(129, 128, 127), BitmapColor_RGB(123,  87,  66), BitmapColor_RGB(174, 124,  74), BitmapColor_RGB(184, 151, 105), BitmapColor_RGB(200, 200, 197), BitmapColor_RGB(175, 173, 173), BitmapColor_RGB(153, 101,  75), 
+	BitmapColor_RGB(118, 111, 101), BitmapColor_RGB( 61,  20,  11), BitmapColor_RGB(179,  67,  23), BitmapColor_RGB(154, 128,  89), BitmapColor_RGB(163,   2,  29), BitmapColor_RGB(203, 206,   2), BitmapCol_Make(86,144,216,128), BitmapColor_RGB( 38,  88,  41),
+	/* 16*/
+	BitmapColor_RGB(165, 163, 159), BitmapColor_RGB( 37,  48,  61), BitmapColor_RGB(227, 223, 151), BitmapColor_RGB(160, 152, 147), BitmapColor_RGB( 90,  71,  58), BitmapColor_RGB(173, 135,  87), BitmapColor_RGB( 38,  98,  37), BitmapColor_RGB(225, 229, 235), 
+	BitmapColor_RGB(246, 231,  23), BitmapColor_RGB(225, 218, 157), BitmapColor_RGB(247, 243, 234), BitmapColor_RGB( 57, 115, 158), BitmapColor_RGB(226,  18,  18), BitmapColor_RGB(172, 131, 101), BitmapColor_RGB(255, 122,  31), BitmapColor_RGB( 79, 120,  79),
+	/* 32 */
+	BitmapColor_RGB(129, 128, 127), BitmapColor_RGB(189, 151, 134), BitmapColor_RGB( 53,  44,  61), BitmapColor_RGB(180, 151, 102), BitmapColor_RGB(165, 163, 159), BitmapColor_RGB( 20,  20,  33), BitmapColor_RGB(243, 139,  28), BitmapColor_RGB(193, 197, 202), 
+	BitmapColor_RGB(235, 188,  32), BitmapColor_RGB(203, 193, 135), BitmapColor_RGB(224, 220, 212), BitmapColor_RGB( 52,  90, 134), BitmapColor_RGB( 57, 115, 158), BitmapColor_RGB( 52,  90, 134), BitmapColor_RGB( 57, 115, 158), BitmapColor_RGB(174, 124,  74),
+	/* 48 */
+	BitmapColor_RGB(175, 148,  43), BitmapColor_RGB(188, 225, 231), BitmapColor_RGB(238, 245, 245), BitmapCol_Make(205,232,252,128),BitmapColor_RGB(153, 150, 149), BitmapColor_RGB(105,  80,  54), BitmapColor_RGB(236, 236, 240), BitmapColor_RGB(161, 165, 170),
+	BitmapColor_RGB(225, 146,  30), BitmapColor_RGB(203, 193, 135), BitmapColor_RGB(247, 243, 234), BitmapColor_RGB( 57, 115, 158), BitmapColor_RGB( 52,  90, 134), BitmapColor_RGB( 57, 115, 158), BitmapColor_RGB( 52,  90, 134), BitmapColor_RGB( 57, 115, 158),
+	/* 64 */
+	BitmapColor_RGB(217,  35,  35), BitmapColor_RGB(219, 137,  13), BitmapColor_RGB(224, 224,   0), BitmapColor_RGB(128, 221,   2), BitmapColor_RGB( 13, 217,  13), BitmapColor_RGB(  8, 218, 133), BitmapColor_RGB(  4, 219, 219), BitmapColor_RGB( 89, 175, 219),
+	BitmapColor_RGB(122, 122, 217), BitmapColor_RGB(131,  39, 225), BitmapColor_RGB(178,  69, 230), BitmapColor_RGB(227,  52, 227), BitmapColor_RGB(227,  41, 133), BitmapColor_RGB( 73,  73,  73), BitmapColor_RGB(151, 151, 151), BitmapColor_RGB(227, 227, 227),
+	/* 80 */
+	BitmapColor_RGB(220, 127, 162), BitmapColor_RGB( 42,  66,   8), BitmapColor_RGB( 75,  37,  11), BitmapColor_RGB( 24,  37, 149), BitmapColor_RGB( 29, 113, 149), BitmapColor_RGB(155, 161, 174), BitmapColor_RGB(167,  41,  13), BitmapColor_RGB( 57, 115, 158), 
+	BitmapColor_RGB( 52,  90, 134), BitmapColor_RGB( 57, 115, 158), BitmapColor_RGB( 52,  90, 134), BitmapColor_RGB( 57, 115, 158), BitmapColor_RGB( 52,  90, 134), BitmapColor_RGB( 57, 115, 158), BitmapColor_RGB( 52,  90, 134), BitmapColor_RGB( 57, 115, 158),
+	/* 96 */
+	BitmapColor_RGB( 57, 115, 158), BitmapColor_RGB( 52,  90, 134), BitmapColor_RGB( 57, 115, 158), BitmapColor_RGB( 52,  90, 134), BitmapColor_RGB( 57, 115, 158), BitmapColor_RGB( 52,  90, 134), BitmapColor_RGB( 57, 115, 158), BitmapColor_RGB( 52,  90, 134), 
+	BitmapColor_RGB( 57, 115, 158), BitmapColor_RGB( 52,  90, 134), BitmapColor_RGB( 57, 115, 158), BitmapColor_RGB( 52,  90, 134), BitmapColor_RGB( 57, 115, 158), BitmapColor_RGB( 52,  90, 134), BitmapColor_RGB( 57, 115, 158), BitmapColor_RGB( 52,  90, 134),
+	/* 112 */
+	BitmapColor_RGB( 52,  90, 134), BitmapColor_RGB( 57, 115, 158), BitmapColor_RGB( 52,  90, 134), BitmapColor_RGB( 57, 115, 158), BitmapColor_RGB( 52,  90, 134), BitmapColor_RGB( 57, 115, 158), BitmapColor_RGB( 52,  90, 134), BitmapColor_RGB( 57, 115, 158), 
+	BitmapColor_RGB( 52,  90, 134), BitmapColor_RGB( 57, 115, 158), BitmapColor_RGB( 52,  90, 134), BitmapColor_RGB( 57, 115, 158), BitmapColor_RGB( 52,  90, 134), BitmapColor_RGB( 57, 115, 158), BitmapColor_RGB( 52,  90, 134), BitmapColor_RGB( 57, 115, 158),
+};
+
+static void LoadFallbackAtlas(void) {
+	struct Bitmap bmp, src;
+	src.width  = 16;
+	src.height = 8;
+	src.scan0  = fallback_terrain;
+	
+	if (Gfx.MinTexWidth || Gfx.MinTexHeight) {
+		Bitmap_Allocate(&bmp, 16 * Gfx.MinTexWidth, 8 * Gfx.MinTexHeight);
+		Bitmap_Scale(&bmp, &src, 0, 0, 16, 8);
+		Atlas_TryChange(&bmp);
+	} else {
+		Atlas_TryChange(&src);
+	}
+}
+
+/*########################################################################################################################*
+*------------------------------------------------------TerrainAtlas-------------------------------------------------------*
+*#########################################################################################################################*/
+struct _Atlas2DData Atlas2D;
+struct _Atlas1DData Atlas1D;
+int TexturePack_ReqID;
+
+TextureRec Atlas1D_TexRec(TextureLoc texLoc, int uCount, int* index) {
+	TextureRec rec;
+	int y  = Atlas1D_RowId(texLoc);
+	*index = Atlas1D_Index(texLoc);
+
+	/* Adjust coords to be slightly inside - fixes issues with AMD/ATI cards */	
+	rec.u1 = 0.0f; 
+	rec.v1 = y * Atlas1D.InvTileSize;
+	rec.u2 = (uCount - 1) + UV2_Scale;
+	rec.v2 = rec.v1       + UV2_Scale * Atlas1D.InvTileSize;
+	return rec;
+}
+
+static void Atlas_Convert2DTo1D(void) {
+	int tileSize      = Atlas2D.TileSize;
+	int tilesPerAtlas = Atlas1D.TilesPerAtlas;
+	int atlasesCount  = Atlas1D.Count;
+	struct Bitmap atlas1D;
+	int atlasX, atlasY;
+	int tile = 0, i, y;
+
+	Platform_Log2("Loaded terrain atlas: %i bmps, %i per bmp", &atlasesCount, &tilesPerAtlas);
+	Bitmap_Allocate(&atlas1D, tileSize, tilesPerAtlas * tileSize);
+	
+	for (i = 0; i < atlasesCount; i++) {
+		for (y = 0; y < tilesPerAtlas; y++, tile++) {
+			atlasX = Atlas2D_TileX(tile) * tileSize;
+			atlasY = Atlas2D_TileY(tile) * tileSize;
+
+			Bitmap_UNSAFE_CopyBlock(atlasX, atlasY, 0, y * tileSize,
+								&Atlas2D.Bmp, &atlas1D, tileSize);
+		}
+		Gfx_RecreateTexture(&Atlas1D.TexIds[i], &atlas1D, TEXTURE_FLAG_MANAGED | TEXTURE_FLAG_DYNAMIC, Gfx.Mipmaps);
+	}
+	Mem_Free(atlas1D.scan0);
+}
+
+static void Atlas_Update1D(void) {
+	int maxAtlasHeight, maxTilesPerAtlas, maxTiles;
+	int maxTexHeight = Gfx.MaxTexHeight;
+
+	/* E.g. a graphics backend may support textures up to 256 x 256 */
+	/*   dimension wise, but only have enough storage for 16 x 256 */
+	if (Gfx.MaxTexSize) {
+		int maxCurHeight = Gfx.MaxTexSize / Atlas2D.TileSize;
+		maxTexHeight     = min(maxTexHeight, maxCurHeight);
+	}
+
+	maxAtlasHeight   = min(4096, maxTexHeight);
+	maxTilesPerAtlas = maxAtlasHeight / Atlas2D.TileSize;
+	maxTiles         = Atlas2D.RowsCount * ATLAS2D_TILES_PER_ROW;
+
+	Atlas1D.TilesPerAtlas = min(maxTilesPerAtlas, maxTiles);
+	Atlas1D.Count = Math_CeilDiv(maxTiles, Atlas1D.TilesPerAtlas);
+
+	Atlas1D.InvTileSize = 1.0f / Atlas1D.TilesPerAtlas;
+	Atlas1D.Mask  = Atlas1D.TilesPerAtlas - 1;
+	Atlas1D.Shift = Math_ilog2(Atlas1D.TilesPerAtlas);
+}
+
+/* Loads the given atlas and converts it into an array of 1D atlases. */
+static void Atlas_Update(struct Bitmap* bmp) {
+	Atlas2D.Bmp       = *bmp;
+	Atlas2D.TileSize  = bmp->width  / ATLAS2D_TILES_PER_ROW;
+	Atlas2D.RowsCount = bmp->height / Atlas2D.TileSize;
+	Atlas2D.RowsCount = min(Atlas2D.RowsCount, ATLAS2D_MAX_ROWS_COUNT);
+
+	Atlas_Update1D();
+	Atlas_Convert2DTo1D();
+}
+
+GfxResourceID Atlas2D_LoadTile(TextureLoc texLoc) {
+	int size = Atlas2D.TileSize;
+	struct Bitmap tile;
+
+	int x = Atlas2D_TileX(texLoc), y = Atlas2D_TileY(texLoc);
+	if (y >= Atlas2D.RowsCount) return 0;
+
+	tile.scan0  = Bitmap_GetRow(&Atlas2D.Bmp, y * size) + (x * size);
+	tile.width  = size;
+	tile.height = size;
+	return Gfx_CreateTexture2(&tile, Atlas2D.Bmp.width, 0, Gfx.Mipmaps);
+}
+
+static void Atlas2D_Free(void) {
+	if (Atlas2D.Bmp.scan0 != fallback_terrain)
+		Mem_Free(Atlas2D.Bmp.scan0);
+
+	Atlas2D.Bmp.scan0 = NULL;
+	Atlas2D.RowsCount = 0;
+}
+
+static void Atlas1D_Free(void) {
+	int i;
+	for (i = 0; i < Atlas1D.Count; i++) {
+		Gfx_DeleteTexture(&Atlas1D.TexIds[i]);
+	}
+}
+
+cc_bool Atlas_TryChange(struct Bitmap* atlas) {
+	static const cc_string terrain = String_FromConst("terrain.png");
+	int tileSize;
+
+	if (!Game_ValidateBitmapPow2(&terrain, atlas)) return false;
+	tileSize = atlas->width / ATLAS2D_TILES_PER_ROW;
+
+	if (tileSize <= 0) {
+		Chat_AddRaw("&cUnable to use terrain.png from the texture pack.");
+		Chat_AddRaw("&c It must be 16 or more pixels wide.");
+		return false;
+	}
+	if (atlas->height < tileSize) {
+		Chat_AddRaw("&cUnable to use terrain.png from the texture pack.");
+		Chat_AddRaw("&c It must have at least one row in it.");
+		return false;
+	}
+
+	if (!Gfx_CheckTextureSize(tileSize, tileSize, 0)) {
+		Chat_AddRaw("&cUnable to use terrain.png from the texture pack.");
+		Chat_Add4("&c Tile size is (%i,%i), your GPU supports (%i,%i) at most.", 
+			&tileSize, &tileSize, &Gfx.MaxTexWidth, &Gfx.MaxTexHeight);
+		return false;
+	}
+
+	if (atlas->height < atlas->width) {
+		/* Probably wouldn't want to use these, but you still can technically */
+		Chat_AddRaw("&cHeight of terrain.png is less than its width.");
+		Chat_AddRaw("&c Some tiles will therefore appear completely white.");
+	}
+	if (atlas->width > Gfx.MaxTexWidth) {
+		/* Super HD textures probably won't work great on this GPU */
+		Chat_AddRaw("&cYou may experience significantly reduced performance.");
+		Chat_Add4("&c terrain.png size is (%i,%i), your GPU supports (%i,%i) at most.", 
+			&atlas->width, &atlas->height, &Gfx.MaxTexWidth, &Gfx.MaxTexHeight);
+	}
+
+	if (Gfx.LostContext) return false;
+	Atlas1D_Free();
+	Atlas2D_Free();
+
+	Atlas_Update(atlas);
+	Event_RaiseVoid(&TextureEvents.AtlasChanged);
+	return true;
+}
+
+
+/*########################################################################################################################*
+*------------------------------------------------------TextureCache-------------------------------------------------------*
+*#########################################################################################################################*/
+static struct StringsBuffer acceptedList, deniedList, etagCache, lastModCache;
+#define ACCEPTED_TXT "texturecache/acceptedurls.txt"
+#define DENIED_TXT   "texturecache/deniedurls.txt"
+#define ETAGS_TXT    "texturecache/etags.txt"
+#define LASTMOD_TXT  "texturecache/lastmodified.txt"
+
+/* Initialises cache state (loading various lists) */
+static void TextureCache_Init(void) {
+	EntryList_UNSAFE_Load(&acceptedList, ACCEPTED_TXT);
+	EntryList_UNSAFE_Load(&deniedList,   DENIED_TXT);
+	EntryList_UNSAFE_Load(&etagCache,    ETAGS_TXT);
+	EntryList_UNSAFE_Load(&lastModCache, LASTMOD_TXT);
+}
+
+cc_bool TextureCache_HasAccepted(const cc_string* url) { return EntryList_Find(&acceptedList, url, ' ') >= 0; }
+cc_bool TextureCache_HasDenied(const cc_string* url)   { return EntryList_Find(&deniedList,   url, ' ') >= 0; }
+
+void TextureCache_Accept(const cc_string* url) {
+	EntryList_Set(&acceptedList, url, &String_Empty, ' '); 
+	EntryList_Save(&acceptedList, ACCEPTED_TXT);
+}
+void TextureCache_Deny(const cc_string* url) {
+	EntryList_Set(&deniedList,  url, &String_Empty, ' '); 
+	EntryList_Save(&deniedList, DENIED_TXT);
+}
+
+int TextureCache_ClearDenied(void) {
+	int count = deniedList.count;
+	StringsBuffer_Clear(&deniedList);
+	EntryList_Save(&deniedList, DENIED_TXT);
+	return count;
+}
+
+CC_INLINE static void HashUrl(cc_string* key, const cc_string* url) {
+	String_AppendUInt32(key, Utils_CRC32((const cc_uint8*)url->buffer, url->length));
+}
+
+static cc_bool createdCache, cacheInvalid;
+static cc_bool UseDedicatedCache(cc_string* path, const cc_string* key) {
+	cc_result res;
+	Directory_GetCachePath(path);
+	if (!path->length || cacheInvalid) return false;
+
+	String_AppendConst(path, "/texturecache");
+	res = Directory_Create(path);
+
+	/* Check if something is deleting the cache directory behind our back */
+	/*  (Several users have reported this happening on some Android devices) */
+	if (createdCache && res == 0) {
+		Chat_AddRaw("&cSomething has deleted system managed cache folder");
+		Chat_AddRaw("  &cFalling back to caching to game folder instead..");
+		cacheInvalid = true;
+	}
+	if (res == 0) createdCache = true;
+
+	String_Format1(path, "/%s", key);
+	return !cacheInvalid;
+}
+
+CC_NOINLINE static void MakeCachePath(cc_string* mainPath, cc_string* altPath, const cc_string* url) {
+	cc_string key; char keyBuffer[STRING_INT_CHARS];
+	String_InitArray(key, keyBuffer);
+	HashUrl(&key, url);
+	
+	if (UseDedicatedCache(mainPath, &key)) {
+		/* If using dedicated cache directory, also fallback to default cache directory */
+		String_Format1(altPath,  "texturecache/%s",  &key);
+	} else {
+		mainPath->length = 0;
+		String_Format1(mainPath, "texturecache/%s",  &key);
+	}
+}
+
+/* Returns non-zero if given URL has been cached */
+static int IsCached(const cc_string* url) {
+	cc_string mainPath; char mainBuffer[FILENAME_SIZE];
+	cc_string altPath;  char  altBuffer[FILENAME_SIZE];
+	String_InitArray(mainPath, mainBuffer);
+	String_InitArray(altPath,   altBuffer);
+
+	MakeCachePath(&mainPath, &altPath, url);
+	return File_Exists(&mainPath) || (altPath.length && File_Exists(&altPath));
+}
+
+/* Attempts to open the cached data stream for the given url */
+static cc_bool OpenCachedData(const cc_string* url, struct Stream* stream) {
+	cc_string mainPath; char mainBuffer[FILENAME_SIZE];
+	cc_string altPath;  char  altBuffer[FILENAME_SIZE];
+	cc_result res;
+	String_InitArray(mainPath, mainBuffer);
+	String_InitArray(altPath,   altBuffer);
+	
+
+	MakeCachePath(&mainPath, &altPath, url);
+	res = Stream_OpenFile(stream, &mainPath);
+
+	/* try fallback cache if can't find in main cache */
+	if (res == ReturnCode_FileNotFound && altPath.length)
+		res = Stream_OpenFile(stream, &altPath);
+
+	if (res == ReturnCode_FileNotFound) return false;
+	if (res) { Logger_SysWarn2(res, "opening cache for", url); return false; }
+	return true;
+}
+
+CC_NOINLINE static cc_string GetCachedTag(const cc_string* url, struct StringsBuffer* list) {
+	cc_string key; char keyBuffer[STRING_INT_CHARS];
+	String_InitArray(key, keyBuffer);
+
+	HashUrl(&key, url);
+	return EntryList_UNSAFE_Get(list, &key, ' ');
+}
+
+static cc_string GetCachedLastModified(const cc_string* url) {
+	int i;
+	cc_string entry = GetCachedTag(url, &lastModCache);
+	/* Entry used to be a timestamp of C# DateTime ticks since 01/01/0001 */
+	/* Check whether timestamp entry is old or new format */
+	for (i = 0; i < entry.length; i++) {
+		if (entry.buffer[i] < '0' || entry.buffer[i] > '9') return entry;
+	}
+
+	/* Entry is all digits, so the old unsupported format */
+	entry.length = 0; return entry;
+}
+
+static cc_string GetCachedETag(const cc_string* url) {
+	return GetCachedTag(url, &etagCache);
+}
+
+CC_NOINLINE static void SetCachedTag(const cc_string* url, struct StringsBuffer* list,
+									 const cc_string* data, const char* file) {
+	cc_string key; char keyBuffer[STRING_INT_CHARS];
+	if (!data->length) return;
+
+	String_InitArray(key, keyBuffer);
+	HashUrl(&key, url);
+	EntryList_Set(list, &key, data, ' ');
+	EntryList_Save(list, file);
+}
+
+/* Updates cached data, ETag, and Last-Modified for the given URL */
+static void UpdateCache(struct HttpRequest* req) {
+	cc_string url, altPath, value;
+	cc_string path; char pathBuffer[FILENAME_SIZE];
+	cc_result res;
+	url = String_FromRawArray(req->url);
+
+	value = String_FromRawArray(req->etag);
+	SetCachedTag(&url, &etagCache,    &value, ETAGS_TXT);
+	value = String_FromRawArray(req->lastModified);
+	SetCachedTag(&url, &lastModCache, &value, LASTMOD_TXT);
+
+	String_InitArray(path, pathBuffer);
+	altPath = String_Empty;
+	MakeCachePath(&path, &altPath, &url);
+
+	res = Stream_WriteAllTo(&path, req->data, req->size);
+	if (res) { Logger_SysWarn2(res, "caching", &url); }
+}
+
+
+/*########################################################################################################################*
+*-------------------------------------------------------TexturePack-------------------------------------------------------*
+*#########################################################################################################################*/
+static char textureUrlBuffer[URL_MAX_SIZE];
+static char texpackPathBuffer[FILENAME_SIZE];
+
+cc_string TexturePack_Url  = String_FromArray(textureUrlBuffer);
+cc_string TexturePack_Path = String_FromArray(texpackPathBuffer);
+cc_bool TexturePack_DefaultMissing;
+
+void TexturePack_SetDefault(const cc_string* texPack) {
+	TexturePack_Path.length = 0;
+	String_Format1(&TexturePack_Path, "texpacks/%s", texPack);
+	Options_Set(OPT_DEFAULT_TEX_PACK, texPack);
+}
+
+cc_result TexturePack_ExtractDefault(DefaultZipCallback callback) {
+	cc_result res = ReturnCode_FileNotFound;
+	const char* defaults[3];
+	cc_string path;
+	int i;
+
+	defaults[0] = Game_Version.DefaultTexpack;
+	defaults[1] = "texpacks/default.zip";
+	defaults[2] = "texpacks/classicube.zip";
+
+	for (i = 0; i < Array_Elems(defaults); i++) 
+	{
+		path = String_FromReadonly(defaults[i]);
+		res  = callback(&path);
+		if (!res) return 0;
+	}
+	return res;
+}
+
+
+static cc_bool SelectZipEntry(const cc_string* path) { return true; }
+static cc_result ProcessZipEntry(const cc_string* path, struct Stream* stream, struct ZipEntry* source) {
+	cc_string name = *path;
+	Utils_UNSAFE_GetFilename(&name);
+	Event_RaiseEntry(&TextureEvents.FileChanged, stream, &name);
+	return 0;
+}
+
+static cc_result ExtractPng(struct Stream* stream) {
+	struct Bitmap bmp;
+	cc_result res = Png_Decode(&bmp, stream);
+	if (!res && Atlas_TryChange(&bmp)) return 0;
+
+	Mem_Free(bmp.scan0);
+	return res;
+}
+
+static cc_bool needReload;
+static cc_result ExtractFrom(struct Stream* stream, const cc_string* path) {
+	cc_result res;
+
+	Event_RaiseVoid(&TextureEvents.PackChanged);
+	/* If context is lost, then trying to load textures will just fail */
+	/* So defer loading the texture pack until context is restored */
+	if (Gfx.LostContext) { needReload = true; return 0; }
+	needReload = false;
+
+	res = ExtractPng(stream);
+	if (res == PNG_ERR_INVALID_SIG) {
+		/* file isn't a .png image, probably a .zip archive then */
+		res = Zip_Extract(stream, SelectZipEntry, ProcessZipEntry);
+
+		if (res) Logger_SysWarn2(res, "extracting", path);
+	} else if (res) {
+		Logger_SysWarn2(res, "decoding", path);
+	}
+	return res;
+}
+
+#ifdef CC_BUILD_PS1
+#include "../misc/ps1/classicubezip.h"
+
+static cc_result ExtractFromFile(const cc_string* path) {
+	struct Stream stream;
+	Stream_ReadonlyMemory(&stream, ccTextures, ccTextures_length);
+
+	return ExtractFrom(&stream, path);
+}
+#else
+static cc_result ExtractFromFile(const cc_string* path) {
+	struct Stream stream;
+	cc_result res;
+
+	res = Stream_OpenFile(&stream, path);
+	if (res) { Logger_SysWarn2(res, "opening", path); return res; }
+
+	res = ExtractFrom(&stream, path);
+	/* No point logging error for closing readonly file */
+	(void)stream.Close(&stream);
+	return res;
+}
+#endif
+
+static cc_result ExtractUserTextures(void) {
+	cc_string path;
+	cc_result res;
+
+	/* TODO: Log error for multiple default texture pack extract failure */
+	res = TexturePack_ExtractDefault(ExtractFromFile);
+	/* Game shows a warning dialog if default textures are missing */
+	TexturePack_DefaultMissing = res == ReturnCode_FileNotFound;
+
+	path = TexturePack_Path;
+	if (String_CaselessEqualsConst(&path, "texpacks/default.zip")) path.length = 0;
+	if (Game_ClassicMode || path.length == 0) return res;
+
+	/* override default textures with user's selected texture pack */
+	return ExtractFromFile(&path);
+}
+
+static cc_bool usingDefault;
+cc_result TexturePack_ExtractCurrent(cc_bool forceReload) {
+	cc_string url = TexturePack_Url;
+	struct Stream stream;
+	cc_result res = 0;
+
+	/* don't pointlessly load default texture pack */
+	if (!usingDefault || forceReload) {
+		res = ExtractUserTextures();
+		usingDefault = true;
+	}
+
+	if (url.length && OpenCachedData(&url, &stream)) {
+		res = ExtractFrom(&stream, &url);
+		usingDefault = false;
+
+		/* No point logging error for closing readonly file */
+		(void)stream.Close(&stream);
+	}
+
+	/* Use fallback terrain texture with 1 pixel per tile */
+	if (!Atlas2D.Bmp.scan0) LoadFallbackAtlas();
+	return res;
+}
+
+/* Extracts and updates cache for the downloaded texture pack */
+static void ApplyDownloaded(struct HttpRequest* item) {
+	struct Stream mem;
+	cc_string url;
+
+	url = String_FromRawArray(item->url);
+	if (!Platform_ReadonlyFilesystem) UpdateCache(item);
+	/* Took too long to download and is no longer active texture pack */
+	if (!String_Equals(&TexturePack_Url, &url)) return;
+
+	Stream_ReadonlyMemory(&mem, item->data, item->size);
+	ExtractFrom(&mem, &url);
+	usingDefault = false;
+}
+
+void TexturePack_CheckPending(void) {
+	struct HttpRequest item;
+	if (!Http_GetResult(TexturePack_ReqID, &item)) return;
+
+	if (item.success) {
+		ApplyDownloaded(&item);
+	} else if (item.result) {
+		Http_LogError("trying to download texture pack", &item);
+	} else if (item.statusCode == 200 || item.statusCode == 304) {
+		/* Empty responses is okay for these status codes, so don't log an error */
+	} else if (item.statusCode == 404) {
+		Chat_AddRaw("&c404 Not Found error when trying to download texture pack");
+		Chat_AddRaw("  &cThe texture pack URL may be incorrect or no longer exist");
+	} else if (item.statusCode == 401 || item.statusCode == 403) {
+		Chat_Add1("&c%i Not Authorised error when trying to download texture pack", &item.statusCode);
+		Chat_AddRaw("  &cThe texture pack URL may not be publicly shared");
+	} else {
+		Chat_Add1("&c%i error when trying to download texture pack", &item.statusCode);
+	}
+	HttpRequest_Free(&item);
+}
+
+/* Asynchronously downloads the given texture pack */
+static void DownloadAsync(const cc_string* url) {
+	cc_string etag = String_Empty;
+	cc_string time = String_Empty;
+
+	/* Only retrieve etag/last-modified headers if the file exists */
+	/* This inconsistency can occur if user deleted some cached files */
+	if (IsCached(url)) {
+		time = GetCachedLastModified(url);
+		etag = GetCachedETag(url);
+	}
+
+	Http_TryCancel(TexturePack_ReqID);
+	TexturePack_ReqID = Http_AsyncGetDataEx(url, HTTP_FLAG_PRIORITY, &time, &etag, NULL);
+}
+
+void TexturePack_Extract(const cc_string* url) {
+	if (url->length) DownloadAsync(url);
+
+	if (String_Equals(url, &TexturePack_Url)) return;
+	String_Copy(&TexturePack_Url, url);
+	TexturePack_ExtractCurrent(false);
+}
+
+static struct TextureEntry* entries_head;
+static struct TextureEntry* entries_tail;
+
+void TextureEntry_Register(struct TextureEntry* entry) {
+	LinkedList_Append(entry, entries_head, entries_tail);
+}
+
+
+/*########################################################################################################################*
+*---------------------------------------------------Textures component----------------------------------------------------*
+*#########################################################################################################################*/
+static void TerrainPngProcess(struct Stream* stream, const cc_string* name) {
+	struct Bitmap bmp;
+	cc_result res = Png_Decode(&bmp, stream);
+
+	if (res) {
+		Logger_SysWarn2(res, "decoding", name);
+		Mem_Free(bmp.scan0);
+	} else if (!Atlas_TryChange(&bmp)) {
+		Mem_Free(bmp.scan0);
+	}
+}
+static struct TextureEntry terrain_entry = { "terrain.png", TerrainPngProcess };
+
+
+static void OnFileChanged(void* obj, struct Stream* stream, const cc_string* name) {
+	struct TextureEntry* e;
+
+	for (e = entries_head; e; e = e->next) {
+		if (!String_CaselessEqualsConst(name, e->filename)) continue;
+
+		e->Callback(stream, name);
+		return;
+	}
+}
+
+static void OnContextLost(void* obj) {
+	if (!Gfx.ManagedTextures) Atlas1D_Free();
+}
+
+static void OnContextRecreated(void* obj) {
+	if (!Gfx.ManagedTextures || needReload) {
+		TexturePack_ExtractCurrent(true);
+	}
+}
+
+static void OnInit(void) {
+	cc_string file;
+	Event_Register_(&TextureEvents.FileChanged,  NULL, OnFileChanged);
+	Event_Register_(&GfxEvents.ContextLost,      NULL, OnContextLost);
+	Event_Register_(&GfxEvents.ContextRecreated, NULL, OnContextRecreated);
+
+	TexturePack_Path.length = 0;
+	if (Options_UNSAFE_Get(OPT_DEFAULT_TEX_PACK, &file)) {
+		String_Format1(&TexturePack_Path, "texpacks/%s", &file);
+	}
+	
+	/* TODO temp hack to fix mobile, need to properly fix */
+	/*  issue is that Drawer2D_Component.Init is called from Launcher,*/
+	/*  which called TextureEntry_Register, whoops*/
+	entries_head = NULL;
+
+	TextureEntry_Register(&terrain_entry);
+	Utils_EnsureDirectory("texpacks");
+	Utils_EnsureDirectory("texturecache");
+	TextureCache_Init();
+}
+
+static void OnReset(void) {
+	if (!TexturePack_Url.length) return;
+	TexturePack_Url.length = 0;
+	TexturePack_ExtractCurrent(false);
+}
+
+static void OnFree(void) {
+	OnContextLost(NULL);
+	Atlas2D_Free();
+	TexturePack_Url.length = 0;
+	entries_head = NULL;
+}
+
+struct IGameComponent Textures_Component = {
+	OnInit, /* Init  */
+	OnFree, /* Free  */
+	OnReset /* Reset */
+};