#include "crsm.hpp" #include #include #include #include #include #include #include #include #define MGMT_BUFFER_FILENAME "CRSM-MGMT-Buffer" CRSM::CRSM(QObject *parent) : QObject(parent), Session(this) { qsrand(QDateTime::currentMSecsSinceEpoch()); codec = QTextCodec::codecForName("Windows-1252"); qout = new QTextStream(stdout, QIODevice::WriteOnly | QIODevice::Unbuffered); qin = new QTextStream(stdin, QIODevice::ReadOnly); qout->setCodec(QTextCodec::codecForName("UTF-8")); outputBuffer.setFileName(MGMT_BUFFER_FILENAME); outputBuffer.open(QFile::WriteOnly | QFile::Unbuffered); finish = false; ok = true; readConfig(); if(!ok) return; ok = false; connect(&managementServer, SIGNAL(newConnection()), this, SLOT(newManagementConnection())); managementServer.listen(QHostAddress::LocalHostIPv6, Config.CRSM.ManagementPort); listC4Folders(); readScenarios(); if(!Config.Auto.ProcessManager.ReattachId.isEmpty()) { processManager = new ProcessManager("CRSM-Clonkserver-", Config.Auto.ProcessManager.ReattachId); } else { processManager = new ProcessManager("CRSM-Clonkserver-"); } if(!processManager->isOk()) { out("Could not start Process Manager!\n"); Config.Auto.ProcessManager.ReattachId.clear(); writeConfig(); return; } Config.Auto.ProcessManager.ReattachId = processManager->ID(); writeConfig(); if(processManager->isRunning()) { Session.read(Config.CRSM.SessionFile, false); writeToServer("Der Server Manager läuft wieder.\nBis zur nächsten Runde könnte unerwartetes Verhalten auftreten.\n"); } QFile sessionFile(Config.CRSM.SessionFile); if(sessionFile.exists()) sessionFile.remove(); connect(&greetMapper, SIGNAL(mapped(QString)), this, SLOT(greet(QString))); autoHost = Config.Hosting.Auto; connect(processManager, SIGNAL(readyRead()), this, SLOT(readServerOutput())); connect(processManager, SIGNAL(finished(int)), this, SLOT(scenarioFinished())); afkAdminTimer.setSingleShot(true); connect(&afkAdminTimer, SIGNAL(timeout()), this, SLOT(afkAdminTimeout())); QFile *reallog = new QFile(Config.Auto.Volatile.Clonk.Directory + "CRSM.log"); reallog->open(QIODevice::Append | QIODevice::Text); logstream.setDevice(reallog); ok = true; } CRSM::~CRSM() { } void CRSM::start() { if(autoHost && !processManager->isRunning()) nextScen(); } bool CRSM::isOk() { return ok; } void CRSM::readServerOutput() { static QRegExp timeRemover("^>?\\s*\\[(\\d\\d:\\d\\d:\\d\\d)\\]\\s+(.*)$"); QString what(processManager->readLine()); if(Config.Readline.ServerUses) { while(writtenToServer.length() > 0 && what.length() > 0 && writtenToServer.at(0) == what.at(0)) { writtenToServer.remove(0, 1); what.remove(0, 1); } if(what.at(0) == '>') what.remove(0, 1); } if(what.isEmpty()) return; logstream << what.trimmed() << endl; if(Config.IRC.Use) { foreach(const QString &mess, what.split("\n", QString::SkipEmptyParts)) foreach(const QString &mod, ircModIOList) { connection->sendCommand(IrcCommand::createNotice(mod, mess)); } } out(what.trimmed() + "\n"); if(!timeRemover.exactMatch(what)) return; what = timeRemover.cap(2).trimmed(); static QRegExp userexp("^\\s*(\\*?)\\s*<(.*)\\|(\\d+)\\|([^>]*)>\\s+(.*)$"); if(userexp.exactMatch(what)) { bool isMeMessage = (userexp.cap(1) == "*"); QString user = userexp.cap(4); QString pcName = userexp.cap(2); int cuid = userexp.cap(3).toInt(); if(pcName != Config.Auto.Volatile.Clonk.ServerPCName) { ClientInfo& info = getClientInfo(pcName, cuid, user); if(info == Session.Clonk.Admin) { checkActivity(Session.Clonk.Admin); } QString msg = userexp.cap(5).trimmed(); if(info.floodCheck(Config.Clonk.Chat.AntiFlood.Count, Config.Clonk.Chat.AntiFlood.Time, QDateTime(QDate::currentDate(), QTime::fromString(timeRemover.cap(1), "hh:mm:ss")))) { kick(pcName, "Flooding! Maximal " + QString::number(Config.Clonk.Chat.AntiFlood.Count) + " Nachrichten in " + QString::number(Config.Clonk.Chat.AntiFlood.Time) + "s"); } else if(!isMeMessage) { if(msg.length() > Config.CRSM.CommandSign.length() && msg.left(Config.CRSM.CommandSign.length()) == Config.CRSM.CommandSign) { QString command = msg.mid(Config.CRSM.CommandSign.length()); if(!cmd(command, info)) { respond(info, "Unbekannter Befehl: \"" + command + "\"!\n"); } } else if(Session.IRC.UseIngameChat) { connection->sendCommand(IrcCommand::createMessage(Config.IRC.IngameChannel, "[Clonk]<" + user + "> " + msg)); } } else if(Session.IRC.UseIngameChat) { connection->sendCommand(IrcCommand::createCtcpAction(Config.IRC.IngameChannel, "[Clonk] " + user + " " + msg)); } } } static QRegExp joinExp("^Client (.+) (?:verbunden|connected)\\.\\s*$"); if(joinExp.exactMatch(what)) { static QRegExp infoMatch("^<(.*)\\|(\\d+)\\|(.*)>$"); if(infoMatch.exactMatch(joinExp.cap(1))) { const ClientInfo& info = ClientInfo::clonkClient(infoMatch.cap(3), infoMatch.cap(1), infoMatch.cap(2).toInt()); Session.Clonk.Clients.insert(info.pcName, info); QTimer *timer = new QTimer; connect(timer, SIGNAL(timeout()), &greetMapper, SLOT(map())); connect(timer, SIGNAL(timeout()), timer, SLOT(deleteLater())); greetMapper.setMapping(timer, info.pcName); timer->start(1000); if(Session.IRC.UseIngameChat) { connection->sendCommand(IrcCommand::createMessage(Config.IRC.IngameChannel, "[Clonk] " + info.toString() + " verbunden.")); } } } static QRegExp activatedExp("^Client (.+) (?:aktiviert|activated)\\.\\s*$"); if(activatedExp.exactMatch(what)) { QRegExp infoMatch("^<(.*)\\|(\\d+)\\|(.*)>$"); if(infoMatch.exactMatch(activatedExp.cap(1))) { ClientInfo &info = getClientInfo(infoMatch.cap(1), infoMatch.cap(2).toInt(), infoMatch.cap(3)); info.activated = true; } } static QRegExp deactivatedExp("^Client (.+) (?:deaktiviert|deactivated)\\.\\s*$"); if(deactivatedExp.exactMatch(what)) { QRegExp infoMatch("^<(.*)\\|(\\d+)\\|(.*)>$"); if(infoMatch.exactMatch(joinExp.cap(1))) { ClientInfo &info = getClientInfo(infoMatch.cap(1), infoMatch.cap(2).toInt(), infoMatch.cap(3)); info.activated = false; } } static QRegExp lobbyStartExp(R"((?:Los geht's!|Action go!)\s*)"); if(lobbyStartExp.exactMatch(what)) { Session.State = CRSMSession::Loading; ircSetIngameChannelTopic(); } static QRegExp startExp("^Start!\\s*$"); if(startExp.exactMatch(what)) { Session.State = CRSMSession::Running; Stats.AddScenarioStart(Session.Scenario.wishClient, scenarioFileName(Session.Scenario.name)); if(!Session.League) { writeToServer(QString("/set maxplayer 0\n")); } ircSetIngameChannelTopic(); } static QRegExp leaveExp("^Client (.+) (?:entfernt|removed)(.*)"); if(leaveExp.exactMatch(what)) { QRegExp infoMatch("^<(.*)\\|(\\d+)\\|(.*)>$"); if(infoMatch.exactMatch(leaveExp.cap(1))) { ClientInfo &info = getClientInfo(infoMatch.cap(1), infoMatch.cap(2).toInt(), infoMatch.cap(3)); writeToServer(QString(info.nick +" ist ein L34V0R!\n")); if(Session.IRC.UseIngameChat) { connection->sendCommand(IrcCommand::createMessage(Config.IRC.IngameChannel, "[Clonk] " + info.toString() + " entfernt" + leaveExp.cap(2))); } if(info == Session.Clonk.Admin) { Session.Clonk.LeaveAdmins.insert(info, QDateTime::currentDateTime()); writeToServer("Rundenadmin wurde freigegeben.\n"); Session.Clonk.Admin.clear(); } Session.Clonk.Clients.remove(info.pcName); } if(Session.Clonk.Clients.size() == 0 && userlist.length() > 0 && !Session.UserWish) { processManager->closeProgFifos(); } else if(Session.Clonk.Clients.size() == 0 && Config.Clonk.Server.EmptyTimer != -1 && (Session.CountDown == -1 || Session.CountDown > Config.Clonk.Server.EmptyTimer)) { writeToServer("/start " + QString::number(Config.Clonk.Server.EmptyTimer) + "\n"); } } static QRegExp countDownExp("^(?:Das Spiel beginnt in (\\d+) Sekunden\\!|The game will start in (\\d+) seconds\\.)"); if(countDownExp.exactMatch(what)) { Session.CountDown = countDownExp.cap(1).toInt(); } static QRegExp countDownExp2(R"(^(\d+)\.\.\.)"); if(countDownExp2.exactMatch(what)) { Session.CountDown = countDownExp2.cap(1).toInt(); } static QRegExp countDownStopExp("^(?:Spielstart abgebrochen\\.|Game start aborted\\.)"); if(countDownStopExp.exactMatch(what)) { Session.CountDown = -1; } static QRegExp gameRegisterFailExp(R"(^(?:Spiel konnte nicht registriert werden|Could not register game): (.*))"); if(gameRegisterFailExp.exactMatch(what)) { QString reason = gameRegisterFailExp.cap(1); userlist.clear(); if(autoHost && !hostingIsErrorDeactivated) { hostingIsErrorDeactivated = true; gameRegisterFailTimer.singleShot(5*60*1000, this, SLOT(enableAutoHosting())); } autoHost = false; static const QString gameRegisterFailMessage = "Aufgrund eines Problems beim Registrieren des Spiels am Masterserver (%1) wird Hosting temporär (für 5 Minuten) deaktiviert.\n"; out(gameRegisterFailMessage.arg(reason)); if(Config.IRC.Use) { connection->sendCommand(IrcCommand::createMessage(Config.IRC.Channel, gameRegisterFailMessage.arg(reason))); if(Config.IRC.UseIngameChat) { connection->sendCommand(IrcCommand::createMessage(Config.IRC.IngameChannel, gameRegisterFailMessage.arg(reason))); } } } } void CRSM::nextScen() { if(hostingIsErrorDeactivated) { return; } if(userlist.length()>0) { startScen(userlist.at(0), args); userlist.removeFirst(); Session.UserWish = true; } else { startScen(nextAutoScens.first(), args); nextAutoScens.removeFirst(); updateNextAutoScens(); } } void CRSM::scenarioFinished() { if(finish) { writeFiles(); if(processManager != nullptr) processManager->exit(); if(connection != 0) { connection->quit(Config.IRC.QuitMessage); connect(connection, SIGNAL(disconnected()), QCoreApplication::instance(), SLOT(quit())); QTimer::singleShot(500, QCoreApplication::instance(), SLOT(quit())); } else QCoreApplication::quit(); return; } Session.clear(); if((autoHost || userlist.length() > 0) && !finish) { nextScen(); } else { ircSetIngameChannelTopic(); } QFile lastScenFile(LAST_SCEN_FILE_NAME); QFile curScenFile(CUR_SCEN_FILE_NAME); curScenFile.open(QFile::ReadOnly); lastScenFile.open(QFile::WriteOnly); lastScenFile.write(curScenFile.readAll()); lastScenFile.close(); curScenFile.close(); curScenFile.open(QFile::WriteOnly); curScenFile.close(); } void CRSM::ircMessageReceived(IrcMessage *message) { if(message->isOwn()) return; if(message->type() == IrcMessage::Notice) { if(message->nick() == "NickServ") { QStringList split = message->parameters().at(1).split(' ', QString::SkipEmptyParts); if(((IrcNoticeMessage*)message)->content().contains("nickname is registered")) { connection->sendCommand(IrcCommand::createMessage("NickServ", "IDENTIFY " + Config.IRC.Nick + " " + Config.IRC.Password)); } else if(split.first() == "STATUS") { QString statusNick = split.at(1); int status = split.at(2).toInt(); if(ircStatusFifos.contains(statusNick)) { QList> &fifo = ircStatusFifos[statusNick]; while(fifo.size() > 0) { (this->*fifo.first().second)(fifo.first().first, status, ClientInfo::ircClient(statusNick)); fifo.removeFirst(); } } } } } else if(message->type() == IrcMessage::Private) { IrcPrivateMessage* privMessage = (IrcPrivateMessage*)message; QString target = privMessage->target(); if(target == connection->nickName()) target = message->nick(); QString mess = privMessage->content(); const ClientInfo& client = ClientInfo::ircClient(message->nick(), target); if(client == Session.IRC.Admin) { checkActivity(Session.IRC.Admin); } if(target == Config.IRC.IngameChannel && Session.IRC.UseIngameChat) { if(privMessage->isAction()) { writeToServer("/me [IRC] " + message->nick() + " " + mess + "\n"); } else { writeToServer("[IRC]<" + message->nick() + "> " + mess + "\n"); } } else if(!privMessage->isAction() && (mess.length() > Config.CRSM.CommandSign.length() && mess.left(Config.CRSM.CommandSign.length()) == Config.CRSM.CommandSign)) { QString command = mess.mid(Config.CRSM.CommandSign.length()).trimmed(); if(!cmd(command, client)) { respond(client, "Unbekannter Befehl: \"" + command + "\"!"); } } else if(privMessage->isPrivate() && ircMods.contains(target)) { if(aliasWishEditor == target) { editAliasWishes(mess); } else if(ircModIOList.contains(message->nick())) { QString writeMessage; if(mess.at(0) == '\\' || mess.at(0) == '/') { mess[0] = '/'; } else { writeMessage = "[IRC]<" + message->nick() + "> "; } writeMessage += mess; writeToServer(writeMessage + "\n"); } } if(ircModWatchList.length() > 0) { QString watchMsg = "<"; if(!privMessage->isPrivate()) { watchMsg += target + ":"; } watchMsg += message->nick() + "> " + mess; foreach(const ClientInfo& client, ircModWatchList) { connection->sendCommand(IrcCommand::createMessage(client.nick, watchMsg)); } } } else if(message->type() == IrcMessage::Join) { IrcJoinMessage* joinMessage = (IrcJoinMessage*) message; QString joinChannel = joinMessage->channel(); connection->sendCommand(IrcCommand::createMessage(joinChannel, "Hallo " + message->nick() + "!")); if(joinChannel == Config.IRC.IngameChannel && Session.IRC.UseIngameChat) { writeToServer("[IRC] " + message->nick() + " hat den Channel betreten." + "\n"); } else ircCheckUserStatus(ClientInfo::ircClient(message->nick()), ClientInfo::ircClient(message->nick()), &CRSM::ircModCmd); } else if(message->type() == IrcMessage::Quit) { ircMods.removeAll(message->nick()); ircModChecks.removeAll(message->nick()); ircModIOList.removeAll(message->nick()); ircModWatchList.removeAll(ClientInfo::ircClient(message->nick())); if(aliasWishEditor == message->nick()) { stopAliasWishEditing(); } if(Session.IRC.Admin == ClientInfo::ircClient(message->nick())) { Session.IRC.Admin.clear(); } } else if(message->type() == IrcMessage::Kick) { IrcKickMessage* kickMessage = (IrcKickMessage*)message; if(kickMessage->user() == connection->nickName()) { connection->sendCommand(IrcCommand::createJoin(kickMessage->channel())); } } else if(message->type() == IrcMessage::Part) { IrcPartMessage* partMessage = (IrcPartMessage*)message; QString leaveChannel = partMessage->channel(); if(leaveChannel == Config.IRC.IngameChannel && Session.IRC.UseIngameChat) { writeToServer("[IRC] " + message->nick() + " hat den Channel verlassen." + "\n"); } } else if(message->type() == IrcMessage::Mode) { IrcModeMessage* modeMessage = (IrcModeMessage*)message; QRegExp modeExp("^\\+[a-zA-Z]*(a|o)"); if(Config.IRC.UseIngameChat && message->parameters().size() >= 3 && modeMessage->target() == Config.IRC.IngameChannel && modeExp.exactMatch(modeMessage->mode()) && modeMessage->argument() == Config.IRC.Nick) { ircSetIngameChannelTopic(); } } } void CRSM::ircConnected() { IrcConnection* conn = (IrcConnection*)sender(); Q_ASSERT(conn == connection); connection->sendCommand(IrcCommand::createMode(Config.IRC.Nick, "+B")); connection->sendCommand(IrcCommand::createJoin(Config.IRC.Channel)); if(Config.IRC.UseIngameChat && !Config.IRC.IngameChannel.isEmpty()) { connection->sendCommand(IrcCommand::createJoin(Config.IRC.IngameChannel)); ircSetIngameChannelTopic(); } else { Session.IRC.UseIngameChat = Config.IRC.UseIngameChat = false; } } void CRSM::greet(QString pcName) { if(!Session.Clonk.Clients.contains(pcName)) return; const ClientInfo &info = Session.Clonk.Clients.value(pcName); writeToServer(QString("Hallo " + info.nick + "!\n")); if(Session.Clonk.LeaveAdmins.contains(info)) { int timeGone; if((timeGone = Session.Clonk.LeaveAdmins.value(info).secsTo(QDateTime::currentDateTime())) < Config.Clonk.Chat.RegainAdminTime && !Session.Clonk.Admin.empty()) { writeToServer(info.nick + "! Der Rundenadmin wurde freigegeben, weil du das Spiel verlassen hast.\nDu hast noch " + QString::number(Config.Clonk.Chat.RegainAdminTime - timeGone) + "s Zeit um den Rundenadmin zurückzuholen.\n"); } else { Session.Clonk.LeaveAdmins.remove(info); } } } void CRSM::newManagementConnection() { QTcpSocket* sock = managementServer.nextPendingConnection(); connect(sock, SIGNAL(readyRead()), this, SLOT(newManagementData())); connect(sock, SIGNAL(disconnected()), this, SLOT(managementConnectionDisconnected())); sock->write(QString("Willkommen beim CRSM-Management-Interface!\nIhr Name: ").toUtf8()); } void CRSM::newManagementData() { QTcpSocket* sock = (QTcpSocket*)sender(); QString allData = sock->readAll().trimmed(); foreach(const QString& data, allData.split('\n')) { if(!managementConnections.contains(sock)) { if(data.isEmpty()) { sock->write("Ihr Name: "); } else { ManagementConnection conn; conn.socket = sock; conn.name = data; out(conn.name + " logged in on Management-Interface.\n"); managementConnections.insert(sock, conn); replayOutputBuffer(sock); } } else { if(data.isEmpty()) { continue; } ManagementConnection& conn = managementConnections[sock]; if(data.at(0) == '/') { QString command = data.mid(1).trimmed(); if(cmd(command, ClientInfo::managementClient(conn))) { continue; } else { writeToServer(data + "\n"); } } else { writeToServer(ClientInfo::managementClient(conn).toString(true) + " " + data + "\n"); } } } } void CRSM::managementConnectionDisconnected() { QTcpSocket* sock = dynamic_cast(sender()); if(sock == nullptr) return; if(!managementConnections.value(sock).name.isEmpty()) out(managementConnections.value(sock).name + " disconnected from Management-Interface.\n"); if(managementConnections.contains(sock)) { managementConnections.remove(sock); } } void CRSM::updateNextAutoScens() { if(autolist.length() <= 0) return; while(nextAutoScens.length() < Config.Hosting.UserListLength) { ScenarioSettings next(""); if(Config.Hosting.RandomizeAuto) { next = autolist.at(qrand() % autolist.length()); } else { next = autolist.at(current); if(++current >= autolist.length()) current = 0; } if(next.randomLeague) { next.league = qrand() % 2; } nextAutoScens.append(next); } } void CRSM::startScen(const ScenarioSettings &scen, QStringList argList) { QString filename; QFile lastScenFile(LAST_SCEN_FILE_NAME); QFile curScenFile(CUR_SCEN_FILE_NAME); curScenFile.open(QFile::ReadOnly); lastScenFile.open(QFile::WriteOnly); lastScenFile.write(curScenFile.readAll()); lastScenFile.close(); curScenFile.close(); curScenFile.open(QFile::WriteOnly); curScenFile.write(scen.name.toUtf8()); curScenFile.close(); QFile scoreboardFile(SCOREBOARD_FILE_NAME); scoreboardFile.open(QFile::WriteOnly); scoreboardFile.close(); Session.Scenario = scen; Session.IRC.UseIngameChat = Config.IRC.UseIngameChat; Session.State = CRSMSession::Lobby; ircSetIngameChannelTopic(); filename = scenarioFileName(scen.name); if(scen.league) { argList << "/league"; Session.League = true; } else { argList << "/noleague"; } argList << filename; processManager->setWorkingDirectory(Config.Auto.Volatile.Clonk.Directory); out(Packs.linkScenarioPacks(filename)); processManager->start(Config.Clonk.Server.Executable, argList); } void CRSM::readConfig() { Config.clear(); out(Config.read(CONFIG_FILE_NAME)); applyConfig(); return; } void CRSM::readScenarios() { QFile scenfile("scenarios.lst"); if(!scenfile.exists()) { out("No scenarios.lst found!"); scenfile.open(QFile::WriteOnly); scenfile.write("Worlds.c4f/Goldmine.c4s"); scenfile.close(); } scenfile.open(QFile::ReadOnly); QStringList scenlist = QString(scenfile.readAll()).trimmed().split("\n",QString::SkipEmptyParts); out("Scenarios in list:\n"); autolist.clear(); nextAutoScens.clear(); current = 0; foreach(const QString &args, scenlist) { ScenarioSettings scen(args, ClientInfo::autoClient()); QStringList argList = args.split(' ', QString::KeepEmptyParts); if(argList.first().trimmed() == "--league") { argList.removeFirst(); scen.league = true; scen.name = argList.join(' '); } else if(argList.first().trimmed() == "--random-league") { argList.removeFirst(); scen.randomLeague = true; scen.name = argList.join(' '); } QString scenName = scen.name; if(!(scen.name = scenPath(scen.name)).isEmpty()) { out(scen.name + "\n"); autolist.append(scen); } else { out("WARNING: Scenario " + scenName + " not found!\n"); } } out("\n"); scenfile.close(); updateNextAutoScens(); } void CRSM::listC4Folders() { out("Listing Contents of C4Folders..."); QDirIterator it(Config.Auto.Volatile.Clonk.Directory, QDirIterator::FollowSymlinks); for(; it.hasNext(); it.next()) { if(it.fileName() == ".." || it.fileName() == ".") continue; if((it.fileInfo().suffix() == "c4f" || (it.fileInfo().isDir() && !QDir(it.fileInfo().absoluteFilePath()).entryList(QStringList() << "*.c4f" << "*.c4s").isEmpty())) && it.fileName() != "." && it.fileName() != ".." && !Config.Clonk.Server.IgnoreFolders.contains(it.fileInfo().baseName())) { QFileInfo listInfo(Config.CRSM.ListFolder + it.fileName() + ".lst"); if(listInfo.exists() && it.fileInfo().exists() && it.fileInfo().lastModified() < listInfo.lastModified()) continue; const QStringList& list = listC4Folder(it.filePath()); if(!list.isEmpty()) { QFile listFile(listInfo.filePath()); listFile.open(QFile::WriteOnly); foreach(const QString& scen, list) { listFile.write(scen.toUtf8() + "\n"); } listFile.close(); } } } out(QString(3, '\b') + ": Finished\n"); } void CRSM::cleanUp() { out("\nCleaning up Clonk Folder...\n"); QDirIterator it(Config.Auto.Volatile.Clonk.Directory+"Network/", QDirIterator::FollowSymlinks | QDirIterator::Subdirectories); for(; it.hasNext(); it.next()) if(it.fileInfo().exists()) QFile(it.fileInfo().absoluteFilePath()).remove(); out("\n"); } QString CRSM::scenPath(QString scenName) { bool isAlias = false; QString aliasName; foreach(const QString& alias, Config.Hosting.Alias.keys()) { if(alias.compare(scenName, Qt::CaseInsensitive) == 0) { aliasName = scenName = alias; break; } } while(Config.Hosting.Alias.contains(scenName)) { scenName = Config.Hosting.Alias.value(scenName); isAlias = true; } QFileInfo fileInfo(Config.Auto.Volatile.Clonk.Directory + scenName); if(fileInfo.suffix() != "c4s") { return QString(); } bool exists = fileInfo.exists(); if(exists && isAlias) { return aliasName; } if(exists) { return scenName; } else { QStringList split = scenName.split('/'); QString name = scenName.mid(split.first().length() + 1); if(split.length() >= 2) { if(split.first().right(4) == ".c4f") { const QStringList& entryList = QDir(Config.Auto.Volatile.Clonk.Directory).entryList(QStringList() << "*.c4f"); QString folderName = split.first(); foreach(const QString& entry, entryList) { if(entry.compare(folderName, Qt::CaseInsensitive) == 0) { folderName = entry; break; } } QFile lstFile(Config.CRSM.ListFolder + folderName + ".lst"); if(lstFile.exists()) { lstFile.open(QFile::ReadOnly); while(!lstFile.atEnd()) { const QString& line = lstFile.readLine().trimmed(); if(line.compare(name, Qt::CaseInsensitive) == 0) { lstFile.close(); if(isAlias) { return aliasName; } else { return folderName + '/' + line; } } } } lstFile.close(); } } } return QString(); } QString CRSM::listScenarios(QString commandArgs) { QString ret; if(commandArgs.isEmpty()) { ret += "Folgende Szenarien stehen zur Auswahl:\n"; QDirIterator it(Config.Auto.Volatile.Clonk.Directory, QDirIterator::FollowSymlinks); for(; it.hasNext(); it.next()) { if(it.fileInfo().suffix() == "c4s" && !it.fileInfo().absoluteFilePath().contains(Config.Auto.Volatile.Clonk.Directory+"Network/")) ret += QString(" "+it.fileInfo().absoluteFilePath().replace(Config.Auto.Volatile.Clonk.Directory,"")+"\n"); } ret += "-----------------------------------------------------------------\nFolgende Ordner stehen zur Auswahl:\n"; QDirIterator folderIt(Config.CRSM.ListFolder, QDirIterator::FollowSymlinks); for(; folderIt.hasNext(); folderIt.next()) { if(folderIt.fileInfo().suffix() == "lst" && !Config.Clonk.Server.IgnoreFolders.contains(folderIt.fileInfo().baseName())) ret += " " + folderIt.fileInfo().completeBaseName() + "\n"; } } else if(commandArgs.toLower() == "aliase") { ret += "Vorhandene Aliase:\n"; foreach(const QString &alias, Config.Hosting.Alias.keys()) { ret += QString(" " + alias + " = " + Config.Hosting.Alias.value(alias) + "\n"); } } else { QFile file(Config.CRSM.ListFolder + commandArgs + ".lst"); if(file.exists()) { ret += "Der Ordner \"" + commandArgs + QString("\" enthält folgende Szenarien:\n"); file.open(QFile::ReadOnly); while(!file.atEnd()) ret += " " + QString::fromUtf8(file.readLine()).trimmed() + "\n"; } else ret += "Der Ordner \"" + commandArgs + "\" wurde nicht gefunden!\n"; } return ret; } QString CRSM::printQueue() { QString ret; if(userlist.length() == 0 && !autoHost) { return "Die Warteschlange ist leer.\n"; } ret = "Folgende Szenarien befinden sich in der Warteschlange:\n"; for(int i = 0; i < Config.Hosting.UserListLength; ++i) { const ScenarioSettings *scen; if(i < userlist.length()) { scen = &userlist.at(i); } else if(!autoHost) { break; } else { scen = &nextAutoScens.at(i - userlist.length()); } ret += "\t" + QString::number(i + 1) + ". " + scen->name + (scen->league ? " in der Liga" : "") + " (" + scen->wishClient.toString() + ")\n"; } return ret; } void CRSM::ircCheckModCmd(const QString &nick, CmdFunctionRef func, QString arg) { ircModFifos[nick].append(qMakePair(func, arg)); ircCheckUserStatus(ClientInfo::ircClient(nick), ClientInfo::ircClient(nick), &CRSM::ircModCmd); } QString CRSM::skipScen() { if(userlist.length() > 0) { QString skipped = userlist.first().name + " (" + userlist.first().wishClient.toString() + ")"; userlist.removeFirst(); return skipped; } else return ""; } bool CRSM::skipCurrent() { if(processManager->isRunning()) { processManager->closeProgFifos(); return true; } return false; } void CRSM::writeToServer(const QString &message) { if(!processManager->isWritable()) return; int i = 0; QStringList split = message.split('\n', QString::KeepEmptyParts); foreach(QString line, split) { if(!line.isEmpty()) { if(i < split.length() - 1) line += "\n"; while(line.length() > 240) { QString linePart = line.left(240); int pos = linePart.lastIndexOf(QRegExp("\\s")); bool wordSplit = false; if(pos <= 0) { pos = 240; wordSplit = true; } linePart = line.left(pos); line = line.mid(pos); if(wordSplit) { linePart += "-"; } linePart += "\n"; processManager->write(codec->fromUnicode(linePart)); if(Config.Readline.ServerUses) { if(writtenToServer.length() > Config.Readline.RereadLimit) writtenToServer.clear(); writtenToServer += codec->fromUnicode(linePart); } } processManager->write(codec->fromUnicode(line)); if(Config.Readline.ServerUses) { if(writtenToServer.length() > Config.Readline.RereadLimit) writtenToServer.clear(); writtenToServer += codec->fromUnicode(line); } } ++i; } } void CRSM::writeConfig() { Config.write(CONFIG_FILE_NAME); } QString CRSM::addAliasWish(const QString ¶m) { QRegExp aliasExp("^([^=]+)=(.*)$"); if(aliasExp.exactMatch(param)) { const QString &alias = aliasExp.cap(1).trimmed(); const QString &scen = aliasExp.cap(2).trimmed(); QString scenP = scen; if(Config.Hosting.Alias.contains(alias)) { return "Alias ist bereits vergeben!"; } else if(Config.Auto.Hosting.AliasWishes.contains(alias)) { return "Alias ist bereits als Wunsch vergeben!"; } else if(!(scenP = scenPath(scen)).isEmpty()) { Config.Auto.Hosting.AliasWishes.insert(alias, scenP); informModsAboutAliasWish(); return "Aliaswunsch ist hinterlegt!"; } else { return "Szenario \"" + scen + "\" wurde nicht gefunden!"; } } else { return "Eingabefehler! Siehe " + Config.CRSM.CommandSign + "help für mehr Informationen."; } } void CRSM::informModsAboutAliasWish() { foreach(const QString& mod, ircMods) { connection->sendCommand(IrcCommand::createNotice(mod, "Ein neuer Aliaswunsch ist verfügbar. Insgesamt verfügbar: " + QString::number(Config.Auto.Hosting.AliasWishes.size()))); } } void CRSM::editAliasWishes() { if(Config.Auto.Hosting.AliasWishes.isEmpty()) { connection->sendCommand(IrcCommand::createMessage(aliasWishEditor, "Keine Aliaswünsche " + (currentAliasWish == "" ? QString("") : QString("mehr ")) + "vorhanden.")); stopAliasWishEditing(); } else { currentAliasWish = Config.Auto.Hosting.AliasWishes.firstKey(); connection->sendCommand(IrcCommand::createMessage(aliasWishEditor, currentAliasWish + " = " + Config.Auto.Hosting.AliasWishes[currentAliasWish] + " [Ja|Nein|Stop]")); } } void CRSM::editAliasWishes(const QString &message) { if(message.toLower() == "j" || message.toLower() == "ja") { Config.Hosting.Alias[currentAliasWish] = Config.Auto.Hosting.AliasWishes[currentAliasWish]; Config.Auto.Hosting.AliasWishes.remove(currentAliasWish); } else if(message.toLower() == "n" || message.toLower() == "nein") { Config.Auto.Hosting.AliasWishes.remove(currentAliasWish); } else if(message.toLower() == "s" || message.toLower() == "stop") { connection->sendCommand(IrcCommand::createMessage(aliasWishEditor, "Aliaswunsch-Bearbeitung gestoppt.")); stopAliasWishEditing(); return; } else { connection->sendCommand(IrcCommand::createMessage(aliasWishEditor, "\"" + message + "\" ist keine Antwortmöglichkeit. Antwortmöglichkeiten: [Ja|Nein|Stop]")); } editAliasWishes(); } void CRSM::stopAliasWishEditing() { aliasWishEditor = ""; currentAliasWish = ""; } QString CRSM::ircActivateIngameChat(bool activated) { if(Config.IRC.UseIngameChat && !Config.IRC.IngameChannel.isEmpty()) { Session.IRC.UseIngameChat = activated; ircSetIngameChannelTopic(); return "Ingamechat wurde " + (Session.IRC.UseIngameChat ? QString("") : QString("de")) + "aktviert."; } else return "Ingamechat ist administrativ deaktiviert!"; } QStringList CRSM::listC4Folder(const QString &path) { QStringList ret; QFileInfo fileInfo(path); if(fileInfo.isDir()) { QDir dir(path); const QStringList folderList = dir.entryList(QStringList() << "*.c4f", QDir::NoFilter, QDir::Name | QDir::IgnoreCase); ret.append(dir.entryList(QStringList() << "*.c4s", QDir::NoFilter, QDir::Name | QDir::IgnoreCase)); foreach(const QString &folder, folderList) { const QStringList &folderList = listC4Folder(path + '/' + folder); foreach (const QString &scen, folderList) { ret.append(folder + '/' + scen); } } } else { QProcess c4group; c4group.start(Config.Auto.Volatile.Clonk.Directory + C4GROUP_EXECUTABLE, QStringList() << path << "-l", QProcess::ReadOnly); c4group.waitForFinished(); c4group.readLine(); QRegExp finishExp("^\\d+ Entries, \\d+ Bytes$"); QRegExp scenarioExp("^(.*\\.c4s)\\s+\\d+ Bytes\\s.*$"); QRegExp folderExp("^(.*\\.c4f)\\s+\\d+ Bytes\\s.*$"); QString line; while(!c4group.atEnd()) { line = codec->toUnicode(c4group.readLine().trimmed()); if(line.isEmpty()) continue; if(finishExp.exactMatch(line)) break; if(scenarioExp.exactMatch(line)) { ret.append(scenarioExp.cap(1)); } else if(folderExp.exactMatch(line)) { const QStringList &folderList = listC4Folder(path + '/' + folderExp.cap(1)); foreach (const QString &scen, folderList) { ret.append(folderExp.cap(1) + '/' + scen); } } } } return ret; } void CRSM::ircSetIngameChannelTopic() { if(Config.IRC.UseIngameChat && !Config.IRC.IngameChannel.isEmpty()) { if(Session.State == CRSMSession::None) { connection->sendCommand(IrcCommand::createTopic(Config.IRC.IngameChannel, "Kein laufendes Spiel.")); } else { QString statusText = ""; switch(Session.State) { case CRSMSession::Lobby: statusText = "Lobby"; break; case CRSMSession::Loading: statusText = "Lädt "; break; case CRSMSession::Running: statusText = "Läuft"; break; default: break; } connection->sendCommand(IrcCommand::createTopic(Config.IRC.IngameChannel, "Aktuelles Szenario: " + Session.Scenario.name + " | " + statusText + " | Ingamechat ist " + (Session.IRC.UseIngameChat ? "" : "de") + "aktviert.")); } } } void CRSM::addCommand(const QString &name, CmdFunction func, int interfaces, UserType userType, const QString &shortDescription, const QString &argList, const QString &longDescription) { cmds.insert(name.trimmed(), CmdFunctionRef(name.trimmed(), func, interfaces, userType, shortDescription, argList, longDescription)); } void CRSM::addCommandGroup(const QString &name, int interfaces, UserType userType, const QString &shortDescription, const QString &longDescription, CmdFunction defaultFunc) { if(!cmdGroups.contains(name)) cmdGroups.append(name); addCommand(name, defaultFunc, interfaces, userType, shortDescription, "", longDescription); addCommand(name + " help", &CRSM::grouphelp, interfaces, userType, "Gibt Hilfe zu den Unterbefehlen von " + name, "[Unterbefehlsname]"); } bool CRSM::cmdExists(const QString &name, ClientInterface interface) { return cmds.contains(name) && cmds.value(name).interfaces & interface; } CmdFunctionRef* CRSM::findCommand(const QString &cmd, ClientInterface interface) { return findCommand(cmd.split(QRegularExpression(R"(\s)"), QString::KeepEmptyParts), interface); } CmdFunctionRef* CRSM::findCommand(QStringList&& cmd, ClientInterface interface) { if(cmd.length() > 0) { const QString& cmdPart = cmd.join(' '); if(cmdExists(cmdPart, interface)) { return &cmds[cmdPart]; } else { cmd.removeLast(); return findCommand(std::move(cmd), interface); } } else { return nullptr; } } bool CRSM::cmd(const QString &cmd, const ClientInfo &client) { CmdFunctionRef* cmdPtr; if((cmdPtr = findCommand(cmd, client.interface)) != nullptr) { CmdFunctionRef cmdRef = *cmdPtr; QString args = cmd.mid(cmdRef.name.length()).trimmed(); switch(client.interface) { case Clonk: if(clientUserType(client) >= cmdRef.userType) { callCommand(cmdRef, args, client, clientUserType(client)); } else { rightsFailMessage(client, cmdRef.userType); Stats.AddCommandResult(client, cmdRef.name, RightsFail); } break; case IRC: if(cmdRef.userType == User || ((cmdRef.userType == Admin && (clientUserType(client) >= Admin)) || (cmdRef.userType == Moderator && clientUserType(client) >= Moderator))) { callCommand(cmdRef, args, client, clientUserType(client)); } else if(cmdRef.userType >= Admin) { ircCheckModCmd(client.nick, cmdRef, args); } break; case Management: callCommand(cmdRef, args, client, UserType::Max); break; case Auto: //just to avoid the compiler warning, there can't be a command from this interface break; } return true; } else { Stats.AddCommandResult(client, cmd, UnknownCommand); return false; } } void CRSM::rightsFailMessage(const ClientInfo &info, UserType minUserType) { respond(info, "Nur ein " + userTypeStrings.value(minUserType) + " kann diesen Befehl verwenden.\n", RespondType::PrivateNotice); } UserType CRSM::clientUserType(const ClientInfo &client) { switch(client.interface) { case Clonk: if(Config.Clonk.Chat.Moderators.contains(client.CUID)) return Moderator; if(Session.Clonk.Admin == client) return Admin; break; case IRC: if(ircMods.contains(client.nick)) return Moderator; else if(Session.IRC.Admin == client) return Admin; break; case Management: return Max; case Auto: //just to avoid the compiler warning break; } return User; } void CRSM::setupCmds() { cmds.clear(); cmdGroups.clear(); addCommand("admin", &CRSM::admin, Clonk | IRC, User, "Ohne Name trägt es den Autor der Nachricht als Rundenadmin ein, bzw. mit Name den Spieler mit entsprechendem Namen, insofern nicht bereits ein Rundenadmin feststeht.", "[Chatnick¦PC-Name]"); addCommand("admin?", &CRSM::getAdmin, Clonk | IRC | Management, User, "Fragt den aktuellen Rundenadmin ab."); addCommand("afkadmin", &CRSM::afkAdmin, Clonk | IRC | Management, User, "Gibt den Rundenadmin frei, wenn er nicht in den nächsten " + QString::number(Config.Hosting.AfkAdminTime) + "s reagiert."); addCommand("ingameadmin", &CRSM::ingameadmin, IRC | Management, Admin, "Legt den Ingame-Rundenadmin fest.", ""); addCommand("noadmin", &CRSM::noadmin, Clonk | IRC | Management, Admin, "Entzieht dem (IRC-)Rundenadmin seine Rechte, damit jemand anders Rundenadmin sein kann."); addCommand("aliaswish", &CRSM::aliaswish, Clonk | IRC, User, "Deponiert den Wunsch, als Alias für einzutragen. Ein Moderator entscheidet darüber.", " = "); addCommand("queue", &CRSM::queue, Clonk | IRC | Management, User, "Zeigt die nächsten " + QString::number(Config.Hosting.UserListLength) + " Szenarien auf der Warteliste."); addCommand("host", &CRSM::host, Clonk | IRC | Management, User, "Nimmt das angegebene Szenario in die Warteschlange auf. Optional in der Liga, wenn \"--league\" angegeben wird.", "[--league] "); addCommand("list", &CRSM::list, Clonk | IRC | Management, User, "Listet alle definierten Aliase oder alle möglichen Szenarien und Ordner auf, bzw. alle Szenarien im Ordner oder Rundenordner.", "[Aliase¦Rundenordner[.c4f]]"); addCommand("clientlist", &CRSM::clientlist, IRC | Management, User, "Listet alle verbundenen Clients mit PC-Name und Chatnick auf."); addCommand("help", &CRSM::help, Clonk | IRC | Management, User, "Zeigt die Hilfe an.", "[long¦Befehlsname]", "Listet alle verfügbaren Befehle auf. Mit long werden alle verfügbaren Befehle mit Kurzbeschreibung aufgelistet."); addCommand("stop", &CRSM::passToClonk, Clonk | IRC | Management, Admin, "Stoppt einen laufenden Countdown."); addCommand("start", &CRSM::passToClonkNumericOrEmpty, Clonk | IRC | Management, Admin, "Startet den Countdown.", "[Countdownzeit in s]"); addCommand("teamdist", &CRSM::passToClonk, Clonk | IRC | Management, Admin, "Ändert die Teamverteilung.", ""); addCommand("plrteam", &CRSM::passToClonk, Clonk | IRC, Admin, "Ändert das Team eines Spielers.", " ", "Verschiebt in das ."); addCommand("pause", &CRSM::passToClonk, Clonk | IRC | Management, Admin, "Pausiert das Spiel."); addCommand("unpause", &CRSM::passToClonk, Clonk | IRC | Management, Admin, "Setzt das pausierte Spiel fort."); addCommand("observer", &CRSM::passToClonkPcName, Clonk | IRC | Management, Admin, "Der angegebene Host muss zuschauen.", ""); addCommand("deactivate", &CRSM::passToClonkPcName, Clonk | IRC | Management, Admin, "Deaktiviert den angegebenen Host.", ""); addCommand("activate", &CRSM::passToClonkPcName, Clonk | IRC | Management, Admin, "Aktiviert den angegebenen Host.", ""); addCommand("kick", &CRSM::passToClonkPcName, Clonk | IRC | Management, Admin, "Kickt den angegebenen Host.", ""); addCommand("script", &CRSM::passToClonk, Clonk | IRC | Management, Admin, "Führt das angegebene Script aus.", "