Anonymous avatar Anonymous committed bdc5d5b Draft

[vfs, #3] All file reading code goes through the VFS now, new mod downloader & mod selector in place. Also a bunch of other stuff. (...)

- HTTP networking support, mods can be downloaded via the builtin downloader.
All network activity runs in a seperate thread, which is started
as soon as any network activity is requested.
- The master server is hard-coded to fg.wzff.de/aqmods/ if not specified otherwise;
this setting can be overridden in the config file.
- The mod selector screen is now a grid-view for much better navigation;
also works with joystick.
- VFS code is functionally similar to the old molebox-packed release
for win32. The game could also have its data shipped in a Zip file
or any other kind of archive.
- It is still possible to build without VFS support, but then the mod
downloader and soft-patching will not be available.

The full commit history can be found here:
https://github.com/fgenesis/Aquaria_clean/compare/master...vfs

The most important commit messages follow:
[...]
This replaces all std::ifstream with InStream, and fopen(), ... with vfopen(), ...
Some code is #ifdef'd for better performance and less memory-copying.
VFILE is defined to whatever type of file is in use:
- FILE if BBGE_BUILD_VFS is not defined
- tttvfs::VFSFile if it is.

Other changes:
- [un]packFile() is now unused and obsolete. That code has not been adjusted to use VFILE.
- glpng can now load from a memory buffer.
- TinyXML uses the VFS for reading operations now.
- The rather clunky binary stream loading of glfont2 got replaced with ByteBuffer,
which gets its data in one block (necessary to use the VFS without implementing
a somewhat STL-compliant std::ifstream replacement.)
-------------
Implement loading mods from zip files.
-------------
Implement soft-patching game data files. (Replacing textures/audio/... on the fly)
-------------
Misc bits:
- Extended GUI focus handling a bit
- Fixed weirdness in texture loading... not sure but this seems more correct to me.
Actually, considering that the texture will have its native size after restarting the game,
the lines removed with this commit seem pretty useless.

Comments (0)

Files changed (41)

Aquaria/AquariaMenuItem.cpp

 
 int AquariaGuiElement::currentGuiInputLevel = 0;
 
+AquariaGuiElement *AquariaGuiElement::currentFocus = 0;
+
 AquariaGuiElement::AquariaGuiElement()
 {
 	for (int i = 0; i < DIR_MAX; i++)
 	
 	if (v)
 	{
+		currentFocus = this;
 		if (dsq->inputMode == INPUT_JOYSTICK)
 			core->setMousePosition(getGuiPosition());
 
 			}
 		}
 	}
+	else if(this == currentFocus)
+		currentFocus = 0;
 }
 
 void AquariaGuiElement::updateMovement(float dt)
 	}
 }
 
+AquariaGuiElement *AquariaGuiElement::getClosestGuiElement(const Vector& pos)
+{
+	AquariaGuiElement *gui = 0, *closest = 0;
+	float minlen = 0;
+	for (GuiElements::iterator i = guiElements.begin(); i != guiElements.end(); i++)
+	{
+		gui = (*i);
+		if (gui->isGuiVisible() && gui->hasInput())
+		{
+			Vector dist = gui->getGuiPosition() - pos;
+			float len = dist.getSquaredLength2D();
+			if(!closest || len < minlen)
+			{
+				closest = gui;
+				minlen = len;
+			}
+		}
+	}
+	return closest;
+}
+
 
 AquariaGuiQuad::AquariaGuiQuad() : Quad(), AquariaGuiElement()
 {
 
 void AquariaMenuItem::destroy()
 {
+	setFocus(false);
 	Quad::destroy();
 	AquariaGuiElement::clean();
 }
 	{
 		std::swap(hw, hh);
 	}
-	if (v.y > position.y - hh && v.y < position.y + hh)
+	Vector pos = getWorldPosition();
+	if (v.y > pos.y - hh && v.y < pos.y + hh)
 	{
-		if (v.x > position.x - hw && v.x < position.x + hw)
+		if (v.x > pos.x - hw && v.x < pos.x + hw)
 		{
 			return true;
 		}

Aquaria/AquariaMenuItem.h

 	int guiInputLevel;
 	static int currentGuiInputLevel;
 	bool hasInput();
+	static AquariaGuiElement *currentFocus;
+	static AquariaGuiElement *getClosestGuiElement(const Vector& pos);
 protected:
 	typedef std::list<AquariaGuiElement*> GuiElements;
 	static GuiElements guiElements;
 	void useGlow(const std::string &tex, int w, int h);
 	void useSound(const std::string &tex);
 	
-	bool isCursorInMenuItem();
+	virtual bool isCursorInMenuItem();
 	Vector getGuiPosition();
 	bool isGuiVisible();
 	int shareAlpha;
+
 protected:
 
 	std::string useSfx;

Aquaria/Continuity.cpp

 #include "ScriptedEntity.h"
 #include "AutoMap.h"
 #include "GridRender.h"
+#include "DeflateCompressor.h"
 
 #include "../ExternalLibs/tinyxml.h"
 
 	std::string line, gfx;
 	int num, use;
 	float sz;
-	std::ifstream in2("data/treasures.txt");
+	InStream in2("data/treasures.txt");
 	while (std::getline(in2, line))
 	{
 		std::istringstream is(line);
 
 	/*
 	int num;
-	std::ifstream in2("data/ingredientdescriptions.txt");
+	InStream in2("data/ingredientdescriptions.txt");
 	while (std::getline(in2, line))
 	{
 		IngredientDescription desc;
 	clearIngredientData();
 	recipes.clear();
 
-	std::ifstream in(file.c_str());
+	InStream in(file.c_str());
 
 	bool recipes = false;
 	while (std::getline(in, line))
 {
 	eats.clear();
 
-	std::ifstream inf("data/eats.txt");
+	InStream inf("data/eats.txt");
 
 	EatData curData;
 	std::string read;
 void Continuity::loadPetData()
 {
 	petData.clear();
-	std::ifstream in("data/pets.txt");
+	InStream in("data/pets.txt");
 	std::string read;
 	while (std::getline(in, read))
 	{
 	doc.InsertEndChild(startData);
 
 
-	// FIXME: Patch TinyXML to write out a string and compress in-memory
-
-	doc.SaveFile(dsq->getSaveDirectory() + "/poot.tmp");
-
-	packFile(dsq->getSaveDirectory() + "/poot.tmp", getSaveFileName(slot, "aqs"), 9);
-	remove((dsq->getSaveDirectory() + "/poot.tmp").c_str());
+	std::string fn = core->adjustFilenameCase(getSaveFileName(slot, "aqs"));
+	FILE *fh = fopen(fn.c_str(), "wb");
+	if(!fh)
+	{
+		debugLog("FAILED TO SAVE GAME");
+		return;
+	}
+
+	TiXmlPrinter printer;
+	doc.Accept( &printer );
+	const char* xmlstr = printer.CStr();
+	ZlibCompressor z;
+	z.init((void*)xmlstr, printer.Size(), ZlibCompressor::REUSE);
+	z.SetForceCompression(true);
+	z.Compress(3);
+	std::ostringstream os;
+	os << "Writing " << z.size() << " bytes to save file " << fn;
+	debugLog(os.str());
+	size_t written = fwrite(z.contents(), 1, z.size(), fh);
+	if (written != z.size())
+	{
+		debugLog("FAILED TO WRITE SAVE FILE COMPLETELY");
+	}
+	fclose(fh);
 }
 
 std::string Continuity::getSaveFileName(int slot, const std::string &pfix)
 	{
 		unsigned long size = 0;
 		char *buf = readCompressedFile(teh_file, &size);
-		if (!doc.LoadMem(buf, size))
+		if (!buf || !doc.LoadMem(buf, size))
 			errorLog("Failed to load save data: " + teh_file);
 		return;
 	}
 	health = maxHealth;
 
 	speedTypes.clear();
-	std::ifstream inFile("data/speedtypes.txt");
+	InStream inFile("data/speedtypes.txt");
 	int n, spd;
 	while (inFile >> n)
 	{
 
 #include "RoundedRect.h"
 #include "TTFFont.h"
+#include "ModSelector.h"
+#include "Network.h"
+
 
 #ifdef BBGE_BUILD_OPENGL
 	#include <sys/stat.h>
 	almb = armb = 0;
 	bar_left = bar_right = bar_up = bar_down = barFade_left = barFade_right = 0;
 	
-	// do copy stuff
-#ifdef BBGE_BUILD_UNIX
-	std::string fn;
-	fn = getPreferencesFolder() + "/" + userSettingsFilename;
-	if (!exists(fn))
-		Linux_CopyTree(core->adjustFilenameCase(userSettingsFilename).c_str(), core->adjustFilenameCase(fn).c_str());
-
-	fn = getUserDataFolder() + "/_mods";
-	if (!exists(fn))
-		Linux_CopyTree(core->adjustFilenameCase("_mods").c_str(), core->adjustFilenameCase(fn).c_str());
-#endif
-
-	std::string p1 = getUserDataFolder();
-	std::string p2 = getUserDataFolder() + "/save";
-#if defined(BBGE_BUILD_UNIX)
-	mkdir(p1.c_str(), S_IRWXU);
-	mkdir(p2.c_str(), S_IRWXU);
-#elif defined(BBGE_BUILD_WINDOWS)
-	CreateDirectoryA(p2.c_str(), NULL);
-#endif
-	
 	difficulty = DIFF_NORMAL;
 
 	/*
 	subtext = 0;
 	subbox = 0;
 	menuSelectDelay = 0;
-	modSelector = 0;
+	modSelectorScr = 0;
 	blackout = 0;
 	useMic = false;
 	autoSingMenuOpen = false;
 	achievement_box = 0;
 #endif
 
-	vars = &v;
-	v.load();
-
 #ifdef AQUARIA_BUILD_CONSOLE
 	console = 0;
 #endif
 	for (int i = 0; i < 16; i++)
 		firstElementOnLayer[i] = 0;
 
-	addStateInstance(game = new Game);
-	addStateInstance(new GameOver);
-#ifdef AQUARIA_BUILD_SCENEEDITOR
-	addStateInstance(new AnimationEditor);
-#endif
-	addStateInstance(new Intro2);
-	addStateInstance(new BitBlotLogo);
-#ifdef AQUARIA_BUILD_SCENEEDITOR
-	addStateInstance(new ParticleEditor);
-#endif
-	addStateInstance(new Credits);
-	addStateInstance(new Intro);
-	addStateInstance(new Nag);
-
-	//addStateInstance(new Logo);
-	//addStateInstance(new SCLogo);
-	//addStateInstance(new IntroText);
-	//addStateInstance(new Intro);
-
 	//stream = 0;
 }
 
 	// steam callbacks are inited here
 	dsq->continuity.init();
 
+	vars = &v;
+	v.load();
+
+	// do copy stuff
+#ifdef BBGE_BUILD_UNIX
+	std::string fn;
+	fn = getPreferencesFolder() + "/" + userSettingsFilename;
+	if (!exists(fn))
+		Linux_CopyTree(core->adjustFilenameCase(userSettingsFilename).c_str(), core->adjustFilenameCase(fn).c_str());
+
+	fn = getUserDataFolder() + "/_mods";
+	if (!exists(fn))
+		Linux_CopyTree(core->adjustFilenameCase("_mods").c_str(), core->adjustFilenameCase(fn).c_str());
+#endif
+
+	std::string p1 = getUserDataFolder();
+	std::string p2 = getUserDataFolder() + "/save";
+#if defined(BBGE_BUILD_UNIX)
+	mkdir(p1.c_str(), S_IRWXU);
+	mkdir(p2.c_str(), S_IRWXU);
+#elif defined(BBGE_BUILD_WINDOWS)
+	CreateDirectoryA(p2.c_str(), NULL);
+#endif
+
+	addStateInstance(game = new Game);
+	addStateInstance(new GameOver);
+#ifdef AQUARIA_BUILD_SCENEEDITOR
+	addStateInstance(new AnimationEditor);
+#endif
+	addStateInstance(new Intro2);
+	addStateInstance(new BitBlotLogo);
+#ifdef AQUARIA_BUILD_SCENEEDITOR
+	addStateInstance(new ParticleEditor);
+#endif
+	addStateInstance(new Credits);
+	addStateInstance(new Intro);
+	addStateInstance(new Nag);
+
+	//addStateInstance(new Logo);
+	//addStateInstance(new SCLogo);
+	//addStateInstance(new IntroText);
+	//addStateInstance(new Intro);
+
 	//packReadInfo("mus.dat");
 
 	this->setBaseTextureDirectory("gfx/");
 
 	user.apply();
 
-	/*
-
-	sound->loadLocalSound("bbfloppy");
-
-	Quad *disk = new Quad("bitblot/disk", Vector(400, 300));
-	disk->alpha = 0;
-	disk->alpha.interpolateTo(1, 0.5);
-	disk->scale = Vector(0.6, 0.6);
-	addRenderObject(disk, LR_HUD);
-
-	debugLog("core->main");
-	core->main(0.5);
-	debugLog("end of core->main");
-
-	disk->position.interpolateTo(Vector(400,560), 0.5);
-	
-	core->main(0.4);
-
-	sound->playSfx("bbfloppy");
-
-	core->main(0.1);
-
-	*/
-	
-
-	/*
-	loading = new Quad("loading", Vector(400,300));
-	loading->followCamera = 1;
-	loading->alpha = 0.01;
-	addRenderObject(loading, LR_HUD);
-	*/
+	applyPatches();
 
 	loading = new Quad("loading/juice", Vector(400,300));
 	loading->alpha = 1.0;
 	*/
 }
 
-void loadModsCallback(const std::string &filename, intptr_t param)
+void DSQ::loadModsCallback(const std::string &filename, intptr_t param)
 {
 	//errorLog(filename);
 	int pos = filename.find_last_of('/')+1;
 	std::string name = filename.substr(pos, pos2-pos);
 	ModEntry m;
 	m.path = name;
+	m.id = dsq->modEntries.size();
+
+	TiXmlDocument d;
+	if(!Mod::loadModXML(&d, name))
+	{
+		std::ostringstream os;
+		os << "Failed to load mod xml: " << filename << " -- Error: " << d.ErrorDesc();
+		dsq->debugLog(os.str());
+		return;
+	}
+
+	m.type = Mod::getTypeFromXML(d.FirstChildElement("AquariaMod"));
+
 	dsq->modEntries.push_back(m);
 
-	debugLog("Loaded ModEntry [" + m.path + "]");
+	std::ostringstream ss;
+	ss << "Loaded ModEntry [" << m.path << "] -> " << m.id << "  | type " << m.type;
+
+	dsq->debugLog(ss.str());
+}
+
+void DSQ::loadModPackagesCallback(const std::string &filename, intptr_t param)
+{
+	bool ok = dsq->mountModPackage(filename);
+
+	std::ostringstream ss;
+	ss << "Mount Mod Package '" << filename << "' : " << (ok ? "ok" : "FAIL");
+	dsq->debugLog(ss.str());
+
+	// they will be enumerated by the following loadModsCallback round
 }
 
 void DSQ::startSelectedMod()
 	}
 }
 
-void DSQ::selectNextMod()
-{
-	selectedMod ++;
-
-	if (selectedMod >= modEntries.size())
-		selectedMod = 0;
-
-	if (modSelector)
-		modSelector->refreshTexture();
-}
-
-void DSQ::selectPrevMod()
-{
-	selectedMod --;
-
-	if (selectedMod < 0)
-		selectedMod = modEntries.size()-1;
-
-	if (modSelector)
-		modSelector->refreshTexture();
-}
-
 ModEntry* DSQ::getSelectedModEntry()
 {
 	if (!modEntries.empty() && selectedMod >= 0 && selectedMod < modEntries.size())
 void DSQ::loadMods()
 {
 	modEntries.clear();
-	
+
+#ifdef BBGE_BUILD_VFS
+
+	// first load the packages, then enumerate XMLs
+	forEachFile(mod.getBaseModPath(), ".aqmod", loadModPackagesCallback, 0);
+	forEachFile(mod.getBaseModPath(), ".zip", loadModPackagesCallback, 0);
+#endif
+
 	forEachFile(mod.getBaseModPath(), ".xml", loadModsCallback, 0);
 	selectedMod = 0;
 }
 
+void DSQ::applyPatches()
+{
+#ifndef AQUARIA_DEMO
+#ifdef BBGE_BUILD_VFS
+
+	// This is to allow files in patches to override files in mods on non-win32 systems (theoretically)
+	if(!vfs.GetDir("_mods"))
+	{
+		vfs.MountExternalPath(mod.getBaseModPath().c_str(), "_mods");
+	}
+
+	// user wants mods, but not yet loaded
+	if(activePatches.size() && modEntries.empty())
+		loadMods();
+
+	for (std::set<std::string>::iterator it = activePatches.begin(); it != activePatches.end(); ++it)
+		for(int i = 0; i < modEntries.size(); ++i)
+			if(modEntries[i].type == MODTYPE_PATCH)
+				if(!nocasecmp(modEntries[i].path.c_str(), it->c_str()))
+					applyPatch(modEntries[i].path);
+#endif
+#endif
+}
+
+
+#ifdef BBGE_BUILD_VFS
+
+static void refr_pushback(ttvfs::VFSDir *vd, void *user)
+{
+    std::list<ttvfs::VFSDir*> *li = (std::list<ttvfs::VFSDir*>*)user;
+    li->push_back(vd);
+}
+
+static void refr_insert(VFILE *vf, void *user)
+{
+    // texture names are like: "naija/naija2-frontleg3" - no .png extension, and no gfx/ path
+    std::set<std::string>*files = (std::set<std::string>*)user;
+    std::string t = vf->fullname();
+    size_t dotpos = t.rfind('.');
+    size_t pathstart = t.find("gfx/");
+    if(dotpos == std::string::npos || pathstart == std::string::npos || dotpos < pathstart)
+        return; // whoops
+
+    files->insert(t.substr(pathstart + 4, dotpos - (pathstart + 4)));
+}
+
+
+// this thing is rather heuristic... but works for normal mod paths
+// there is apparently nothing else except Textures that is a subclass of Resource,
+// thus directly using "gfx" subdir should be fine...
+void DSQ::refreshResourcesForPatch(const std::string& name)
+{
+	ttvfs::VFSDir *vd = vfs.GetDir((mod.getBaseModPath() + name + "/gfx").c_str()); // only textures are resources, anyways
+	if(!vd)
+		return;
+
+	std::list<ttvfs::VFSDir*> left;
+	std::set<std::string> files;
+	left.push_back(vd);
+
+	do
+	{
+		vd = left.front();
+		left.pop_front();
+		vd->forEachDir(refr_pushback, &left);
+		vd->forEachFile(refr_insert, &files);
+	}
+	while(left.size());
+
+	std::ostringstream os;
+	os << "refreshResourcesForPatch - " << files.size() << " to refresh";
+	debugLog(os.str());
+
+	for(int i = 0; i < dsq->resources.size(); ++i)
+	{
+		Resource *r = dsq->resources[i];
+		if(files.find(r->name) != files.end())
+			r->reload();
+	}
+}
+#else
+void DSQ::refreshResourcesForPatch(const std::string& name) {}
+#endif
+
+void DSQ::applyPatch(const std::string& name)
+{
+#ifdef BBGE_BUILD_VFS
+#ifdef AQUARIA_DEMO
+	return;
+#endif
+
+	std::string src = mod.getBaseModPath();
+	src += name;
+	debugLog("Apply patch: " + src);
+	vfs.Mount(src.c_str(), "", true);
+
+	activePatches.insert(name);
+	refreshResourcesForPatch(name);
+#endif
+}
+
+void DSQ::unapplyPatch(const std::string& name)
+{
+#ifdef BBGE_BUILD_VFS
+	std::string src = mod.getBaseModPath();
+	src += name;
+	debugLog("Unapply patch: " + src);
+	vfs.Unmount(src.c_str(), "");
+
+	activePatches.erase(name);
+	refreshResourcesForPatch(name);
+#endif
+}
+
 void DSQ::playMenuSelectSfx()
 {
 	core->sound->playSfx("MenuSelect");
 
 void DSQ::shutdown()
 {
+	Network::shutdown();
+
 	scriptInterface.shutdown();
 	precacher.clean();
 	/*
 	modIsSelected = false;
 
 	dsq->loadMods();
-	
-	selectedMod = user.data.lastSelectedMod;
-	
-	if (selectedMod >= modEntries.size() || selectedMod < 0)
-		selectedMod = 0;
 
 	createModSelector();
 
 		
 	if (modIsSelected)
 	{
-		user.data.lastSelectedMod = selectedMod;
 		dsq->startSelectedMod();
 	}
 
 	blackout->alpha.interpolateTo(1, 0.2);
 	addRenderObject(blackout, LR_MENU);
 
-	menu.resize(4);
-
-	menu[0] = new Quad("Cancel", Vector(750,580));
-	menu[0]->followCamera = 1;
-	addRenderObject(menu[0], LR_MENU);
-
-
-	AquariaMenuItem *a = new AquariaMenuItem();
-	//menu[0]->setLabel("Cancel");
-	a->useGlow("glow", 200, 50);
-	a->event.set(MakeFunctionEvent(DSQ,onExitSaveSlotMenu));
-	a->position = Vector(750, 580);
-	addRenderObject(a, LR_MENU);
-	menu[1] = a;
-	AquariaMenuItem *m1 = a;
-
-
-	a = new AquariaMenuItem();
-	a->useQuad("gui/arrow-left");
-	a->useGlow("glow", 100, 50);
-	a->useSound("Click");
-	a->event.set(MakeFunctionEvent(DSQ, selectPrevMod));
-	a->position = Vector(150, 300);
-	addRenderObject(a, LR_MENU);
-
-	menu[2] = a;
-
-	AquariaMenuItem *m2 = a;
-
-	a = new AquariaMenuItem();
-	a->useQuad("gui/arrow-right");
-	a->useGlow("glow", 100, 50);
-	a->useSound("Click");
-	a->event.set(MakeFunctionEvent(DSQ, selectNextMod));
-	a->position = Vector(650, 300);
-	addRenderObject(a, LR_MENU);
-
-	menu[3] = a;
-
-	AquariaMenuItem *m3 = a;
-
-	modSelector = new ModSelector();
-	modSelector->position = Vector(400,300);
-	modSelector->alpha = 0;
-	modSelector->alpha.interpolateTo(1, 0.4);
-	modSelector->followCamera = 1;
-	addRenderObject(modSelector, LR_MENU);
-
-	modSelector->setFocus(true);
-
-	m2->setDirMove(DIR_RIGHT, modSelector);
-	modSelector->setDirMove(DIR_RIGHT, m3);
-	modSelector->setDirMove(DIR_LEFT, m2);
-	m2->setDirMove(DIR_LEFT, modSelector);
-
-	modSelector->setDirMove(DIR_DOWN, m1);
-	m2->setDirMove(DIR_DOWN, m1);
-	m3->setDirMove(DIR_DOWN, m1);
-
-	m1->setDirMove(DIR_UP, modSelector);
+	modSelectorScr = new ModSelectorScreen();
+	modSelectorScr->position = Vector(400,300);
+	modSelectorScr->setWidth(getVirtualWidth()); // just to be sure
+	modSelectorScr->setHeight(getVirtualHeight());
+	modSelectorScr->autoWidth = AUTO_VIRTUALWIDTH;
+	modSelectorScr->autoHeight = AUTO_VIRTUALHEIGHT;
+	modSelectorScr->init();
+	addRenderObject(modSelectorScr, LR_MENU);
+}
+
+bool DSQ::modIsKnown(const std::string& name)
+{
+	std::string nlower = name;
+	stringToLower(nlower);
+
+	for(int i = 0; i < modEntries.size(); ++i)
+	{
+		std::string elower = modEntries[i].path;
+		stringToLower(elower);
+		if(nlower == elower)
+			return true;
+	}
+	return false;
+}
+
+bool DSQ::mountModPackage(const std::string& pkg)
+{
+#ifdef BBGE_BUILD_VFS
+	ttvfs::VFSDir *vd = vfs.AddArchive(pkg.c_str(), false, mod.getBaseModPath().c_str());
+	if (!vd)
+	{
+		debugLog("Package: Unable to load " + pkg);
+		return false;
+	}
+	debugLog("Package: Mounted " + pkg + " as archive in _mods");
+	return true;
+#else
+	debugLog("Package: Can't mount " + pkg + ", VFS support disabled");
+	return false;
+#endif
+}
+
+#ifdef BBGE_BUILD_VFS
+static void _CloseSubdirCallback(ttvfs::VFSDir *vd, void*)
+{
+	vd->close();
+	ttvfs::VFSBase *origin = vd->getOrigin();
+	if(origin)
+		origin->close();
+}
+#endif
+
+// This just closes some file handles, nothing fancy
+void DSQ::unloadMods()
+{
+#ifdef BBGE_BUILD_VFS
+	ttvfs::VFSDir *mods = vfs.GetDir(mod.getBaseModPath().c_str());
+	if(mods)
+		mods->forEachDir(_CloseSubdirCallback);
+#endif
 }
 
 void DSQ::applyParallaxUserSettings()
 		blackout = 0;
 	}
 
-	if (modSelector)
+	if(modSelectorScr)
 	{
-		modSelector->setLife(1);
-		modSelector->setDecayRate(2);
-		modSelector->fadeAlphaWithLife = 1;
-		modSelector = 0;
+		modSelectorScr->close();
+		modSelectorScr->setLife(1);
+		modSelectorScr->setDecayRate(2);
+		modSelectorScr->fadeAlphaWithLife = 1;
+		modSelectorScr = 0;
 	}
 
+	// This just closes some file handles, nothing fancy
+	unloadMods();
+
 	clearMenu();
 }
 
 	{
 		mod.shutdown();
 	}
+
+	// Will be re-loaded on demand
+	unloadMods();
 	
 	// VERY important
 	dsq->continuity.reset();
 			{
 				std::ostringstream os;
 				os << dsq->getSaveDirectory() << "/screen-" << numToZeroString(selectedSaveSlot->getSlotIndex(), 4) << ".zga";
-				std::string tempfile = dsq->getSaveDirectory() + "/poot-s.tmp";
-
-				//saveCenteredScreenshotTGA(tempfile, scrShotWidth);
-				//saveSizedScreenshotTGA(tempfile,512,1);
 
 				// Cut off top and bottom to get a 4:3 aspect ratio.
 				int adjHeight = (scrShotWidth * 3.0f) / 4.0f;
 				int adjOffset = scrShotWidth * ((scrShotHeight-adjHeight)/2) * 4;
 				memmove(scrShotData, scrShotData + adjOffset, adjImageSize);
 				memset(scrShotData + adjImageSize, 0, imageDataSize - adjImageSize);
-				tgaSave(tempfile.c_str(), scrShotWidth, scrShotHeight, 32, scrShotData);
+				zgaSave(os.str().c_str(), scrShotWidth, scrShotHeight, 32, scrShotData);
 				scrShotData = 0;  // deleted by tgaSave()
-
-				// FIXME: Get rid of tempfile and compress in-memory
-				packFile(dsq->getSaveDirectory() + "/poot-s.tmp", os.str(),9);
-				remove((dsq->getSaveDirectory() + "/poot-s.tmp").c_str());
 			}
 
 			PlaySfx sfx;
 	bgLabel->scale.interpolateTo(Vector(1,1), t);
 	addRenderObject(bgLabel, LR_CONFIRM);
 
+	const int GUILEVEL_CONFIRM = 200;
+
+	AquariaGuiElement::currentGuiInputLevel = GUILEVEL_CONFIRM;
+
 	dsq->main(t);
 
 	float t2 = 0.05;
 	addRenderObject(no, LR_CONFIRM);
 	*/
 
-	const int GUILEVEL_CONFIRM = 200;
-
-	AquariaGuiElement::currentGuiInputLevel = GUILEVEL_CONFIRM;
-
 	AquariaMenuItem *yes=0;
 	AquariaMenuItem *no=0;
 
 
 	bgLabel->safeKill();
 	txt->safeKill();
-	if (yes)	yes->safeKill();
-	if (no)		no->safeKill();
+	if (yes)
+	{
+		yes->setFocus(false);
+		yes->safeKill();
+	}
+	if (no)
+	{
+		no->setFocus(false);
+		no->safeKill();
+	}
 
 	bool ret = (confirmDone == 1);
 
 	return "dialogue/" + languagePack + "/" + f + ".txt";
 }
 
-void DSQ::jumpToSection(std::ifstream &inFile, const std::string &section)
+void DSQ::jumpToSection(InStream &inFile, const std::string &section)
 {
 	if (section.empty()) return;
 	std::string file = dsq->getDialogueFilename(dialogueFile);
 
 
 	lockMouse();
+
+	Network::update();
 }
 
 void DSQ::lockMouse()
 	bool vis, hidden;
 };
 
+enum ModType
+{
+	MODTYPE_MOD,
+	MODTYPE_PATCH,
+};
+
 struct ModEntry
 {
+	unsigned int id; // index in vector
+	ModType type;
 	std::string path;
 };
 
+class ModSelectorScreen;
+
 class Mod
 {
 public:
 	Mod();
 	void clear();
-	void loadModXML(TiXmlDocument *d, std::string modName);
 	void setActive(bool v);
 	void start();
 	void stop();
 	
 	void shutdown();
 	bool isShuttingDown();
+
+	static bool loadModXML(TiXmlDocument *d, std::string modName);
+	static ModType getTypeFromXML(TiXmlElement *xml);
+
 protected:
 	bool shuttingDown;
 	bool active;
 	std::string path;
 };
 
-class ModSelector : public AquariaGuiQuad
-{
-public:
-	ModSelector();
-	void refreshTexture();
-protected:
-	bool refreshing;
-	BitmapText *label;
-	void onUpdate(float dt);
-	bool mouseDown;
-};
-
 class AquariaScreenTransition : public ScreenTransition
 {
 public:
 	void takeScreenshot();
 	void takeScreenshotKey();
 
-	void jumpToSection(std::ifstream &inFile, const std::string &section);
+	void jumpToSection(InStream &inFile, const std::string &section);
 
 	PathFinding pathFinding;
 	void runGesture(const std::string &line);
 
 	void createModSelector();
 	void clearModSelector();
+	bool mountModPackage(const std::string&);
+	bool modIsKnown(const std::string& name);
+	void unloadMods();
+	static void loadModsCallback(const std::string &filename, intptr_t param);
+	static void loadModPackagesCallback(const std::string &filename, intptr_t param);
 
 	bool doScreenTrans;
 
 	Mod mod;
 
 	void loadMods();
+	void applyPatches();
+	void refreshResourcesForPatch(const std::string& name);
+	void applyPatch(const std::string& name);
+	void unapplyPatch(const std::string& name);
+	bool isPatchActive(const std::string& name) { return activePatches.find(name) != activePatches.end(); }
 
 	std::vector<ModEntry> modEntries;
+	std::set<std::string> activePatches;
 	int selectedMod;
-	ModSelector *modSelector;
+	ModSelectorScreen *modSelectorScr;
 
 	void startSelectedMod();
-	void selectNextMod();
-	void selectPrevMod();
 	ModEntry* getSelectedModEntry();
 
 #ifdef BBGE_BUILD_ACHIEVEMENTS_INTERNAL

Aquaria/Emote.cpp

 void Emote::load(const std::string &file)
 {
 	emotes.clear();
-	std::ifstream in(file.c_str());
+	InStream in(file.c_str());
 	std::string line;
 
 	while (std::getline(in, line))
 // and group list!
 {
 	entityTypeList.clear();
-	std::ifstream in("scripts/entities/entities.txt");
+	InStream in("scripts/entities/entities.txt");
 	std::string line;
 	if(!in)
 	{
 		fn = dsq->mod.getPath() + "entitygroups.txt";
 	}
 
-	std::ifstream in2(fn.c_str());
+	InStream in2(fn.c_str());
 
 	int curGroup=0;
 	while (std::getline(in2, line))
 
 void Game::setWarpAreaSceneName(WarpArea &warpArea)
 {
-	std::ifstream in("data/warpAreas.txt");
+	InStream in("data/warpAreas.txt");
 	std::string color, area1, dir1, area2, dir2;
 	std::string line;
 	while (std::getline(in, line))
 
 void appendFileToString(std::string &string, const std::string &file)
 {
-	std::ifstream inf(file.c_str());
+	InStream inf(file.c_str());
 
 	if (inf.is_open())
 	{
 		tileCache.clean();
 	}
 
-	std::ifstream in(fn.c_str());
+	InStream in(fn.c_str());
 	std::string line;
 	while (std::getline(in, line))
 	{

Aquaria/GameplayVariables.cpp

 
 void GameplayVariables::load()
 {
-	std::ifstream inFile("data/variables.txt");
+	InStream inFile("data/variables.txt");
 	if(!inFile)
 	{
 		core->messageBox("error", "Variables data not found! Aborting...");
 static void CheckConfig(void)
 {
 #ifdef BBGE_BUILD_WINDOWS
-    bool hasCfg = exists("usersettings.xml", false);
+    bool hasCfg = exists("usersettings.xml", false, true);
     if(!hasCfg)
         StartAQConfig();
 #endif
 	return blockEditor;
 }
 
-void Mod::loadModXML(TiXmlDocument *d, std::string modName)
+bool Mod::loadModXML(TiXmlDocument *d, std::string modName)
 {
-	d->LoadFile(baseModPath + modName + ".xml");
+	return d->LoadFile(baseModPath + modName + ".xml");
 }
 
 std::string Mod::getBaseModPath()
 		applyStart();
 	}
 }
+
+ModType Mod::getTypeFromXML(TiXmlElement *xml) // should be <AquariaMod>...</AquariaMod> - element
+{
+	if(xml)
+	{
+		TiXmlElement *prop = xml->FirstChildElement("Properties");
+		if(prop)
+		{
+			const char *type = prop->Attribute("type");
+			if(type)
+			{
+				if(!strcmp(type, "mod"))
+					return MODTYPE_MOD;
+				else if(!strcmp(type, "patch"))
+					return MODTYPE_PATCH;
+				else
+				{
+					std::ostringstream os;
+					os << "Unknown mod type '" << type << "' in XML, default to MODTYPE_MOD";
+					debugLog(os.str());
+				}
+			}
+		}
+	}
+	return MODTYPE_MOD; // the default
+}

Aquaria/ModDownloader.cpp

+#include "DSQ.h"
+#include "minihttp.h"
+
+#ifdef BBGE_BUILD_VFS
+
+#include "ModDownloader.h"
+#include "ModSelector.h"
+#include "Network.h"
+#include "tinyxml.h"
+
+#ifdef BBGE_BUILD_UNIX
+#include <sys/stat.h>
+#endif
+
+using Network::NetEvent;
+using Network::NE_ABORT;
+using Network::NE_FINISH;
+using Network::NE_UPDATE;
+
+
+// external, global
+ModDL moddl;
+
+
+// TODO: move this to Base.cpp and replace other similar occurrances
+static void createDir(const char *d)
+{
+#if defined(BBGE_BUILD_UNIX)
+	mkdir(d, S_IRWXU);
+#elif defined(BBGE_BUILD_WINDOWS)
+	CreateDirectoryA(d, NULL);
+#endif
+}
+
+// .../_mods/<MODNAME>
+// .../_mods/<MODNAME>.zip
+static std::string _PathToModName(const std::string& path)
+{
+	size_t pos = path.find_last_of('/')+1;
+	size_t pos2 = path.find_last_of('.');
+	return path.substr(pos, pos2-pos);
+}
+
+// fuuugly
+static bool _CompareByPackageURL(ModIconOnline *ico, const std::string& n)
+{
+	return ico->packageUrl == n;
+}
+static bool _CompareByIcon(ModIconOnline *ico, const std::string& n)
+{
+	return ico->iconfile == n;
+}
+// this function is required because it is never guaranteed that the original
+// ModIconOnline which triggered the download still exists.
+// This means the pointer to the icon can't be stored anywhere without risking crashing.
+// Instead, use this way to find the correct icon, even if it was deleted and recreated in the meantime.
+static ModIconOnline *_FindModIconOnline(const std::string& n, bool (*func)(ModIconOnline*,const std::string&))
+{
+	ModSelectorScreen* scr = dsq->modSelectorScr;
+	IconGridPanel *grid = scr? scr->panels[2] : NULL;
+	if(!grid)
+		return NULL;
+
+	for(RenderObject::Children::iterator it = grid->children.begin(); it != grid->children.end(); ++it)
+	{
+		ModIconOnline *ico = dynamic_cast<ModIconOnline*>(*it);
+		if(ico && func(ico, n))
+			return ico;
+	}
+	return NULL;
+}
+
+class ModlistRequest : public Network::RequestData
+{
+public:
+	ModlistRequest(bool chain) : allowChaining(chain), first(false) {}
+	virtual ~ModlistRequest() {}
+	virtual void notify(NetEvent ev, size_t recvd, size_t total)
+	{
+		moddl.NotifyModlist(this, ev, recvd, total);
+		if(ev == NE_ABORT || ev == NE_FINISH)
+			delete this;
+	}
+	bool allowChaining;
+	bool first;
+};
+
+class IconRequest : public Network::RequestData
+{
+public:
+	virtual ~IconRequest() {}
+	virtual void notify(NetEvent ev, size_t recvd, size_t total)
+	{
+		moddl.NotifyIcon(this, ev, recvd, total);
+		if(ev == NE_ABORT || ev == NE_FINISH)
+			delete this;
+	}
+};
+
+class ModRequest : public Network::RequestData
+{
+public:
+	virtual ~ModRequest() {}
+	virtual void notify(NetEvent ev, size_t recvd, size_t total)
+	{
+		moddl.NotifyMod(this, ev, recvd, total);
+		if(ev == NE_ABORT || ev == NE_FINISH)
+			delete this;
+	}
+	std::string modname;
+};
+
+ModDL::ModDL()
+{
+}
+
+ModDL::~ModDL()
+{
+}
+
+void ModDL::init()
+{
+	tempDir = dsq->getUserDataFolder() + "/webcache";
+	createDir(tempDir.c_str());
+
+	ttvfs::VFSDir *vd = vfs.GetDir(tempDir.c_str());
+	if(vd)
+		vd->load(false);
+}
+
+bool ModDL::hasUrlFileCached(const std::string& url)
+{
+	return exists(remoteToLocalName(url));
+}
+
+std::string ModDL::remoteToLocalName(const std::string& url)
+{
+	if(!url.length())
+		return "";
+
+	std::string here;
+	here.reserve(url.length() + tempDir.length() + 2);
+	here += tempDir;
+	here += '/';
+	for(size_t i = 0; i < url.length(); ++i)
+	{
+		if(!(isalnum(url[i]) || url[i] == '_' || url[i] == '-' || url[i] == '.'))
+			here += '_';
+		else
+			here += url[i];
+	}
+	return here;
+}
+
+void ModDL::GetModlist(const std::string& url, bool allowChaining, bool first)
+{
+	if(first)
+		knownServers.clear();
+	
+	// Prevent recursion, self-linling, or cycle linking.
+	// In theory, this allows setting up a server network
+	// where each server links to any servers it knows,
+	// without screwing up, but this isn't going to happen anyways.
+	// It's still useful for safety. -- FG
+	if(knownServers.size() > 30)
+	{
+		debugLog("GetModlist: Too many servers. Whaat?!");
+		return;
+	}
+	else
+	{
+		std::string host, dummy_file;
+		int dummy_port;
+		minihttp::SplitURI(url, host, dummy_file, dummy_port);
+		stringToLower(host);
+		if(knownServers.find(host) != knownServers.end())
+		{
+			debugLog("GetModlist: Already seen host: " + host + " - ignoring");
+			return;
+		}
+		knownServers.insert(host);
+	}
+
+	std::ostringstream os;
+	os << "Fetching mods list [" << url << "], chain: " << allowChaining;
+	debugLog(os.str());
+	
+	std::string localName = remoteToLocalName(url);
+
+	debugLog("... to: " + localName);
+
+	ModlistRequest *rq = new ModlistRequest(allowChaining);
+	rq->tempFilename = localName;
+	rq->finalFilename = localName;
+	rq->allowChaining = allowChaining;
+	rq->url = url;
+	rq->first = first;
+
+	Network::download(rq);
+
+	ModSelectorScreen* scr = dsq->modSelectorScr;
+	if(scr)
+	{
+		scr->globeIcon->quad->color.interpolateTo(Vector(1,1,1), 0.3f);
+		scr->globeIcon->alpha.interpolateTo(0.5f, 0.2f, -1, true, true);
+		scr->dlText.setText("Retrieving online mod list...");
+		scr->dlText.alpha.stopPath();
+		scr->dlText.alpha.interpolateTo(1, 0.1f);
+	}
+}
+
+void ModDL::NotifyModlist(ModlistRequest *rq, NetEvent ev, size_t recvd, size_t total)
+{
+	if(ev == NE_UPDATE)
+		return;
+
+	ModSelectorScreen* scr = dsq->modSelectorScr;
+
+	if(ev == NE_ABORT)
+	{
+		dsq->sound->playSfx("denied");
+		if(scr)
+		{
+			scr->globeIcon->alpha.stop();
+			scr->globeIcon->alpha.interpolateTo(1, 0.5f, 0, false, true);
+			scr->globeIcon->quad->color.interpolateTo(Vector(0.5f, 0.5f, 0.5f), 0.3f);
+			scr->dlText.setText("Unable to retrieve online mod list.\nCheck your connection and try again."); // TODO: put into stringbank
+			scr->dlText.alpha = 0;
+			scr->dlText.alpha.ensureData();
+			scr->dlText.alpha.data->path.addPathNode(0, 0);
+			scr->dlText.alpha.data->path.addPathNode(1, 0.1);
+			scr->dlText.alpha.data->path.addPathNode(1, 0.7);
+			scr->dlText.alpha.data->path.addPathNode(0, 1);
+			scr->dlText.alpha.startPath(5);
+
+			// Allow requesting another server list if the initial fetch failed.
+			// Do not care for child servers.
+			if(rq->first)
+				scr->gotServerList = false;
+		}
+		return;
+	}
+	
+	if(scr)
+	{
+		scr->globeIcon->alpha.stop();
+		scr->globeIcon->alpha.interpolateTo(1, 0.2f);
+		scr->dlText.alpha.stopPath();
+		scr->dlText.alpha.interpolateTo(0, 0.3f);
+		if(rq->first)
+			dsq->clickRingEffect(scr->globeIcon->getWorldPosition(), 1);
+	}
+
+	if(!ParseModXML(rq->finalFilename, rq->allowChaining))
+	{
+		if(scr)
+		{
+			scr->dlText.alpha.stopPath();
+			scr->dlText.alpha.interpolateTo(1, 0.5f);
+			scr->dlText.setText("Server error!\nBad XML, please contact server admin.\nURL: " + rq->url); // TODO: -> stringbank
+		}
+	}
+}
+
+bool ModDL::ParseModXML(const std::string& fn, bool allowChaining)
+{
+	TiXmlDocument xml;
+	if(!xml.LoadFile(fn))
+	{
+		debugLog("Failed to parse downloaded XML: " + fn);
+		return false;
+	}
+
+	ModSelectorScreen* scr = dsq->modSelectorScr;
+	IconGridPanel *grid = scr? scr->panels[2] : NULL;
+
+	// XML Format:
+	/*
+	<ModList>
+		<Server url="example.com/mods.xml" chain="1" /> //-- Server network - link to other servers
+		...
+		<AquariaMod>
+			<Fullname text="Jukebox"/>
+			<Description text="Listen to all the songs in the game!" />
+			<Icon url="localhost/aq/jukebox.png" size="1234" /> // -- size is optional, used to detect file change on server
+			<Package url="localhost/aq/jukebox.aqmod" saveAs="jukebox" size="1234" /> // -- saveAs is optional, and ".aqmod" appended to it
+			<Author name="Dolphin's Cry" />  //-- optional tag
+			<Confirm text="" />  //-- optional tag, pops up confirm dialog
+			<Properties type="patch" /> //-- optional tag, if not given, "mod" is assumed.
+		</AquariaMod>
+		
+		<AquariaMod>
+		...
+		</AquariaMod>
+	<ModList>
+	*/
+
+	TiXmlElement *modlist = xml.FirstChildElement("ModList");
+	if(!modlist)
+	{
+		debugLog("ModList root tag not found");
+		return false;
+	}
+
+	if(allowChaining)
+	{
+		TiXmlElement *servx = modlist->FirstChildElement("Server");
+		while(servx)
+		{
+			int chain = 0;
+			servx->Attribute("chain", &chain);
+			if(const char *url = servx->Attribute("url"))
+				GetModlist(url, chain, false);
+
+			servx = servx->NextSiblingElement("Server");
+		}
+	}
+
+	TiXmlElement *modx = modlist->FirstChildElement("AquariaMod");
+	while(modx)
+	{
+		std::string namestr, descstr, iconurl, pkgurl, confirmStr, localname;
+		std::string sizestr;
+		bool isPatch = false;
+		int serverSize = 0;
+		int serverIconSize = 0;
+		TiXmlElement *fullname, *desc, *icon, *pkg, *confirm, *props;
+		fullname = modx->FirstChildElement("Fullname");
+		desc = modx->FirstChildElement("Description");
+		icon = modx->FirstChildElement("Icon");
+		pkg = modx->FirstChildElement("Package");
+		confirm = modx->FirstChildElement("Confirm");
+		props = modx->FirstChildElement("Properties");
+
+		if(fullname && fullname->Attribute("text"))
+			namestr = fullname->Attribute("text");
+
+		if(desc && desc->Attribute("text"))
+			descstr = desc->Attribute("text");
+
+		if(icon)
+		{
+			if(icon->Attribute("url"))
+				iconurl = icon->Attribute("url");
+			if(icon->Attribute("size"))
+				icon->Attribute("size", &serverIconSize);
+		}
+
+		if(props && props->Attribute("type"))
+			isPatch = !strcmp(props->Attribute("type"), "patch");
+
+		if(pkg)
+		{
+			if(pkg->Attribute("url"))
+			{
+				pkgurl = pkg->Attribute("url");
+				localname = _PathToModName(pkgurl);
+			}
+			if(pkg->Attribute("saveAs"))
+				localname = _PathToModName(pkg->Attribute("saveAs"));
+
+			if(pkg->Attribute("size"))
+				pkg->Attribute("size", &serverSize);
+		}
+
+		if(confirm && confirm->Attribute("text"))
+			confirmStr = confirm->Attribute("text");
+
+		modx = modx->NextSiblingElement("AquariaMod");
+
+		// -------------------
+
+		if (descstr.size() > 255)
+			descstr.resize(255);
+
+		std::string localIcon = remoteToLocalName(iconurl);
+
+		size_t localIconSize = 0;
+		if(ttvfs::VFSFile *vf = vfs.GetFile(localIcon.c_str()))
+		{
+			localIconSize = vf->size();
+		}
+
+		debugLog("NetMods: " + namestr);
+
+		ModIconOnline *ico = NULL;
+		if(grid)
+		{
+			ico = new ModIconOnline;
+			ico->iconfile = localIcon;
+			ico->packageUrl = pkgurl;
+			ico->namestr = namestr;
+			ico->desc = descstr;
+			ico->confirmStr = confirmStr;
+			ico->localname = localname;
+			ico->label = "--[ " + namestr + " ]--\n" + descstr;
+			ico->isPatch = isPatch;
+
+			if(serverSize && dsq->modIsKnown(localname))
+			{
+				std::string modpkg = dsq->mod.getBaseModPath() + localname;
+				modpkg += ".aqmod";
+				ttvfs::VFSFile *vf = vfs.GetFile(modpkg.c_str());
+				if(vf)
+				{
+					size_t sz = vf->size();
+					ico->hasUpdate = (serverSize && ((size_t)serverSize != sz));
+				}
+				// if vf==NULL, then the mod was not installed with the mod downloader.
+				// There is a warning on download that's supposed to prevent this.
+				// However, if we end up with vf==NULL, there's not much to do about it.
+			}
+
+			// try to set texture, if its not there, download it.
+			// download a new icon if file size changed.
+			if(!ico->fixIcon() || !localIconSize || (serverIconSize && (size_t)serverIconSize != localIconSize))
+			{
+				ico->setDownloadProgress(0, 10);
+				GetIcon(iconurl, localIcon);
+				// we do not pass the ico ptr to the call above; otherwise it will crash if the mod menu is closed
+				// while a download is in progress
+			}
+
+			grid->add(ico);
+		}
+	}
+
+	return true;
+}
+
+void ModDL::GetMod(const std::string& url, const std::string& localname)
+{
+	ModRequest *rq = new ModRequest;
+
+	if(localname.empty())
+		rq->modname = _PathToModName(url);
+	else
+		rq->modname = localname;
+
+	rq->tempFilename = remoteToLocalName(url);
+	rq->finalFilename = rq->tempFilename; // we will fix this later on
+	rq->url = url;
+
+	debugLog("ModDL::GetMod: " + rq->finalFilename);
+
+	Network::download(rq);
+}
+
+void ModDL::GetIcon(const std::string& url, const std::string& localname)
+{
+	if(url.empty())
+		return;
+	IconRequest *rq = new IconRequest;
+	rq->url = url;
+	rq->finalFilename = localname;
+	rq->tempFilename = localname;
+	debugLog("ModDL::GetIcon: " + localname);
+	Network::download(rq);
+}
+
+void ModDL::NotifyIcon(IconRequest *rq, NetEvent ev, size_t recvd, size_t total)
+{
+	ModIconOnline *ico = _FindModIconOnline(rq->finalFilename, _CompareByIcon);
+	if(ico)
+	{
+		float perc = -1; // no progress bar
+		if(ev == NE_FINISH)
+			ico->fixIcon();
+		else if(ev == NE_UPDATE)
+			perc = total ? ((float)recvd / (float)total) : 0.0f;
+
+		ico->setDownloadProgress(perc, 10); // must be done after setting the new texture for proper visuals
+	}
+}
+
+void ModDL::NotifyMod(ModRequest *rq, NetEvent ev, size_t recvd, size_t total)
+{
+	if(ev == NE_ABORT)
+		dsq->sound->playSfx("denied");
+	else if(ev == NE_FINISH)
+		dsq->sound->playSfx("gem-collect");
+
+	ModIconOnline *ico = _FindModIconOnline(rq->url, _CompareByPackageURL);
+	if(!ico)
+	{
+		if(ev == NE_FINISH)
+			dsq->centerMessage("Finished downloading mod " + rq->modname, 420); // TODO: -> stringbank
+		return;
+	}
+
+	float perc = -1;
+	if(ev == NE_UPDATE)
+		perc = total ? ((float)recvd / (float)total) : 0.0f;
+
+	ico->setDownloadProgress(perc);
+	ico->clickable = (ev == NE_ABORT || ev == NE_FINISH);
+
+	if(ev == NE_FINISH)
+	{
+		const std::string& localname = ico->localname;
+		std::string moddir = dsq->mod.getBaseModPath() + localname;
+		// the mod file can already exist, and if it does, it will most likely be mounted.
+		// zip archives are locked and cannot be deleted/replaced, so we need to unload it first.
+		// this effectively closes the file handle only, nothing else.
+		ttvfs::VFSDir *vd = vfs.GetDir(moddir.c_str());
+		if(vd)
+		{
+			ttvfs::VFSBase *origin = vd->getOrigin();
+			if(origin)
+				origin->close();
+		}
+
+		std::string archiveFile = moddir + ".aqmod";
+
+		// At least on win32 rename() fails when the destination file already exists
+		remove(archiveFile.c_str());
+		if(rename(rq->tempFilename.c_str(), archiveFile.c_str()))
+		{
+			debugLog("Could not rename mod " + rq->tempFilename + " to " + archiveFile);
+			return;
+		}
+		else
+			debugLog("ModDownloader: Renamed mod " + rq->tempFilename + " to " + archiveFile);
+
+		if(vd)
+		{
+			// Dir already exists, just remount everything
+			vfs.Reload();
+		}
+		else if(!dsq->mountModPackage(archiveFile)) 
+		{
+			// make package readable (so that the icon can be shown)
+			// But only if it wasn't mounted before!
+			dsq->screenMessage("Failed to mount archive: " + archiveFile);
+			return;
+		}
+
+		// if it is already known, the file was re-downloaded
+		if(!dsq->modIsKnown(localname))
+		{
+			// yay, got something new!
+			DSQ::loadModsCallback(archiveFile, 0); // does not end in ".xml" but thats no problem here
+			if(dsq->modSelectorScr)
+				dsq->modSelectorScr->initModAndPatchPanel(); // HACK
+		}
+
+		ico->hasUpdate = false;
+		ico->fixIcon();
+	}
+}
+
+#endif // BBGE_BUILD_VFS

Aquaria/ModDownloader.h

+#ifndef MODDOWNLOADER_H
+#define MODDOWNLOADER_H
+#ifdef BBGE_BUILD_VFS
+
+#include <string>
+#include <set>
+#include "Network.h"
+
+#define DEFAULT_MASTER_SERVER "fg.wzff.de/aqmods/"
+
+class ModlistRequest;
+class ModRequest;
+class IconRequest;
+
+class ModDL
+{
+public:
+	ModDL();
+	~ModDL();
+	void init();
+
+	void GetModlist(const std::string& url, bool allowChaining, bool first);
+	void NotifyModlist(ModlistRequest *rq, Network::NetEvent ev, size_t recvd, size_t total);
+	bool ParseModXML(const std::string& fn, bool allowChaining);
+
+	void GetMod(const std::string& url, const std::string& localname);
+	void NotifyMod(ModRequest *rq, Network::NetEvent ev, size_t recvd, size_t total);
+
+	void GetIcon(const std::string& url, const std::string& localname);
+	void NotifyIcon(IconRequest *rq, Network::NetEvent ev, size_t recvd, size_t total);
+
+
+	std::string remoteToLocalName(const std::string& url);
+	bool hasUrlFileCached(const std::string& url);
+
+
+	std::set<std::string> knownServers;
+	std::string tempDir;
+};
+
+extern ModDL moddl;
+
+
+#endif
+#endif

Aquaria/ModSelector.cpp

 #include "../BBGE/DebugFont.h"
 
 #include "DSQ.h"
+#include "AquariaProgressBar.h"
+#include "tinyxml.h"
+#include "ModSelector.h"
 
+#ifdef BBGE_BUILD_VFS
+#include "ModDownloader.h"
+#endif
 
-ModSelector::ModSelector() : AquariaGuiQuad(), label(0)
+#define MOD_ICON_SIZE 150
+#define MINI_ICON_SIZE 32
+
+
+static bool _modname_cmp(const ModIcon *a, const ModIcon *b)
 {
-	label = new BitmapText(&dsq->smallFont);
-	//label->position = Vector(-200, 160);
-	label->position = Vector(0, 160);
-	label->setWidth(400);
-	addChild(label, PM_POINTER);
-
-	refreshTexture();
-
-	mouseDown = false;
-	refreshing = false;
-
-
-	shareAlphaWithChildren = 1;
+	return a->fname < b->fname;
 }
 
-void ModSelector::refreshTexture()
+ModSelectorScreen::ModSelectorScreen() : Quad(), ActionMapper(),
+currentPanel(-1), gotServerList(false), dlText(&dsq->smallFont), subtext(&dsq->subsFont)
 {
-	float t = 0.2;
-	bool doit=false;
-	refreshing = true;
-	if (texture)
+	followCamera = 1;
+	shareAlphaWithChildren = false;
+	alpha = 1;
+	alphaMod = 0.1f;
+	color = 0;
+	globeIcon = NULL;
+	modsIcon = NULL;
+	subFadeT = -1;
+}
+
+void ModSelectorScreen::moveUp()
+{
+	move(5);
+}
+
+void ModSelectorScreen::moveDown()
+{
+	move(-5);
+}
+
+void ModSelectorScreen::move(int ud, bool instant /* = false */)
+{
+	IconGridPanel *grid = panels[currentPanel];
+	InterpolatedVector& v = grid->position;
+	const float ch = ud * 42;
+	const float t = instant ? 0.0f : 0.2f;
+	if(!instant && v.isInterpolating())
 	{
-		alpha.interpolateTo(0, t);
-		scale.interpolateTo(Vector(0.5, 0.5), t);
-		dsq->main(t);
-		doit = true;
-	}
-	ModEntry *e = dsq->getSelectedModEntry();
-	if (e)
-	{
-		std::string texToLoad = e->path + "/" + "mod-icon";
-		texToLoad = dsq->mod.getBaseModPath() + texToLoad;
-		setTexture(texToLoad);
-		width = 256;
-		height = 256;
+		v.data->from = v;
+		v.data->target.y += ch;
+		v.data->timePassed = 0;
+
+		if(v.data->target.y > 150)
+			v.data->target.y = 150;
+		else if(v.data->target.y < -grid->getUsedY() / 2)
+			v.data->target.y = -grid->getUsedY() / 2;
 	}
 	else
 	{
-		return;
+		Vector v2 = grid->position;
+		v2.y += ch; // scroll down == grid pos y gets negative (grid scrolls up)
+
+		if(v2.y > 150)
+			grid->position.interpolateTo(Vector(v2.x, 150), t);
+		else if(v2.y < -grid->getUsedY() / 2)
+			grid->position.interpolateTo(Vector(v2.x, -grid->getUsedY() / 2), t);
+		else
+			grid->position.interpolateTo(v2, t, 0, false, true);
 	}
-	
-	TiXmlDocument d;
-	
-	dsq->mod.loadModXML(&d, e->path);
-	
-	if (label)
+}
+
+void ModSelectorScreen::onUpdate(float dt)
+{
+	Quad::onUpdate(dt);
+
+	// mouse wheel scroll
+	if(dsq->mouse.scrollWheelChange)
 	{
-		label->setText("No Description");
-		TiXmlElement *top = d.FirstChildElement("AquariaMod");
-		if (top)
+		move(dsq->mouse.scrollWheelChange);
+	}
+
+	if(subFadeT >= 0)
+	{
+		subFadeT = subFadeT - dt;
+		if(subFadeT <= 0)
 		{
-			TiXmlElement *desc = top->FirstChildElement("Description");
-			if (desc)
-			{
-				if (desc->Attribute("text"))
-				{
-					std::string txt = desc->Attribute("text");
-					if (txt.size() > 255)
-						txt.resize(255);
-					label->setText(txt);
-				}
-			}
+			subbox.alpha.interpolateTo(0, 1.0f);
+			subtext.alpha.interpolateTo(0, 1.2f);
 		}
 	}
-	if (doit)
+
+	if(!AquariaGuiElement::currentFocus && dsq->inputMode == INPUT_JOYSTICK)
 	{
-		alpha.interpolateTo(1, t);
-		scale.interpolateTo(Vector(1, 1), t);
-		dsq->main(t);
-	}
-	refreshing = false;
-}
-
-void ModSelector::onUpdate(float dt)
-{
-	AquariaGuiQuad::onUpdate(dt);
-
-	if (!refreshing)
-	{
-		if (isCoordinateInside(core->mouse.position))
+		AquariaGuiElement *closest = AquariaGuiElement::getClosestGuiElement(core->mouse.position);
+		if(closest)
 		{
-			scale.interpolateTo(Vector(1.1, 1.1), 0.1);
-			const bool anyButton = core->mouse.buttons.left || core->mouse.buttons.right;
-			if (anyButton && !mouseDown)
-			{
-				mouseDown = true;
-			}
-			else if (!anyButton && mouseDown)
-			{
-				core->quitNestedMain();
-				dsq->modIsSelected = true;
-				dsq->sound->playSfx("click");
-				dsq->sound->playSfx("pet-on");
-				mouseDown = false;
-			}
-		}
-		else
-		{
-			scale.interpolateTo(Vector(1, 1), 0.1);
-			mouseDown = false;
+			debugLog("Lost focus, setting nearest gui element");
+			closest->setFocus(true);
 		}
 	}
 }
 
+void ModSelectorScreen::showPanel(int id)
+{
+	if(id == currentPanel)
+		return;
+
+	const float t = 0.2f;
+	IconGridPanel *newgrid = panels[id];
+
+	// fade in selected panel
+	if(currentPanel < 0) // just bringing up?
+	{
+		newgrid->scale = Vector(0.8f,0.8f);
+		newgrid->alpha = 0;
+	}
+
+	currentPanel = id;
+
+	updateFade();
+}
+
+void ModSelectorScreen::updateFade()
+{
+	// fade out background panels
+	// necessary to do all of them, that icon alphas are 0... they would trigger otherwise, even if invisible because parent panel is not shown
+	for(int i = 0; i < panels.size(); ++i)
+		panels[i]->fade(i == currentPanel, true);
+}
+
+static void _MenuIconClickCallback(int id, void *user)
+{
+	ModSelectorScreen *ms = (ModSelectorScreen*)user;
+	switch(id) // see MenuIconBar::init()
+	{
+		case 2: // network
+			ms->initNetPanel();
+			break;
+
+		case 3: // exit
+			dsq->quitNestedMain();
+			return;
+	}
+
+	ms->showPanel(id);
+}
+
+// can be called multiple times without causing trouble
+void ModSelectorScreen::init()
+{
+	leftbar.width = 100;
+	leftbar.height = height;
+	leftbar.alpha = 0;
+	leftbar.alpha.interpolateTo(1, 0.2f);
+	leftbar.position = Vector((leftbar.width - width) / 2, 0);
+	leftbar.followCamera = 1;
+	if(!leftbar.getParent())
+	{
+		leftbar.init();
+		addChild(&leftbar, PM_STATIC);
+
+		panels.resize(leftbar.icons.size());
+		std::fill(panels.begin(), panels.end(), (IconGridPanel*)NULL);
+	}
+
+	rightbar.width = 100;
+	rightbar.height = height;
+	rightbar.alpha = 0;
+	rightbar.alpha.interpolateTo(1, 0.2f);
+	rightbar.position = Vector(((width - rightbar.width) / 2), 0);
+	rightbar.followCamera = 1;
+	if(!rightbar.getParent())
+	{
+		rightbar.init();
+		addChild(&rightbar, PM_STATIC);
+	}
+
+	for(int i = 0; i < panels.size(); ++i)
+	{
+		if(panels[i])
+			continue;
+		panels[i] = new IconGridPanel();
+		panels[i]->followCamera = 1;
+		panels[i]->width = width - leftbar.width - rightbar.width;
+		panels[i]->height = 750;
+		panels[i]->position = Vector(0, 0);
+		panels[i]->alpha = 0;
+		panels[i]->spacing = 20; // for the grid
+		panels[i]->scale = Vector(0.8f, 0.8f);
+		leftbar.icons[i]->cb = _MenuIconClickCallback;
+		leftbar.icons[i]->cb_data = this;
+		addChild(panels[i], PM_POINTER);
+	}
+
+	arrowUp.useQuad("Gui/arrow-left");
+	arrowUp.useSound("click");
+	arrowUp.useGlow("particles/glow", 128, 64);
+	arrowUp.position = Vector(0, -230);
+	arrowUp.followCamera = 1;
+	arrowUp.rotation.z = 90;
+	arrowUp.event.set(MakeFunctionEvent(ModSelectorScreen, moveUp));
+	arrowUp.guiInputLevel = 100;
+	arrowUp.alpha = 0;
+	arrowUp.alpha.interpolateTo(1, 0.2f);
+	arrowUp.setDirMove(DIR_DOWN, &arrowDown);
+	rightbar.addChild(&arrowUp, PM_STATIC);
+
+	arrowDown.useQuad("Gui/arrow-right");
+	arrowDown.useSound("click");
+	arrowDown.useGlow("particles/glow", 128, 64);
+	arrowDown.position = Vector(0, 170);
+	arrowDown.followCamera = 1;
+	arrowDown.rotation.z = 90;
+	arrowDown.event.set(MakeFunctionEvent(ModSelectorScreen, moveDown));
+	arrowDown.guiInputLevel = 100;
+	arrowDown.alpha = 0;
+	arrowDown.alpha.interpolateTo(1, 0.2f);
+	arrowDown.setDirMove(DIR_UP, &arrowUp);
+	rightbar.addChild(&arrowDown, PM_STATIC);
+
+	dlText.alpha = 0;
+	dlText.position = Vector(0, 0);
+	dlText.setFontSize(15);
+	dlText.scale = Vector(1.5f, 1.5f);
+	dlText.followCamera = 1;
+	addChild(&dlText, PM_STATIC);
+
+	initModAndPatchPanel();
+	// net panel inited on demand
+
+	showPanel(0);
+
+	subbox.position = Vector(0,260);
+	subbox.alpha = 0;
+	subbox.alphaMod = 0.7;
+	subbox.followCamera = 1;
+	subbox.autoWidth = AUTO_VIRTUALWIDTH;
+	subbox.setHeight(80);
+	subbox.color = Vector(0, 0, 0);
+	addChild(&subbox, PM_STATIC);
+
+	subtext.position = Vector(0,230);
+	subtext.followCamera = 1;
+	subtext.alpha = 0;
+	subtext.setFontSize(12);
+	subtext.setWidth(800);
+	subtext.setAlign(ALIGN_CENTER);
+	addChild(&subtext, PM_STATIC);
+
+	dsq->toggleVersionLabel(false);
+
+	modsIcon->setFocus(true);
+
+	// TODO: keyboard/gamepad control
+}
+
+void ModSelectorScreen::initModAndPatchPanel()
+{
+	IconGridPanel *modgrid = panels[0];
+	IconGridPanel *patchgrid = panels[1];
+	ModIcon *ico;
+	std::vector<ModIcon*> tv; // for sorting
+	tv.resize(dsq->modEntries.size());
+	for(unsigned int i = 0; i < tv.size(); ++i)
+	{
+		ico = NULL;
+		for(RenderObject::Children::iterator it = modgrid->children.begin(); it != modgrid->children.end(); ++it)
+			if(ModIcon* other = dynamic_cast<ModIcon*>(*it))
+				if(other->modId == i)
+				{
+					ico = other;
+					break;
+				}
+
+		if(!ico)
+		{
+			for(RenderObject::Children::iterator it = patchgrid->children.begin(); it != patchgrid->children.end(); ++it)
+				if(ModIcon* other = dynamic_cast<ModIcon*>(*it))
+					if(other->modId == i)
+					{
+						ico = other;
+						break;
+					}
+
+			if(!ico) // ok, its really not there.
+			{
+				ico = new ModIcon;
+				ico->followCamera = 1;
+				std::ostringstream os;
+				os << "Created ModIcon " << i;
+				debugLog(os.str());
+			}
+		}
+
+		tv[i] = ico;
+		ico->loadEntry(dsq->modEntries[i]);
+	}
+	std::sort(tv.begin(), tv.end(), _modname_cmp);
+
+	for(int i = 0; i < tv.size(); ++i)
+	{
+		if(!tv[i]->getParent()) // ensure it was not added earlier
+		{
+			if(tv[i]->modType == MODTYPE_PATCH)
+				patchgrid->add(tv[i]);
+			else
+				modgrid->add(tv[i]);
+		}
+	}
+	updateFade();
+}
+
+void ModSelectorScreen::initNetPanel()
+{
+#ifdef BBGE_BUILD_VFS
+	if(!gotServerList)
+	{
+		// FIXME: demo should be able to see downloadable mods imho
+#ifndef AQUARIA_DEMO
+		moddl.init();
+		std::string serv = dsq->user.network.masterServer;
+		if(serv.empty())
+			serv = DEFAULT_MASTER_SERVER;
+		moddl.GetModlist(serv, true, true);
+#endif
+		gotServerList = true; // try this only once (is automatically reset on failure)
+	}
+#endif
+}
+
+void ModSelectorScreen::setSubText(const std::string& s)
+{
+	subtext.setText(s);
+	subtext.alpha.interpolateTo(1, 0.2f);
+	subbox.alpha.interpolateTo(1, 0.2f);
+	subFadeT = 1;
+}
+
+static void _FadeOutAll(RenderObject *r, float t)
+{
+	//r->shareAlphaWithChildren = true;
+	r->alpha.interpolateTo(0, t);
+	for(RenderObject::Children::iterator it = r->children.begin(); it != r->children.end(); ++it)
+		_FadeOutAll(*it, t);
+}
+
+void ModSelectorScreen::close()
+{
+	/*for(int i = 0; i < panels.size(); ++i)
+		if(i != currentPanel)
+			panels[i]->setHidden(true);*/
+
+	const float t = 0.5f;
+	_FadeOutAll(this, t);
+	//panels[currentPanel]->scale.interpolateTo(Vector(0.9f, 0.9f), t); // HMM
+	dsq->user.save();
+	dsq->toggleVersionLabel(true);
+
+	// kinda hackish
+	/*dlText.setHidden(true);
+	arrowDown.glow->setHidden(true);
+	arrowUp.glow->setHidden(true);
+	subbox.setHidden(true);
+	subtext.setHidden(true);*/
+}
+
+JuicyProgressBar::JuicyProgressBar() : Quad(), txt(&dsq->smallFont)
+{
+	setTexture("modselect/tube");
+	//shareAlphaWithChildren = true;
+	followCamera = 1;
+	alpha = 1;
+
+	juice.setTexture("loading/juice");
+	juice.alpha = 0.8;
+	juice.followCamera = 1;
+	addChild(&juice, PM_STATIC);
+
+	txt.alpha = 0.7;
+	txt.followCamera = 1;
+	addChild(&txt, PM_STATIC);
+
+	progress(0);
+}
+
+void JuicyProgressBar::progress(float p)
+{
+	juice.width = p * width;
+	juice.height = height - 4;
+	perc = p;
+}
+
+BasicIcon::BasicIcon()
+: mouseDown(false), scaleNormal(1,1), scaleBig(scaleNormal * 1.1f)
+{
+	// HACK: Because AquariaMenuItem assigns onClick() in it's ctor,
+	// but we handle this ourselves.
+	clearCreatedEvents();
+	clearActions();
+	shareAlpha = true;
+	guiInputLevel = 100;
+}
+
+bool BasicIcon::isGuiVisible()
+{
+	return !isHidden() && alpha.x > 0.1f && alphaMod > 0.1f && (!parent || parent->alpha.x == 1);
+}
+
+bool BasicIcon::isCursorInMenuItem()
+{
+	if(quad)
+		return quad->isCoordinateInside(core->mouse.position);
+	return AquariaMenuItem::isCursorInMenuItem();
+}
+
+void BasicIcon::onUpdate(float dt)
+{
+	AquariaMenuItem::onUpdate(dt);
+
+	// Autoscroll if selecting icon outside of screen
+	if(hasFocus && dsq->modSelectorScr)
+	{
+		Vector pos = getRealPosition();
+		if(pos.y < 20 || pos.y > 580)
+		{
+			if(pos.y < 300)
+				dsq->modSelectorScr->move(5, true);
+			else
+				dsq->modSelectorScr->move(-5, true);
+			core->main(FRAME_TIME); // HACK: this is necessary to correctly position the mouse on the object after mofing the panel
+			setFocus(true); // re-position mouse
+		}
+	}
+
+	if(!quad)
+		return;
+
+	if (hasInput() && quad->isCoordinateInside(core->mouse.position))
+	{
+		scale.interpolateTo(scaleBig, 0.1f);
+		const bool anyButton = core->mouse.buttons.left || core->mouse.buttons.right;
+		if (anyButton && !mouseDown)
+		{
+			mouseDown = true;
+		}
+		else if (!anyButton && mouseDown)
+		{
+			if(isGuiVisible()) // do not trigger if invis
+				onClick();
+			mouseDown = false;
+		}
+	}