1
0
Fork 0
mirror of https://github.com/AquariaOSE/Aquaria.git synced 2024-12-03 00:15:46 +00:00
Aquaria/Aquaria/ModDownloader.cpp

541 lines
14 KiB
C++

#include "DSQ.h"
#include "minihttp.h"
#ifdef BBGE_BUILD_VFS
#include "ModDownloader.h"
#include "ModSelector.h"
#include "Network.h"
#include "ttvfs.h"
#include "tinyxml2.h"
using namespace tinyxml2;
using Network::NetEvent;
using Network::NE_ABORT;
using Network::NE_FINISH;
using Network::NE_UPDATE;
// external, global
ModDL moddl;
// .../_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());
}
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-linking, 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, dummy_protocol;
int dummy_port;
bool dummy_useSSL;
minihttp::SplitURI(url, dummy_protocol, host, dummy_file, dummy_port, dummy_useSSL);
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(dsq->continuity.stringBank.get(2033));
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(dsq->continuity.stringBank.get(2021));
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(dsq->continuity.stringBank.get(2022) + " " + rq->url);
}
}
}
bool ModDL::ParseModXML(const std::string& fn, bool allowChaining)
{
XMLDocument xml;
if(readXML(fn, xml) != XML_SUCCESS)
{
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. Can be "mod", "patch", or "weblink".
// if type=="weblink", <Package url> will be opened with the default web browser.
</AquariaMod>
<AquariaMod>
...
</AquariaMod>
<ModList>
*/
XMLElement *modlist = xml.FirstChildElement("ModList");
if(!modlist)
{
debugLog("ModList root tag not found");
return false;
}
if(allowChaining)
{
XMLElement *servx = modlist->FirstChildElement("Server");
while(servx)
{
int chain = servx->IntAttribute("chain");
if(const char *url = servx->Attribute("url"))
GetModlist(url, chain, false);
servx = servx->NextSiblingElement("Server");
}
}
XMLElement *modx = modlist->FirstChildElement("AquariaMod");
while(modx)
{
std::string namestr, descstr, iconurl, pkgurl, confirmStr, localname;
std::string sizestr, weburl;
ModPackageType pkgtype = MPT_MOD;
int serverSize = 0;
int serverIconSize = 0;
XMLElement *fullname, *desc, *icon, *pkg, *confirm, *props, *web;
fullname = modx->FirstChildElement("Fullname");
desc = modx->FirstChildElement("Description");
icon = modx->FirstChildElement("Icon");
pkg = modx->FirstChildElement("Package");
confirm = modx->FirstChildElement("Confirm");
props = modx->FirstChildElement("Properties");
web = modx->FirstChildElement("Web");
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");
serverIconSize = icon->IntAttribute("size");
}
if(props && props->Attribute("type"))
{
const char *ty = props->Attribute("type");
if(!strcmp(ty, "patch"))
pkgtype = MPT_PATCH;
else if(!strcmp(ty, "mod"))
pkgtype = MPT_MOD;
else if(!strcmp(ty, "weblink"))
pkgtype = MPT_WEBLINK;
}
if(pkg)
{
if(pkg->Attribute("url"))
{
pkgurl = pkg->Attribute("url");
localname = _PathToModName(pkgurl);
}
if(pkg->Attribute("saveAs"))
localname = _PathToModName(pkg->Attribute("saveAs"));
serverSize = pkg->IntAttribute("size");
}
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::File *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->pkgtype = pkgtype;
if(serverSize && dsq->modIsKnown(localname))
{
std::string modpkg = dsq->mod.getBaseModPath() + localname;
modpkg += ".aqmod";
ttvfs::File *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(dsq->continuity.stringBank.get(2023) + " " + rq->modname, 420);
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::DirBase *vd = vfs.GetDir(moddir.c_str());
if(vd)
vd->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()))
{
dsq->screenMessage("Failed to rename mod\n" + rq->tempFilename + "\n to\n" + archiveFile);
return;
}
else
debugLog("ModDownloader: Renamed mod " + rq->tempFilename + " to " + archiveFile);
if(vd)
{
// Nothing to do
}
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