diff --git a/src/client.cpp b/src/client.cpp
index 1daeeba36fde0f73130885204d8c5465d3604958..c89273a800f90a652aa5039359ab5966d30992d1 100644
--- a/src/client.cpp
+++ b/src/client.cpp
@@ -34,6 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 #include "nodemetadata.h"
 #include "nodedef.h"
 #include "tooldef.h"
+#include <IFileSystem.h>
 
 /*
 	QueuedMeshUpdate
@@ -1523,6 +1524,62 @@ void Client::ProcessData(u8 *data, u32 datasize, u16 sender_peer_id)
 		m_mesh_update_thread.setRun(true);
 		m_mesh_update_thread.Start();
 	}
+	else if(command == TOCLIENT_TEXTURES)
+	{
+		infostream<<"Client: Received textures: packet size: "<<datasize
+				<<std::endl;
+
+		io::IFileSystem *irrfs = m_device->getFileSystem();
+		video::IVideoDriver *vdrv = m_device->getVideoDriver();
+
+		std::string datastring((char*)&data[2], datasize-2);
+		std::istringstream is(datastring, std::ios_base::binary);
+
+		// Stop threads while updating content definitions
+		m_mesh_update_thread.stop();
+		
+		/*
+			u16 command
+			u32 number of textures
+			for each texture {
+				u16 length of name
+				string name
+				u32 length of data
+				data
+			}
+		*/
+		int num_textures = readU32(is);
+		infostream<<"Client: Received textures: count: "<<num_textures
+				<<std::endl;
+		for(int i=0; i<num_textures; i++){
+			std::string name = deSerializeString(is);
+			std::string data = deSerializeLongString(is);
+			// Silly irrlicht's const-incorrectness
+			Buffer<char> data_rw(data.c_str(), data.size());
+			// Create an irrlicht memory file
+			io::IReadFile *rfile = irrfs->createMemoryReadFile(
+					*data_rw, data.size(), "_tempreadfile");
+			assert(rfile);
+			// Read image
+			video::IImage *img = vdrv->createImageFromFile(rfile);
+			if(!img){
+				errorstream<<"Client: Cannot create image from data of "
+						<<"received texture \""<<name<<"\""<<std::endl;
+				rfile->drop();
+				continue;
+			}
+			m_tsrc->insertImage(name, img);
+			rfile->drop();
+		}
+
+		// Update texture atlas
+		if(g_settings->getBool("enable_texture_atlas"))
+			m_tsrc->buildMainAtlas(this);
+		
+		// Resume threads
+		m_mesh_update_thread.setRun(true);
+		m_mesh_update_thread.Start();
+	}
 	else
 	{
 		infostream<<"Client: Ignoring unknown command "
diff --git a/src/clientserver.h b/src/clientserver.h
index ef8188b2a44476acd8787db2d784b07aa924cb52..0d553f7697b1ccd7f077b465cf57ecb39a64bc44 100644
--- a/src/clientserver.h
+++ b/src/clientserver.h
@@ -29,6 +29,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 		Base for writing changes here
 	PROTOCOL_VERSION 4:
 		Add TOCLIENT_TOOLDEF
+		Add TOCLIENT_TEXTURES
 */
 
 #define PROTOCOL_VERSION 4
@@ -198,6 +199,18 @@ enum ToClientCommand
 		serialized ToolDefManager
 	*/
 	
+	TOCLIENT_TEXTURES = 0x39,
+	/*
+		u16 command
+		u32 number of textures
+		for each texture {
+			u16 length of name
+			string name
+			u32 length of data
+			data
+		}
+	*/
+	
 	//TOCLIENT_CONTENT_SENDING_MODE = 0x38,
 	/*
 		u16 command
diff --git a/src/server.cpp b/src/server.cpp
index 9a7f1e97218b7e7f152ed8f05a74390222b6095c..44c66447c3eda6a35bde5f5e283759ca59bbe10d 100644
--- a/src/server.cpp
+++ b/src/server.cpp
@@ -944,6 +944,35 @@ u32 PIChecksum(core::list<PlayerInfo> &l)
 	return checksum;
 }
 
+struct ModSpec
+{
+	std::string name;
+	std::string path;
+
+	ModSpec(const std::string &name_="", const std::string path_=""):
+		name(name_),
+		path(path_)
+	{}
+};
+
+static core::list<ModSpec> getMods(core::list<std::string> &modspaths)
+{
+	core::list<ModSpec> mods;
+	for(core::list<std::string>::Iterator i = modspaths.begin();
+			i != modspaths.end(); i++){
+		std::string modspath = *i;
+		std::vector<fs::DirListNode> dirlist = fs::GetDirListing(modspath);
+		for(u32 j=0; j<dirlist.size(); j++){
+			if(!dirlist[j].dir)
+				continue;
+			std::string modname = dirlist[j].name;
+			std::string modpath = modspath + DIR_DELIM + modname;
+			mods.push_back(ModSpec(modname, modpath));
+		}
+	}
+	return mods;
+}
+
 /*
 	Server
 */
@@ -988,6 +1017,9 @@ Server::Server(
 	
 	// Initialize default node definitions
 	content_mapnode_init(NULL, m_nodemgr);
+	
+	// Add default global mod path
+	m_modspaths.push_back(porting::path_data + DIR_DELIM + "mods");
 
 	// Initialize scripting
 	
@@ -997,26 +1029,17 @@ Server::Server(
 	// Export API
 	scriptapi_export(m_lua, this);
 	// Load and run scripts
-	core::list<std::string> modspaths;
-	modspaths.push_back(porting::path_data + DIR_DELIM + "mods");
-	for(core::list<std::string>::Iterator i = modspaths.begin();
-			i != modspaths.end(); i++){
-		std::string modspath = *i;
-		std::vector<fs::DirListNode> dirlist = fs::GetDirListing(modspath);
-		for(u32 j=0; j<dirlist.size(); j++){
-			if(!dirlist[j].dir)
-				continue;
-			std::string modname = dirlist[j].name;
-			infostream<<"Server: Loading mod \""<<modname<<"\" script..."
-					<<std::endl;
-			std::string scriptpath = modspath + DIR_DELIM + modname
-					+ DIR_DELIM + "init.lua";
-			bool success = script_load(m_lua, scriptpath.c_str());
-			if(!success){
-				errorstream<<"Server: Failed to load and run "
-						<<scriptpath<<std::endl;
-				assert(0);
-			}
+	core::list<ModSpec> mods = getMods(m_modspaths);
+	for(core::list<ModSpec>::Iterator i = mods.begin();
+			i != mods.end(); i++){
+		ModSpec mod = *i;
+		infostream<<"Server: Loading mod \""<<mod.name<<"\""<<std::endl;
+		std::string scriptpath = mod.path + DIR_DELIM + "init.lua";
+		bool success = script_load(m_lua, scriptpath.c_str());
+		if(!success){
+			errorstream<<"Server: Failed to load and run "
+					<<scriptpath<<std::endl;
+			assert(0);
 		}
 	}
 	
@@ -2115,6 +2138,9 @@ void Server::ProcessData(u8 *data, u32 datasize, u16 peer_id)
 		/*
 			Send some initialization data
 		*/
+
+		// Send textures
+		SendTextures(peer_id);
 		
 		// Send tool definitions
 		SendToolDef(m_con, peer_id, m_toolmgr);
@@ -4080,6 +4106,105 @@ void Server::SendBlocks(float dtime)
 	}
 }
 
+struct SendableTexture
+{
+	std::string name;
+	std::string path;
+	std::string data;
+
+	SendableTexture(const std::string &name_="", const std::string path_="",
+			const std::string &data_=""):
+		name(name_),
+		path(path_),
+		data(data_)
+	{}
+};
+
+void Server::SendTextures(u16 peer_id)
+{
+	DSTACK(__FUNCTION_NAME);
+
+	infostream<<"Server::SendTextures(): Sending textures to client"<<std::endl;
+	
+	/* Read textures */
+	
+	core::list<SendableTexture> textures;
+	core::list<ModSpec> mods = getMods(m_modspaths);
+	for(core::list<ModSpec>::Iterator i = mods.begin();
+			i != mods.end(); i++){
+		ModSpec mod = *i;
+		std::string texturepath = mod.path + DIR_DELIM + "textures";
+		std::vector<fs::DirListNode> dirlist = fs::GetDirListing(texturepath);
+		for(u32 j=0; j<dirlist.size(); j++){
+			if(dirlist[j].dir) // Ignode dirs
+				continue;
+			std::string tname = dirlist[j].name;
+			std::string tpath = texturepath + DIR_DELIM + tname;
+			// Read data
+			std::ifstream fis(tpath.c_str(), std::ios_base::binary);
+			if(fis.good() == false){
+				errorstream<<"Server::SendTextures(): Could not open \""
+						<<tname<<"\" for reading"<<std::endl;
+				continue;
+			}
+			std::ostringstream tmp_os(std::ios_base::binary);
+			bool bad = false;
+			for(;;){
+				char buf[1024];
+				fis.read(buf, 1024);
+				std::streamsize len = fis.gcount();
+				tmp_os.write(buf, len);
+				if(fis.eof())
+					break;
+				if(!fis.good()){
+					bad = true;
+					break;
+				}
+			}
+			if(bad){
+				errorstream<<"Server::SendTextures(): Failed to read \""
+						<<tname<<"\""<<std::endl;
+				continue;
+			}
+			errorstream<<"Server::SendTextures(): Loaded \""
+					<<tname<<"\""<<std::endl;
+			// Put in list
+			textures.push_back(SendableTexture(tname, tpath, tmp_os.str()));
+		}
+	}
+
+	/* Create and send packet */
+
+	/*
+		u16 command
+		u32 number of textures
+		for each texture {
+			u16 length of name
+			string name
+			u32 length of data
+			data
+		}
+	*/
+	std::ostringstream os(std::ios_base::binary);
+
+	writeU16(os, TOCLIENT_TEXTURES);
+	writeU32(os, textures.size());
+	
+	for(core::list<SendableTexture>::Iterator i = textures.begin();
+			i != textures.end(); i++){
+		os<<serializeString(i->name);
+		os<<serializeLongString(i->data);
+	}
+	
+	// Make data buffer
+	std::string s = os.str();
+	infostream<<"Server::SendTextures(): number of textures: "
+			<<textures.size()<<", data size: "<<s.size()<<std::endl;
+	SharedBuffer<u8> data((u8*)s.c_str(), s.size());
+	// Send as reliable
+	m_con.Send(peer_id, 0, data, true);
+}
+
 /*
 	Something random
 */
diff --git a/src/server.h b/src/server.h
index 0354abbd9ca842a42107b486149dd442c14abd9a..e53bf9c9aecb9d7a07976fd0920bbc89902815c3 100644
--- a/src/server.h
+++ b/src/server.h
@@ -515,7 +515,10 @@ class Server : public con::PeerHandler, public MapEventReceiver,
 			IToolDefManager *tooldef);
 	
 	/*
-		Non-static send methods
+		Non-static send methods.
+		Conlock should be always used.
+		Envlock usage is documented badly but it's easy to figure out
+		which ones access the environment.
 	*/
 
 	// Envlock and conlock should be locked when calling these
@@ -546,6 +549,8 @@ class Server : public con::PeerHandler, public MapEventReceiver,
 	
 	// Sends blocks to clients (locks env and con on its own)
 	void SendBlocks(float dtime);
+	
+	void SendTextures(u16 peer_id);
 
 	/*
 		Something random
@@ -682,6 +687,9 @@ class Server : public con::PeerHandler, public MapEventReceiver,
 
 	// Configuration path ("" = no configuration file)
 	std::string m_configpath;
+	
+	// Mod parent directory paths
+	core::list<std::string> m_modspaths;
 
 	bool m_shutdown_requested;
 	
diff --git a/src/tile.cpp b/src/tile.cpp
index eb3616f026531eea675ce5d5ffeb7a378777e19c..8ab92d10548e6d9e1639118fd296045f20cc7a50 100644
--- a/src/tile.cpp
+++ b/src/tile.cpp
@@ -242,20 +242,23 @@ class TextureSource : public IWritableTextureSource
 	*/
 	void updateAP(AtlasPointer &ap);
 
+	/*
+		Processes queued texture requests from other threads.
+
+		Shall be called from the main thread.
+	*/
+	void processQueue();
+	
 	/*
 		Build the main texture atlas which contains most of the
 		textures.
-		
-		This is called by the constructor.
 	*/
 	void buildMainAtlas(class IGameDef *gamedef);
 	
 	/*
-		Processes queued texture requests from other threads.
-
-		Shall be called from the main thread.
+		Insert an image into the cache without touching the filesystem.
 	*/
-	void processQueue();
+	void insertImage(const std::string &name, video::IImage *img);
 	
 private:
 	
@@ -305,31 +308,6 @@ TextureSource::~TextureSource()
 {
 }
 
-void TextureSource::processQueue()
-{
-	/*
-		Fetch textures
-	*/
-	if(m_get_texture_queue.size() > 0)
-	{
-		GetRequest<std::string, u32, u8, u8>
-				request = m_get_texture_queue.pop();
-
-		infostream<<"TextureSource::processQueue(): "
-				<<"got texture request with "
-				<<"name=\""<<request.key<<"\""
-				<<std::endl;
-
-		GetResult<std::string, u32, u8, u8>
-				result;
-		result.key = request.key;
-		result.callers = request.callers;
-		result.item = getTextureIdDirect(request.key);
-
-		request.dest->push_back(result);
-	}
-}
-
 u32 TextureSource::getTextureId(const std::string &name)
 {
 	//infostream<<"getTextureId(): \""<<name<<"\""<<std::endl;
@@ -624,6 +602,31 @@ void TextureSource::updateAP(AtlasPointer &ap)
 	ap = ap2;
 }
 
+void TextureSource::processQueue()
+{
+	/*
+		Fetch textures
+	*/
+	if(m_get_texture_queue.size() > 0)
+	{
+		GetRequest<std::string, u32, u8, u8>
+				request = m_get_texture_queue.pop();
+
+		infostream<<"TextureSource::processQueue(): "
+				<<"got texture request with "
+				<<"name=\""<<request.key<<"\""
+				<<std::endl;
+
+		GetResult<std::string, u32, u8, u8>
+				result;
+		result.key = request.key;
+		result.callers = request.callers;
+		result.item = getTextureIdDirect(request.key);
+
+		request.dest->push_back(result);
+	}
+}
+
 void TextureSource::buildMainAtlas(class IGameDef *gamedef) 
 {
 	assert(gamedef->tsrc() == this);
@@ -864,6 +867,46 @@ void TextureSource::buildMainAtlas(class IGameDef *gamedef)
 	driver->writeImageToFile(atlas_img, atlaspath.c_str());*/
 }
 
+void TextureSource::insertImage(const std::string &name, video::IImage *img)
+{
+	infostream<<"TextureSource::insertImage(): name="<<name<<std::endl;
+	
+	JMutexAutoLock lock(m_atlaspointer_cache_mutex);
+
+	video::IVideoDriver* driver = m_device->getVideoDriver();
+	assert(driver);
+
+	// Create texture
+	video::ITexture *t = driver->addTexture(name.c_str(), img);
+
+	bool reuse_old_id = false;
+	u32 id = m_atlaspointer_cache.size();
+	// Check old id without fetching a texture
+	core::map<std::string, u32>::Node *n;
+	n = m_name_to_id.find(name);
+	// If it exists, we will replace the old definition
+	if(n){
+		id = n->getValue();
+		reuse_old_id = true;
+	}
+	
+	// Create AtlasPointer
+	AtlasPointer ap(id);
+	ap.atlas = t;
+	ap.pos = v2f(0,0);
+	ap.size = v2f(1,1);
+	ap.tiled = 0;
+	core::dimension2d<u32> dim = img->getDimension();
+
+	// Create SourceAtlasPointer and add to containers
+	SourceAtlasPointer nap(name, ap, img, v2s32(0,0), dim);
+	if(reuse_old_id)
+		m_atlaspointer_cache[id] = nap;
+	else
+		m_atlaspointer_cache.push_back(nap);
+	m_name_to_id[name] = id;
+}
+	
 video::IImage* generate_image_from_scratch(std::string name,
 		IrrlichtDevice *device)
 {
diff --git a/src/tile.h b/src/tile.h
index 105692c10bbf7f849602ce19fb3d3e4b6feb4415..17ba74e3396d6633feea8e1ed063d278cbda3e96 100644
--- a/src/tile.h
+++ b/src/tile.h
@@ -156,8 +156,10 @@ class IWritableTextureSource : public ITextureSource
 		{return NULL;}
 	virtual void updateAP(AtlasPointer &ap){};
 
-	virtual void buildMainAtlas(class IGameDef *gamedef)=0;
 	virtual void processQueue()=0;
+	virtual void buildMainAtlas(class IGameDef *gamedef)=0;
+	// img is eaten, do not drop it
+	virtual void insertImage(const std::string &name, video::IImage *img)=0;
 };
 
 IWritableTextureSource* createTextureSource(IrrlichtDevice *device);