#include "crsm.hpp" #include #include #include #include #include #include #include #include #include #define MGMT_BUFFER_FILENAME "CRSM-MGMT-Buffer" CRSM::CRSM(QObject *parent) : QObject(parent), Session(this) { qsrand((uint)QDateTime::currentMSecsSinceEpoch()); codec = QTextCodec::codecForName("Windows-1252"); 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); connect(&dccServer, SIGNAL(newConnection()), this, SLOT(newDCCConnection())); 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.\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())); connect(&gameRegisterFailTimer, SIGNAL(timeout()), this, SLOT(enableAutoHosting())); gameRegisterFailTimer.setInterval(5*60*1000); 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; if(Config.IRC.Use) { foreach(const QString &mess, what.split("\n", QString::SkipEmptyParts)) foreach(const QString &mod, ircModIOList) { sendIrcMessage(mess, mod, false, true, true); } } out(what.trimmed() + "\n"); if(!timeRemover.exactMatch(what)) return; what = timeRemover.cap(2).trimmed(); Log.clonkLog(what); static QRegExp userexp("^\\s*(\\*?)\\s*<(.*)\\|(\\d+)\\|([^>]*)>\\s+(.*)$"); if(userexp.exactMatch(what)) { Log.clonkChatLog(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); Log.clonkUserLog(what, info, isMeMessage); 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) { QString command = getCommand(msg); if(!command.isEmpty()) { if(!cmd(command, info)) { respond(info, "Unbekannter Befehl: \"" + command + "\"!\n"); } } else if(Session.IRC.UseIngameChat) { sendIrcMessage("[Clonk]<" + user + "> " + msg, Config.IRC.IngameChannel, false, false); } } else if(Session.IRC.UseIngameChat) { sendIrcMessage("[Clonk] " + user + " " + msg, Config.IRC.IngameChannel, true, false); } } } 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) { sendIrcMessage("[Clonk] " + info.toString() + " verbunden.", Config.IRC.IngameChannel, false, false); } } } 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::Lobby) { 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) { sendIrcMessage("[Clonk] " + info.toString() + " entfernt" + leaveExp.cap(2), Config.IRC.IngameChannel, false, false); } if(info == Session.Clonk.Admin) { Session.Clonk.LeaveAdmins.insert(info, QDateTime::currentDateTime()); afkAdminTimer.stop(); Session.AfkAdmin = false; 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) || (Session.State == CRSMSession::Running || Session.State == CRSMSession::Loading))) { 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.start(); } 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) { sendIrcMessage(gameRegisterFailMessage.arg(reason), Config.IRC.Channel, false, false); if(Config.IRC.UseIngameChat) { sendIrcMessage(gameRegisterFailMessage.arg(reason), Config.IRC.IngameChannel, false, false); } } } } 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 != nullptr) { 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->type() == IrcMessage::Notice) { IrcNoticeMessage* noticeMessage = (IrcNoticeMessage*)message; if(message->nick() == "NickServ") { QStringList split = message->parameters().at(1).split(' ', QString::SkipEmptyParts); if(noticeMessage->content().contains("nickname is registered")) { sendIrcMessage("IDENTIFY " + Config.IRC.Nick + " " + Config.IRC.Password, "NickServ", false, false); } else if(split.size() >= 2 && 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(); } } } } Log.ircLog(noticeMessage->content(), noticeMessage->nick(), noticeMessage->isPrivate(), noticeMessage->target(), false, true); if(!message->isOwn()) { Log.ircUserLog(noticeMessage->content(), ClientInfo::ircClient(noticeMessage->nick(), noticeMessage->target()), noticeMessage->isPrivate(), noticeMessage->target(), false, true); } } else if(message->type() == IrcMessage::Private) { IrcPrivateMessage* privMessage = (IrcPrivateMessage*)message; if(!privMessage->isRequest()) { QString target = privMessage->target(); if(target == connection->nickName()) target = message->nick(); const ClientInfo& client = ClientInfo::ircClient(message->nick(), target); handleIrcMessage(client, privMessage->content(), privMessage->target(), privMessage->isPrivate(), privMessage->isAction(), message->isOwn()); } else { if(privMessage->isPrivate() && Config.DCC.Use) { if(privMessage->content().startsWith("DCC CHAT CHAT ", Qt::CaseInsensitive)) { const QStringList& parts = privMessage->content().split(' ', QString::SkipEmptyParts); if(parts.size() == 5) { dccChatRequest(ClientInfo::ircClient(message->nick())); } else if(parts.size() == 6 && parts.at(3) == "199" && parts.at(4) == "0") { dccChatRequest(ClientInfo::ircClient(message->nick()), parts.at(5)); } } } } } else if(message->isOwn()) { return; } else if(message->type() == IrcMessage::Join) { IrcJoinMessage* joinMessage = (IrcJoinMessage*)message; QString joinChannel = joinMessage->channel(); if(greetAllowed(ClientInfo::ircClient(joinMessage->nick(), joinMessage->channel()))) { sendIrcMessage("Hallo, " + message->nick() + "!", joinChannel, false, false); } 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() == connection->nickName()) { ircSetIngameChannelTopic(); } } } void CRSM::ircConnected() { IrcConnection* conn = (IrcConnection*)sender(); Q_ASSERT(conn == connection); connection->sendCommand(IrcCommand::createMode(connection->nickName(), "+B")); connection->sendCommand(IrcCommand::createJoin(Config.IRC.Channel)); if(Config.IRC.UseIngameChat) { 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); if(!greetAllowed(info)) { return; } writeToServer(QString("Hallo, " + info.nick + "!\n")); if(Session.Clonk.LeaveAdmins.contains(info)) { qint64 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 { const QString& msg = ClientInfo::managementClient(conn).toString(true) + " " + data + "\n"; writeToServer(msg); if(Session.IRC.UseIngameChat) { sendIrcMessage(msg, Config.IRC.IngameChannel, false, false); } } } } } 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::dccChatRequest(const ClientInfo &client, QString extraArgs) { if(dccNickConnections.contains(client.nick)) { QTimer::singleShot(0, dccNickConnections[client.nick].socket, &QTcpSocket::close); } QHostInfo info = QHostInfo::fromName(Config.DCC.Address); connection->sendCommand(IrcCommand::createCtcpRequest(client.nick, "DCC CHAT chat " + QString::number(info.addresses().first().toIPv4Address()) + " " + QString::number(Config.DCC.ListenPort) + (!extraArgs.isEmpty() ? " " + extraArgs: extraArgs))); } // modified version of http://stackoverflow.com/a/18866593 QString GetRandomString(int length = 5) { static const QString possibleCharacters("ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"); QString randomString; for(int i=0; ibytesAvailable() > 0) { QString message = QString::fromUtf8(socket->readLine()).trimmed(); if(message.length() > 0) { handleIrcMessage(dccConnection.client, message, connection->nickName(), true, false); } } } void CRSM::newDCCConnection() { QTcpSocket* socket = dccServer.nextPendingConnection(); QString identifier; do { identifier = GetRandomString(); } while(dccConnectionIdentifiers.contains(identifier)); dccConnectionIdentifiers.insert(identifier, socket); connect(socket, SIGNAL(disconnected()), this, SLOT(disconnectedDCCConnection())); connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(disconnectedDCCConnection())); socket->write(QString("Weise diese Verbindung deinem IRC-Nick zu, indem du diesen Befehl eingibst: /msg " + connection->nickName() + " dcc identify " + identifier + "\n").toUtf8()); } void CRSM::disconnectedDCCConnection() { QTcpSocket* socket = (QTcpSocket*)sender(); if(dccSocketConnections.contains(socket)) { dccNickConnections.remove(dccSocketConnections[socket].client.nick); dccSocketConnections.remove(socket); } dccConnectionIdentifiers.remove(dccConnectionIdentifiers.key(socket)); socket->deleteLater(); } void CRSM::updateNextAutoScens() { if(autolist.length() <= 0) return; while(nextAutoScens.length() < Config.Hosting.UserListLength || (nextAutoScens.length() <= 0)) { 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 && !Config.Hosting.DisableLeague) { argList << "/league"; Session.League = true; } else { argList << "/noleague"; } argList << Packs.getScenarioCmdOptions(filename).split(' '); argList << filename; processManager->setWorkingDirectory(Config.Auto.Volatile.Clonk.Directory); out(Packs.linkScenarioPacks(filename)); processManager->start(Config.Clonk.Server.Executable, argList); Log.scenLog(scen); } 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().fileName())) { 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("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::caseInsensitive(QString name, QString dir, QString trySuffix) { QString suffixedName; if(!trySuffix.isEmpty()) { suffixedName = name + trySuffix; } const QStringList& entryList = QDir(Config.Auto.Volatile.Clonk.Directory + dir + QDir::separator()).entryList(); foreach(const QString& entry, entryList) { if(entry.compare(name, Qt::CaseInsensitive) == 0 || entry.compare(suffixedName, Qt::CaseInsensitive) == 0) { return entry; } } return QString(); } 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; } if(!isAlias && !scenName.endsWith(".c4s", Qt::CaseInsensitive)) { scenName.append(".c4s"); } 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) { QString folderName = caseInsensitive(split.first(), "", ".c4f"); if(!folderName.isEmpty()) { // QDir dir(Config.Auto.Volatile.Clonk.Directory + folderName); // if(dir.exists()) // { // QString file = caseInsensitive(name, folderName); // if(!file.isEmpty()) // { // if(isAlias) // { // return aliasName; // } // else // { // return folderName + '/' + file; // } // } // else // { // return QString(); // } // } 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().completeBaseName())) 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 { QString folder = caseInsensitive(commandArgs, "", ".c4f"); QFile file(Config.CRSM.ListFolder + folder + ".lst"); if(!folder.isEmpty() && file.exists() && !Config.Clonk.Server.IgnoreFolders.contains(QFileInfo(file).completeBaseName())) { ret += "Der Ordner \"" + folder + 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 { if(i - userlist.length() < nextAutoScens.length()) { scen = &nextAutoScens.at(i - userlist.length()); } else { break; } } 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(int pos) { if(userlist.length() > pos) { QString skipped = userlist.at(pos).name + " (" + userlist.at(pos).wishClient.toString() + ")"; userlist.removeAt(pos); 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.CommandSigns.first() + "help für mehr Informationen."; } } void CRSM::informModsAboutAliasWish() { foreach(const QString& mod, ircMods) { sendIrcMessage("Ein neuer Aliaswunsch ist verfügbar. Insgesamt verfügbar: " + QString::number(Config.Auto.Hosting.AliasWishes.size()), mod, false, true, true); } } void CRSM::editAliasWishes() { if(Config.Auto.Hosting.AliasWishes.isEmpty()) { sendIrcMessage("Keine Aliaswünsche " + (currentAliasWish == "" ? QString("") : QString("mehr ")) + "vorhanden.", aliasWishEditor, false, false); stopAliasWishEditing(); } else { currentAliasWish = Config.Auto.Hosting.AliasWishes.firstKey(); sendIrcMessage(currentAliasWish + " = " + Config.Auto.Hosting.AliasWishes[currentAliasWish] + " [Ja|Nein|Stop]", aliasWishEditor, false, false); } } 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") { sendIrcMessage("Aliaswunsch-Bearbeitung gestoppt.", aliasWishEditor, false, false); stopAliasWishEditing(); return; } else { sendIrcMessage("\"" + message + "\" ist keine Antwortmöglichkeit. Antwortmöglichkeiten: [Ja|Nein|Stop]", aliasWishEditor, false, false); } editAliasWishes(); } void CRSM::stopAliasWishEditing() { aliasWishEditor = ""; currentAliasWish = ""; } QString CRSM::ircActivateIngameChat(bool activated) { if(Config.IRC.UseIngameChat) { Session.IRC.UseIngameChat = activated; ircSetIngameChannelTopic(); return "Ingamechat wurde " + (Session.IRC.UseIngameChat ? QString("") : QString("de")) + "aktiviert."; } 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) { if(Session.State == CRSMSession::None) { connection->sendCommand(IrcCommand::createTopic(Config.IRC.IngameChannel, Config.IRC.IngameChannelExtraTopic + "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; case CRSMSession::None: // is handled above break; } connection->sendCommand(IrcCommand::createTopic(Config.IRC.IngameChannel, Config.IRC.IngameChannelExtraTopic + "Aktuelles Szenario: " + Session.Scenario.name + " | " + statusText + " | Ingamechat ist " + (Session.IRC.UseIngameChat ? "" : "de") + "aktiviert.")); } } } 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, QString &args) { return findCommand(cmd.split(QRegularExpression(R"(\s)"), QString::KeepEmptyParts), interface, args); } CmdFunctionRef* CRSM::findCommand(QStringList&& cmd, ClientInterface interface, QString &realCmd) { if(cmd.length() > 0) { QString cmdPart = cmd.join(' '); realCmd = cmdPart; substituteCommandAlias(cmdPart); if(cmdExists(cmdPart, interface)) { return &cmds[cmdPart]; } else { removeCommandSuffixes(cmdPart); substituteCommandAlias(cmdPart); if(cmdExists(cmdPart, interface)) { return &cmds[cmdPart]; } else { cmd.removeLast(); return findCommand(std::move(cmd), interface, realCmd); } } } else { return nullptr; } } bool CRSM::cmd(const QString& cmd, const ClientInfo &client) { Log.commandLog(cmd, client, false); CmdFunctionRef* cmdPtr; QString realCmd = cmd; if((cmdPtr = findCommand(cmd, client.interface, realCmd)) != nullptr) { CmdFunctionRef cmdRef = *cmdPtr; QString args = cmd.mid(realCmd.length()).trimmed(); removeCommandSuffixes(args); 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, true); } 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(); addCommandGroup("admin", Clonk/* | IRC disabled because of abuse */, 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.", "", &CRSM::admin); addCommand("admin set", &CRSM::admin, Clonk /* | IRC disabled because of abuse */, 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 get", &CRSM::getAdmin, Clonk | IRC | Management, User, "Fragt den aktuellen Rundenadmin ab."); addCommand("admin afk", &CRSM::afkAdmin, Clonk /* | IRC disabled because of abuse */ | Management, User, "Gibt den Rundenadmin frei, wenn er nicht in den nächsten " + QString::number(Config.Hosting.AfkAdminTime) + "s reagiert."); addCommand("admin ingame", &CRSM::ingameadmin, IRC | Management, Admin, "Legt den Ingame-Rundenadmin fest.", ""); addCommand("admin clear", &CRSM::noadmin, Clonk | IRC | Management, Admin, "Entzieht dem (IRC-)Rundenadmin seine Rechte, damit jemand anders Rundenadmin sein kann."); 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] <[Rundenordner[.c4f]/]Szenarioname¦Alias>"); 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.", "