/*
* The ManaVerse Client
* Copyright (C) 2004-2009 The Mana World Development Team
* Copyright (C) 2009-2010 The Mana Developers
* Copyright (C) 2011-2020 The ManaPlus Developers
* Copyright (C) 2020-2025 The ManaVerse Developers
*
* This file is part of The ManaVerse Client.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include "dirs.h"
#include "client.h"
#include "configuration.h"
#include "logger.h"
#include "main.h"
#include "settings.h"
#include "fs/mkdir.h"
#include "fs/paths.h"
#include "fs/virtfs/fs.h"
#include "utils/base64.h"
#include "utils/stringutils.h"
#if defined(__native_client__) || (defined(ANDROID) && defined(USE_SDL2))
#include "fs/files.h"
#endif // defined(__native_client__) || (defined(ANDROID) && defined(USE_SDL2))
#include "utils/cast.h"
#include "utils/gettext.h"
#include "utils/performance.h"
#ifdef ANDROID
#ifdef USE_SDL2
#include "main.h"
#include "render/graphics.h"
#endif // USE_SDL2
#endif // ANDROID
#ifdef __APPLE__
#include
#endif // __APPLE__
#ifdef _WIN32
PRAGMA48(GCC diagnostic push)
PRAGMA48(GCC diagnostic ignored "-Wshadow")
#include
PRAGMA48(GCC diagnostic pop)
#include "fs/specialfolder.h"
#undef ERROR
#endif // _WIN32
#include
#include
#include "debug.h"
#if defined __native_client__
#define _nacl_dir std::string("/persistent/manaplus")
#endif // defined __native_client__
// Normalize paths to use forward slashes for cross-platform compatibility
static std::string normalizePath(const std::string &path)
{
std::string normalized = path;
replaceAll(normalized, "\\", "/");
return normalized;
}
#ifdef _WIN32
// Convert paths to use backslashes for Windows-specific functions
static std::string toWindowsPath(const std::string &path)
{
std::string winPath = path;
replaceAll(winPath, "/", "\\");
return winPath;
}
#endif
#ifdef ANDROID
#ifdef USE_SDL2
int loadingProgressCounter = 1;
static void updateProgress(int cnt)
{
const int progress = cnt + loadingProgressCounter;
const int h = mainGraphics->mHeight;
mainGraphics->setColor(Color(255, 255, 255, 255));
const int maxSize = mainGraphics->mWidth - 100;
const int width = maxSize * progress / 50;
mainGraphics->fillRectangle(Rect(50, h - 100, width, 50));
mainGraphics->updateScreen();
}
void Dirs::setProgress()
{
loadingProgressCounter++;
updateProgress(loadingProgressCounter);
}
static void resetProgress()
{
loadingProgressCounter = 0;
updateProgress(loadingProgressCounter);
}
void extractAssets()
{
if (!getenv("APPDIR"))
{
logger->log("error: APPDIR is not set!");
return;
}
const std::string fileName = normalizePath(pathJoin(getenv("APPDIR"),
"data.zip"));
logger->log("Extracting asset into: " + fileName);
uint8_t *buf = new uint8_t[1000000];
FILE *const file = fopen(fileName.c_str(), "w");
for (int f = 0; f < 100; f ++)
{
std::string part = strprintf("manaplus-data.zip%u%u",
CAST_U32(f / 10),
CAST_U32(f % 10));
logger->log("testing asset: " + part);
SDL_RWops *const rw = SDL_RWFromFile(part.c_str(), "r");
if (rw)
{
const int size = SDL_RWsize(rw);
int size2 = SDL_RWread(rw, buf, 1, size);
logger->log("asset size: %d", size2);
fwrite(buf, 1, size2, file);
SDL_RWclose(rw);
Dirs::setProgress();
}
else
{
break;
}
}
fclose(file);
const std::string fileName2 = normalizePath(pathJoin(getenv("APPDIR"),
"locale.zip"));
FILE *const file2 = fopen(fileName2.c_str(), "w");
SDL_RWops *const rw = SDL_RWFromFile("manaplus-locale.zip", "r");
if (rw)
{
const int size = SDL_RWsize(rw);
int size2 = SDL_RWread(rw, buf, 1, size);
fwrite(buf, 1, size2, file2);
SDL_RWclose(rw);
Dirs::setProgress();
}
fclose(file2);
delete [] buf;
}
#endif // USE_SDL2
#endif // ANDROID
void Dirs::updateDataPath()
{
if (settings.options.dataPath.empty()
&& !branding.getStringValue("dataPath").empty())
{
std::string dataPath = branding.getStringValue("dataPath");
if (isRealPath(dataPath))
{
settings.options.dataPath = normalizePath(dataPath);
}
else
{
settings.options.dataPath = normalizePath(
pathJoin(branding.getDirectory(), dataPath));
}
settings.options.skipUpdate = true;
}
}
void Dirs::extractDataDir()
{
#if defined(ANDROID) && defined(USE_SDL2)
Files::setCopyCallBack(&updateProgress);
resetProgress();
extractAssets();
const std::string zipName = normalizePath(pathJoin(getenv("APPDIR"), "data.zip"));
const std::string dirName = normalizePath(pathJoin(getenv("APPDIR"), "data"));
VirtFs::mountZip2(zipName,
"data",
Append_false);
VirtFs::mountZip2(zipName,
"data/perserver/default",
Append_false);
Files::extractLocale();
#endif // defined(ANDROID) && defined(USE_SDL2)
}
void Dirs::mountDataDir()
{
VirtFs::mountDirSilent(normalizePath(PKG_DATADIR "data/perserver/default"),
Append_false);
VirtFs::mountDirSilent(normalizePath("data/perserver/default"),
Append_false);
#if defined __APPLE__
CFBundleRef mainBundle = CFBundleGetMainBundle();
CFURLRef resourcesURL = CFBundleCopyResourcesDirectoryURL(mainBundle);
char path[PATH_MAX];
if (!CFURLGetFileSystemRepresentation(resourcesURL,
TRUE,
reinterpret_cast(path),
PATH_MAX))
{
fprintf(stderr, "Can't find Resources directory\n");
}
CFRelease(resourcesURL);
std::string path2 = normalizePath(pathJoin(path, "data"));
VirtFs::mountDir(normalizePath(pathJoin(path2, "perserver/default")), Append_false);
VirtFs::mountDir(normalizePath(path2), Append_false);
#endif // defined __APPLE__
VirtFs::mountDirSilent(normalizePath(PKG_DATADIR "data"), Append_false);
setPackageDir(normalizePath(PKG_DATADIR "data"));
VirtFs::mountDirSilent(normalizePath("data"), Append_false);
#ifdef ANDROID
#ifdef USE_SDL2
if (getenv("APPDIR"))
{
const std::string appDir = normalizePath(getenv("APPDIR"));
VirtFs::mountDir(normalizePath(appDir + "/data"), Append_false);
VirtFs::mountDir(normalizePath(appDir + "/data/perserver/default"),
Append_false);
}
#endif // USE_SDL2
#endif // ANDROID
#if defined __native_client__
VirtFs::mountZip(normalizePath("/http/data.zip"), Append_false);
VirtFs::mountZip2(normalizePath("/http/data.zip"),
"perserver/default",
Append_false);
#endif // defined __native_client__
#ifndef _WIN32
// Add branding/data to VirtFS search path
if (!settings.options.brandingPath.empty())
{
std::string path = normalizePath(settings.options.brandingPath);
// Strip blah.manaplus from the path
const int loc = CAST_S32(path.find_last_of('/'));
if (loc > 0)
{
VirtFs::mountDir(normalizePath(path.substr(0, loc + 1).append("data")),
Append_false);
}
}
#endif // _WIN32
}
void Dirs::initRootDir()
{
settings.rootDir = normalizePath(VirtFs::getBaseDir());
const std::string portableName = normalizePath(settings.rootDir + "portable.xml");
struct stat statbuf;
if (stat(portableName.c_str(), &statbuf) == 0 &&
S_ISREG(statbuf.st_mode))
{
std::string dir;
Configuration portable;
portable.init(portableName,
UseVirtFs_false,
SkipError_false);
if (settings.options.brandingPath.empty())
{
branding.init(portableName,
UseVirtFs_false,
SkipError_false);
setBrandingDefaults(branding);
}
logger->log("Portable file: %s", portableName.c_str());
if (settings.options.localDataDir.empty())
{
dir = portable.getValue("dataDir", "");
if (!dir.empty())
{
settings.options.localDataDir = normalizePath(settings.rootDir + dir);
logger->log("Portable data dir: %s",
settings.options.localDataDir.c_str());
}
}
if (settings.options.configDir.empty())
{
dir = portable.getValue("configDir", "");
if (!dir.empty())
{
settings.options.configDir = normalizePath(settings.rootDir + dir);
logger->log("Portable config dir: %s",
settings.options.configDir.c_str());
}
}
if (settings.options.screenshotDir.empty())
{
dir = portable.getValue("screenshotDir", "");
if (!dir.empty())
{
settings.options.screenshotDir = normalizePath(settings.rootDir + dir);
logger->log("Portable screenshot dir: %s",
settings.options.screenshotDir.c_str());
}
}
}
}
void Dirs::initHomeDir()
{
initLocalDataDir();
initTempDir();
initConfigDir();
}
void Dirs::initLocalDataDir()
{
settings.localDataDir = normalizePath(settings.options.localDataDir);
if (settings.localDataDir.empty())
{
#ifdef __APPLE__
settings.localDataDir = normalizePath(pathJoin(VirtFs::getUserDir(),
"Library/Application Support",
branding.getValue("appName", "ManaPlus")));
#elif defined __HAIKU__
settings.localDataDir = normalizePath(pathJoin(VirtFs::getUserDir(),
"config/cache/Mana"));
#elif defined _WIN32
settings.localDataDir = normalizePath(getSpecialFolderLocation(CSIDL_LOCAL_APPDATA));
if (settings.localDataDir.empty())
settings.localDataDir = normalizePath(VirtFs::getUserDir());
settings.localDataDir = normalizePath(pathJoin(settings.localDataDir,
"Mana"));
#elif defined __ANDROID__
settings.localDataDir = normalizePath(pathJoin(getSdStoragePath(),
branding.getValue("appShort", "ManaPlus"),
"local"));
#elif defined __native_client__
settings.localDataDir = normalizePath(pathJoin(_nacl_dir, "local"));
#elif defined __SWITCH__
settings.localDataDir = normalizePath(pathJoin(VirtFs::getUserDir(), "local"));
#else
settings.localDataDir = normalizePath(pathJoin(VirtFs::getUserDir(),
".local/share/mana"));
#endif
}
if (mkdir_r(settings.localDataDir.c_str()) != 0)
{
// TRANSLATORS: directory creation error
logger->error(strprintf(_("%s doesn't exist and can't be created! "
"Exiting."), settings.localDataDir.c_str()));
}
#ifdef USE_PROFILER
Performance::init(normalizePath(pathJoin(settings.localDataDir, "profiler.log")));
#endif
}
void Dirs::initTempDir()
{
settings.tempDir = normalizePath(pathJoin(settings.localDataDir, "temp"));
if (mkdir_r(settings.tempDir.c_str()) != 0)
{
// TRANSLATORS: directory creation error
logger->error(strprintf(_("%s doesn't exist and can't be created! "
"Exiting."), settings.tempDir.c_str()));
}
}
void Dirs::initConfigDir()
{
settings.configDir = normalizePath(settings.options.configDir);
if (settings.configDir.empty())
{
#ifdef __APPLE__
settings.configDir = normalizePath(pathJoin(settings.localDataDir,
branding.getValue("appShort", "mana")));
#elif defined __HAIKU__
settings.configDir = normalizePath(pathJoin(VirtFs::getUserDir(),
"config/settings/Mana",
branding.getValue("appName", "ManaPlus")));
#elif defined _WIN32
settings.configDir = normalizePath(getSpecialFolderLocation(CSIDL_APPDATA));
if (settings.configDir.empty())
{
settings.configDir = normalizePath(settings.localDataDir);
}
else
{
settings.configDir = normalizePath(pathJoin(settings.configDir,
"mana",
branding.getValue("appShort", "mana")));
}
#elif defined __ANDROID__
settings.configDir = normalizePath(pathJoin(getSdStoragePath(),
branding.getValue("appShort", "ManaPlus"),
"config"));
#elif defined __native_client__
settings.configDir = normalizePath(pathJoin(_nacl_dir, "config"));
#elif defined __SWITCH__
settings.configDir = normalizePath(pathJoin(VirtFs::getUserDir(), "config"));
#else
settings.configDir = normalizePath(pathJoin(VirtFs::getUserDir(),
".config/mana",
branding.getValue("appShort", "mana")));
#endif
logger->log("Generating config dir: " + settings.configDir);
}
if (mkdir_r(settings.configDir.c_str()) != 0)
{
// TRANSLATORS: directory creation error
logger->error(strprintf(_("%s doesn't exist and can't be created! "
"Exiting."), settings.configDir.c_str()));
}
}
void Dirs::initUpdatesDir()
{
std::stringstream updates;
if (settings.updateHost.empty())
settings.updateHost = config.getStringValue("updatehost");
if (!checkPath(settings.updateHost))
return;
if (settings.updateHost.length() < 2)
{
if (settings.updatesDir.empty())
settings.updatesDir = normalizePath(pathJoin("updates", settings.serverName));
return;
}
const size_t sz = settings.updateHost.size();
if (settings.updateHost.at(sz - 1) == '/')
settings.updateHost.resize(sz - 1);
const size_t pos = settings.updateHost.find("://");
if (pos != std::string::npos)
{
if (pos + 3 < settings.updateHost.length()
&& !settings.updateHost.empty())
{
updates << "updates/" << settings.updateHost.substr(pos + 3);
settings.updatesDir = normalizePath(updates.str());
}
else
{
// TRANSLATORS: update server initialisation error
logger->log("Error: Invalid update host: %s",
settings.updateHost.c_str());
errorMessage = strprintf(_("Invalid update host: %s."),
settings.updateHost.c_str());
client->setState(State::ERROR);
}
}
else
{
logger->log1("Warning: no protocol was specified for the update host");
updates << "updates/" << settings.updateHost;
settings.updatesDir = normalizePath(updates.str());
}
#ifdef _WIN32
if (settings.updatesDir.find(":") != std::string::npos)
replaceAll(settings.updatesDir, ":", "_");
#endif
const std::string updateDir = normalizePath("/" + settings.updatesDir);
if (!VirtFs::isDirectory(updateDir))
{
if (!VirtFs::mkdir(updateDir))
{
#ifdef _WIN32
std::string newDir = toWindowsPath(normalizePath(pathJoin(settings.localDataDir,
settings.updatesDir)));
if (!CreateDirectory(newDir.c_str(), nullptr) &&
GetLastError() != ERROR_ALREADY_EXISTS)
{
// TRANSLATORS: update server initialisation error
logger->log("Error: %s can't be made, but doesn't exist!",
newDir.c_str());
errorMessage = _("Error creating updates directory!");
client->setState(State::ERROR);
}
#else
// TRANSLATORS: update server initialisation error
logger->log("Error: %s/%s can't be made, but doesn't exist!",
settings.localDataDir.c_str(),
settings.updatesDir.c_str());
errorMessage = _("Error creating updates directory!");
client->setState(State::ERROR);
#endif
}
}
const std::string updateLocal = normalizePath(pathJoin(updateDir, "local"));
const std::string updateFix = normalizePath(pathJoin(updateDir, "fix"));
if (!VirtFs::isDirectory(updateLocal))
VirtFs::mkdir(updateLocal);
if (!VirtFs::isDirectory(updateFix))
VirtFs::mkdir(updateFix);
}
void Dirs::initScreenshotDir()
{
if (!settings.options.screenshotDir.empty())
{
settings.screenshotDir = normalizePath(settings.options.screenshotDir);
if (mkdir_r(settings.screenshotDir.c_str()) != 0)
{
// TRANSLATORS: directory creation error
logger->log(strprintf(
_("Error: %s doesn't exist and can't be created! "
"Exiting."), settings.screenshotDir.c_str()));
}
}
else if (settings.screenshotDir.empty())
{
#ifdef __native_client__
settings.screenshotDir = normalizePath(pathJoin(_nacl_dir, "screenshots/"));
#else
settings.screenshotDir = normalizePath(decodeBase64String(
config.getStringValue("screenshotDirectory3")));
if (settings.screenshotDir.empty())
{
#ifdef __ANDROID__
settings.screenshotDir = normalizePath(getSdStoragePath()
+ std::string("/images"));
if (mkdir_r(settings.screenshotDir.c_str()))
{
// TRANSLATORS: directory creation error
logger->log(strprintf(
_("Error: %s doesn't exist and can't be created! "
"Exiting."), settings.screenshotDir.c_str()));
}
#else
settings.screenshotDir = normalizePath(getPicturesDir());
#endif
if (config.getBoolValue("useScreenshotDirectorySuffix"))
{
const std::string configScreenshotSuffix =
branding.getValue("screenshots", "ManaPlus");
if (!configScreenshotSuffix.empty())
{
settings.screenshotDir = normalizePath(pathJoin(settings.screenshotDir,
configScreenshotSuffix));
}
}
config.setValue("screenshotDirectory3",
encodeBase64String(settings.screenshotDir));
}
#endif
}
// TRANSLATORS: directory creation error
logger->log("screenshotDirectory: " + settings.screenshotDir);
}
void Dirs::initUsersDir()
{
settings.usersDir = normalizePath(settings.serverConfigDir + "/users/");
if (mkdir_r(settings.usersDir.c_str()) != 0)
{
// TRANSLATORS: directory creation error
logger->error(strprintf(_("%s doesn't exist and can't be created!"),
settings.usersDir.c_str()));
}
settings.npcsDir = normalizePath(settings.serverConfigDir + "/npcs/");
if (mkdir_r(settings.npcsDir.c_str()) != 0)
{
// TRANSLATORS: directory creation error
logger->error(strprintf(_("%s doesn't exist and can't be created!"),
settings.npcsDir.c_str()));
}
settings.usersIdDir = normalizePath(settings.serverConfigDir + "/usersid/");
if (mkdir_r(settings.usersIdDir.c_str()) != 0)
{
logger->error(strprintf(_("%s doesn't exist and can't be created!"),
settings.usersIdDir.c_str()));
}
}