QTfrontend/main.cpp
author unC0Rr
Fri, 19 Apr 2024 17:44:55 +0200
changeset 16010 a73b9770467a
parent 15885 ed84eb580643
permissions -rw-r--r--
Port QML frontend to Qt6, use rendering to framebuffer object

/*
 * Hedgewars, a free turn based strategy game
 * Copyright (c) 2004-2015 Andrey Korotaev <unC0Rr@gmail.com>
 *
 * 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; version 2 of the License
 *
 * 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, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "HWApplication.h"

#include <QTranslator>
#include <QLocale>
#include <QRegExp>
#include <QMap>
#include <QSettings>
#include <QStringListModel>
#include <QDate>
#include <QDesktopWidget>
#include <QLabel>
#include <QLibraryInfo>
#include <QStyle>
#include <QStyleFactory>

#include "hwform.h"
#include "hwconsts.h"
#include "newnetclient.h"

#include "DataManager.h"
#include "FileEngine.h"
#include "MessageDialog.h"

#include "SDLInteraction.h"

#ifdef _WIN32
#include <Shlobj.h>
#elif defined __APPLE__
#include "CocoaInitializer.h"

#endif

#ifdef Q_OS_WIN
#include <QSplashScreen>
#endif

#include <QMessageBox>

// Program resources
#ifdef __APPLE__
static CocoaInitializer * cocoaInit = NULL;
#endif
static FileEngineHandler * engine = NULL;

//Determines the day of easter in year
//from http://aa.usno.navy.mil/faq/docs/easter.php,adapted to C/C++
QDate calculateEaster(long year)
{
    int c, n, k, i, j, l, m, d;

    c = year/100;
    n = year - 19*(year/19);
    k = (c - 17)/25;
    i = c - c/4 - (c - k)/3 + 19*n + 15;
    i = i - 30*(i/30);
    i = i - (i/28)*(1 - (i/28)*(29/(i + 1))*((21 - n)/11));
    j = year + year/4 + i + 2 - c + c/4;
    j = j - 7*(j/7);
    l = i - j;
    m = 3 + (l + 40)/44;
    d = l + 28 - 31*(m / 4);

    return QDate(year, m, d);
}

//Checks season and assigns it to the variable season in "hwconsts.h"
void checkSeason()
{
    QDate date = QDate::currentDate();

    //Christmas?
    if (date.month() == 12 && date.day() >= 24
            && date.day() <= 26)
        season = SEASON_CHRISTMAS;
    //Hedgewars birthday?
    else if (date.month() == 10 && date.day() == 31)
    {
        season = SEASON_HWBDAY;
        years_since_foundation = date.year() - 2004;
    }
    else if (date.month() == 4 && date.day() == 1)
    {
        season = SEASON_APRIL1;
    }
    //Easter?
    else if (calculateEaster(date.year()) == date)
        season = SEASON_EASTER;
    else
        season = SEASON_NONE;
}


bool checkForDir(const QString & dir)
{
    QDir tmpdir(dir);
    if (!tmpdir.exists())
        if (!tmpdir.mkpath(dir))
        {
            MessageDialog::ShowErrorMessage(HWApplication::tr("Cannot create directory %1").arg(dir));
            return false;
        }
    return true;
}

// Guaranteed to be the last thing ran in the application's life time.
// Closes resources that need to exist as long as possible.
void closeResources(void)
{
#ifdef __APPLE__
    if (cocoaInit != NULL)
    {
        delete cocoaInit;
        cocoaInit = NULL;
    }
#endif
    if (engine != NULL)
    {
        delete engine;
        engine = NULL;
    }
}

// Simple Message handler that suppresses Qt debug and info messages (qDebug, qInfo).
// Used when printing command line help (--help) or related error to keep console clean.
void restrictedMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
{
    Q_UNUSED(context)
    QByteArray localMsg = msg.toLocal8Bit();
    switch (type) {
    case QtWarningMsg:
    case QtCriticalMsg:
    case QtFatalMsg:
        fprintf(stderr, "%s\n", localMsg.constData());
        break;
    default:
        break;
    }
}

QString getUsage()
{
    return QString(
"%1: hedgewars [%2...] [%3]\n"
"\n"
"%4:\n"
"  --help              %5\n"
"  --config-dir=PATH   %6\n"
"  --data-dir=PATH     %7\n"
"\n"
"%8"
"\n"
).arg(
//: “Usage” as in “how the command-line syntax works”. Shown when running “hedgewars --help” in command-line
HWApplication::tr("Usage", "command-line")
).arg(
//: Name of a command-line argument, shown when running “hedgewars --help” in command-line. “OPTION” as in “command-line option”
HWApplication::tr("OPTION", "command-line")
).arg(
//: Name of a command-line argument, shown when running “hedgewars --help” in command-line
HWApplication::tr("CONNECTSTRING", "command-line")
).arg(
//: “Options” as in “command-line options”
HWApplication::tr("Options", "command-line")
).arg(HWApplication::tr("Display this help", "command-line"))
.arg(HWApplication::tr("Custom path for configuration data and user data", "command-line"))
.arg(HWApplication::tr("Custom path to the game data folder", "command-line"))
.arg(HWApplication::tr("Hedgewars can use a %1 (e.g. \"%2\") to connect on start.", "command-line").arg(HWApplication::tr("CONNECTSTRING", "command-line")).arg(QString("hwplay://") + NETGAME_DEFAULT_SERVER));
}

int main(int argc, char *argv[]) {
    cfgdir->setPath(QDir::homePath());

    // Since we're calling this first, closeResources() will be the last thing called after main() returns.
    atexit(closeResources);

#ifdef __APPLE__
    cocoaInit = new CocoaInitializer(); // Creates the autoreleasepool preventing cocoa object leaks on OS X.
#endif

    HWApplication app(argc, argv);
    app.setAttribute(Qt::AA_DontShowIconsInMenus,false);

    // file engine, to be initialized later
    engine = NULL;

    /*
    This is for messages frelated to translatable command-line arguments.
    If it is non-zero, will print out a message after loading locale
    and exit.
    */
    enum cmdMsgStateEnum {
        cmdMsgNone,
        cmdMsgHelp,
        cmdMsgMalformedArg,
        cmdMsgUnknownArg,
    };
    enum cmdMsgStateEnum cmdMsgState = cmdMsgNone;
    QString cmdMsgStateStr;

    // parse arguments
    QStringList arguments = app.arguments();
    QMap<QString, QString> parsedArgs;
    {
        QList<QString>::iterator i = arguments.begin();
        while(i != arguments.end())
        {
            QString arg = *i;


            QRegExp opt("--(\\S+)=(.+)");
            if(opt.exactMatch(arg))
            {
                parsedArgs[opt.cap(1)] = opt.cap(2);
                i = arguments.erase(i);
            }
            else
            {
                if(arg.startsWith("--")) {
                    if(arg == "--help")
                    {
                        cmdMsgState = cmdMsgHelp;
                        qInstallMessageHandler(restrictedMessageHandler);
                    }
                    else
                    {
                        // argument is something wrong
                        cmdMsgState = cmdMsgMalformedArg;
                        cmdMsgStateStr = arg;
                        qInstallMessageHandler(restrictedMessageHandler);
                        break;
                    }
                }

                // if not starting with --, then always skip
                // (because we can't determine if executable path/call or not - on windows)
                ++i;
            }
        }
    }

    if(cmdMsgState == cmdMsgNone)
    {
        if(parsedArgs.contains("data-dir"))
        {
            QFileInfo f(parsedArgs["data-dir"]);
            parsedArgs.remove("data-dir");
            if(!f.exists())
            {
                qWarning() << "WARNING: Cannot open data-dir=" << f.absoluteFilePath();
            }
            *cDataDir = f.absoluteFilePath();
            custom_data = true;
        }

        if(parsedArgs.contains("config-dir"))
        {
            QFileInfo f(parsedArgs["config-dir"]);
            parsedArgs.remove("config-dir");
            cfgdir->setPath(f.absoluteFilePath());
            custom_config = true;
        }
        else
        {
            custom_config = false;
        }

        if (!parsedArgs.isEmpty())
        {
            cmdMsgState = cmdMsgUnknownArg;
            qInstallMessageHandler(restrictedMessageHandler);
        }

        // end of parameter parsing

        // Select Qt style
        QStyle* coreStyle;
        coreStyle = QStyleFactory::create("Windows");
        if(coreStyle != 0) {
            QApplication::setStyle(coreStyle);
            qDebug("Qt style set: Windows");
        } else {
            // Windows style should not be missing in Qt5 Base. If it does, something went terribly wrong!
            qWarning("No Qt style could be set! Using the default one.");
        }
    }

#ifdef Q_OS_WIN
    // Splash screen for Windows
    QPixmap pixmap(":/res/splash.png");
    QSplashScreen splash(pixmap);
    if(cmdMsgState == cmdMsgNone)
    {
        splash.show();
    }
#endif

    QDateTime now = QDateTime::currentDateTime();
    srand(now.toTime_t());
    rand();

    Q_INIT_RESOURCE(hedgewars);

    qRegisterMetaType<HWTeam>("HWTeam");

    bindir->cd(QCoreApplication::applicationDirPath());

    if(custom_config == false)
    {
#ifdef __APPLE__
        checkForDir(cfgdir->absolutePath() + "/Library/Application Support/Hedgewars");
        cfgdir->cd("Library/Application Support/Hedgewars");
#elif defined _WIN32
        wchar_t path[MAX_PATH];
        if(SHGetFolderPathW(0, CSIDL_PERSONAL, NULL, 0, path) == S_OK)
        {
            cfgdir->cd(QString::fromWCharArray(path));
            checkForDir(cfgdir->absolutePath() + "/Hedgewars");
            cfgdir->cd("Hedgewars");
        }
        else // couldn't retrieve documents folder? almost impossible, but in case fall back to classic path
        {
            checkForDir(cfgdir->absolutePath() + "/.hedgewars");
            cfgdir->cd(".hedgewars");
        }
#else
        checkForDir(cfgdir->absolutePath() + "/.hedgewars");
        cfgdir->cd(".hedgewars");
#endif
    }

    if (checkForDir(cfgdir->absolutePath()))
    {
        // alternative loading/lookup paths
        checkForDir(cfgdir->absolutePath() + "/Data");

        // config/save paths
        checkForDir(cfgdir->absolutePath() + "/Demos");
        checkForDir(cfgdir->absolutePath() + "/DrawnMaps");
        checkForDir(cfgdir->absolutePath() + "/Saves");
        checkForDir(cfgdir->absolutePath() + "/Screenshots");
        checkForDir(cfgdir->absolutePath() + "/Teams");
        checkForDir(cfgdir->absolutePath() + "/Logs");
        checkForDir(cfgdir->absolutePath() + "/Videos");
        checkForDir(cfgdir->absolutePath() + "/VideoTemp");
        checkForDir(cfgdir->absolutePath() + "/VideoThumbnails");
    }

    datadir->cd(bindir->absolutePath());
    datadir->cd(*cDataDir);
    if (!datadir->cd("Data"))
    {
        MessageDialog::ShowFatalMessage(HWApplication::tr("Failed to open data directory:\n%1\n\nPlease check your installation!").arg(datadir->absolutePath()+"/Data"));
        return 1;
    }

    bool isProbablyNewPlayer = false;

    // setup PhysFS
    engine = new FileEngineHandler(argv[0]);
    engine->mount(datadir->absolutePath());
    engine->mount(cfgdir->absolutePath() + "/Data");
    engine->mount(cfgdir->absolutePath());
    engine->setWriteDir(cfgdir->absolutePath());
    engine->mountPacks();

    QTranslator TranslatorHedgewars;
    QTranslator TranslatorQt;
    QSettings settings(DataManager::instance().settingsFileName(), QSettings::IniFormat);
    settings.setIniCodec("UTF-8");
    {
        QString cc = settings.value("misc/locale", QString()).toString();
        if (cc.isEmpty())
        {
            cc = QLocale::system().name();
            qDebug("Detected system locale: %s", qPrintable(cc));

            // Fallback to current input locale if "C" locale is returned
            if(cc == "C")
                cc = HWApplication::inputMethod()->locale().name();
        }
        else
        {
            qDebug("Configured frontend locale: %s", qPrintable(cc));
        }
        QLocale::setDefault(cc);
        QString defaultLocaleName = QLocale().name();
        qDebug("Frontend uses locale: %s", qPrintable(defaultLocaleName));

        if (defaultLocaleName != "C")
        {
            // Load locale files into translators
            if (!TranslatorHedgewars.load(QLocale(), "hedgewars", "_", QString("physfs://Locale")))
                qWarning("Failed to install Hedgewars translation (%s)", qPrintable(defaultLocaleName));
            if (!TranslatorQt.load(QLocale(), "qt", "_", QString(QLibraryInfo::location(QLibraryInfo::TranslationsPath))))
                qWarning("Failed to install Qt translation (%s)", qPrintable(defaultLocaleName));
            app.installTranslator(&TranslatorHedgewars);
            app.installTranslator(&TranslatorQt);
        }
        app.setLayoutDirection(QLocale().textDirection());

        // Handle command line messages
        switch(cmdMsgState)
        {
            case cmdMsgHelp:
            {
                printf("%s", getUsage().toUtf8().constData());
                return 0;
            }
            case cmdMsgMalformedArg:
            {
                fprintf(stderr, "%s\n\n%s",
                    HWApplication::tr("Malformed option argument: %1", "command-line").arg(cmdMsgStateStr).toUtf8().constData(),
                    getUsage().toUtf8().constData());
                return 1;
            }
            case cmdMsgUnknownArg:
            {
                foreach (const QString & key, parsedArgs.keys())
                {
                    fprintf(stderr, "%s\n",
                        HWApplication::tr("Unknown option argument: %1", "command-line").arg(QString("--") + key).toUtf8().constData());
                }
                fprintf(stderr, "\n%s", getUsage().toUtf8().constData());
                return 1;
            }
            default:
            {
                break;
            }
        }

        // Heuristic to figure out if the user is (probably) a first-time player.
        // If nickname is not set, then probably yes.
        // The hidden setting firstLaunch is, if present, used to force HW to
        // treat iself as if it were launched the first time.
        QString nick = settings.value("net/nick", QString()).toString();
        if (settings.contains("frontend/firstLaunch"))
        {
            isProbablyNewPlayer = settings.value("frontend/firstLaunch").toBool();
        }
        else
        {
            isProbablyNewPlayer = nick.isNull();
        }

        // Set firstLaunch to false to make sure we remember we have been launched before.
        settings.setValue("frontend/firstLaunch", false);
    }

#ifdef _WIN32
    // Win32 registry setup (used for external software detection etc.
    // don't set it if running in "portable" mode with a custom config dir)
    if(!custom_config)
    {
        QSettings registry_hklm("HKEY_LOCAL_MACHINE", QSettings::NativeFormat);
        registry_hklm.setValue("Software/Hedgewars/Frontend", bindir->absolutePath().replace("/", "\\") + "\\hedgewars.exe");
        registry_hklm.setValue("Software/Hedgewars/Path", bindir->absolutePath().replace("/", "\\"));
    }
#endif

    SDLInteraction::instance();

    QString style = "";
    QString fname;

    bool holidaySilliness = settings.value("misc/holidaySilliness", true).toBool();
    if(holidaySilliness)
        checkSeason();
    else
        season = SEASON_NONE;

    // For each season, there is an extra stylesheet.
    // TODO: change background for easter
    // (simply replace res/BackgroundEaster.png
    // with an appropriate background).
    switch (season)
    {
        case SEASON_CHRISTMAS :
            fname = "christmas.css";
            break;
        case SEASON_APRIL1 :
            fname = "april1.css";
            break;
        case SEASON_EASTER :
            fname = "easter.css";
            break;
        case SEASON_HWBDAY :
            fname = "birthday.css";
            break;
        default :
            fname = "qt.css";
            break;
    }

    // load external stylesheet if there is any
    QFile extFile("physfs://css/" + fname);

    QFile resFile(":/res/css/" + fname);

    QFile & file = (extFile.exists() ? extFile : resFile);

    if (file.open(QIODevice::ReadOnly | QIODevice::Text))
        style.append(file.readAll());

    qWarning("Starting Hedgewars %s-r%d (%s)", qPrintable(*cVersionString), cRevisionString->toInt(), qPrintable(*cHashString));

    app.form = new HWForm(NULL, style);
#ifdef Q_OS_WIN
    if(cmdMsgState == cmdMsgNone)
        splash.finish(app.form);
#endif
    app.form->show();

    // Show welcome message for (suspected) first-time player and
    // point towards the Training menu.
    if(isProbablyNewPlayer) {
        QMessageBox questionTutorialMsg(app.form);
        questionTutorialMsg.setIcon(QMessageBox::Question);
        questionTutorialMsg.setWindowTitle(QMessageBox::tr("Welcome to Hedgewars"));
        questionTutorialMsg.setText(QMessageBox::tr("Welcome to Hedgewars!\n\nYou seem to be new around here. Would you like to play some training missions first to learn the basics of Hedgewars?"));
        questionTutorialMsg.setTextFormat(Qt::PlainText);
        questionTutorialMsg.setWindowModality(Qt::WindowModal);
        questionTutorialMsg.addButton(QMessageBox::Yes);
        questionTutorialMsg.addButton(QMessageBox::No);

        int answer = questionTutorialMsg.exec();
        if (answer == QMessageBox::Yes) {
            app.form->GoToTraining();
        }
    }

    if (app.urlString)
        app.fakeEvent();
    return app.exec();
}