#include "crsm.hpp" #include #include #include #include #include #include #include #include #include #include "CRSMUtil.hpp" #define MGMT_BUFFER_FILENAME "CRSM-MGMT-Buffer" CRSM::CRSM(QObject *parent) : QObject(parent), Session(this), parser(Session), ingameChat(Config.IRC.IngameChannel, *this) { control.setController(this); parser.addTarget(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, false, codec); } else { processManager = new ProcessManager("CRSM-Clonkserver-", "", false, codec); } 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); control.serverMessage("Der Server Manager läuft wieder."); if(Session.State == CRSMSession::Running) { watchdog(); } unRegisterIngameChat(); } QFile sessionFile(Config.CRSM.SessionFile); if(sessionFile.exists()) sessionFile.remove(); 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); connect(&watchDogTimer, SIGNAL(timeout()), this, SLOT(watchdog())); ok = true; } CRSM::~CRSM() { } void CRSM::start() { if(autoHost && !processManager->isRunning()) nextScen(); } bool CRSM::isOk() { return ok; } QString CRSM::findClientByName(ClientInfo &info, const QString &name) { if(Session.Clonk.Clients.contains(name)) { info = Session.Clonk.Clients.value(name); return ""; } else { for(const QString& pcName : Session.Clonk.Clients.keys()) { if(pcName.compare(name, Qt::CaseInsensitive) == 0) { info = Session.Clonk.Clients.value(pcName); return ""; } } } bool notFound = true; for(const ClientInfo& client : Session.Clonk.Clients) { bool foundHere = false; if(client.nick.compare(name, Qt::CaseInsensitive) == 0) { foundHere = true; } else { for(const QString& player : client.players) { if(name.compare(player, Qt::CaseInsensitive) == 0) { foundHere = true; } } } if(foundHere) { if(!notFound && info != client) { return "Name \"" + name + "\" ist nicht eindeutig. Bitte PC-Namen verwenden.\n"; } info = client; notFound = false; } } if(notFound) { if(name.isEmpty()) { return "Es wird ein PC-Name, Chat-Nick oder Spielername benötigt."; } else { return "\"" + name + "\" wurde nicht gefunden!\n"; } } else { return ""; } } bool CRSM::lobbyCountdown(unsigned int seconds) { Session.CountDown = (int)seconds; return false; } bool CRSM::lobbyCountdownAborted() { Session.CountDown = -1; return false; } bool CRSM::watchdog(const QString& id) { if(id == watchDogString) { watchDogString.clear(); watchDogTimer.start(Config.Clonk.Server.Watchdog.Interval * 1000); } return false; } QString CRSM::cmdErrorText(const QString& command, const ClientInfo& client, bool& ret) { QString retText; QStringList corrections; if(!(ret = cmd(command, client, corrections))) { retText = "Unbekannter Befehl: \"" + command + "\"!\n"; if(!corrections.isEmpty()) { retText += "Meintest du: " + corrections.join(", ") + "?\n"; } } return retText; } bool CRSM::clientMessage(ClientInfo& client, const QString& message, ClonkOutputInterface::MessageType type, const QTime& time) { bool isMeMessage = (type == Action); if(type == Sound) { Log.clonkChatLog("Sound: " + client.toString(true, true) + " " + message); } else if(isMeMessage) { Log.clonkChatLog("* " + client.toString(true, true) + " " + message); } else { Log.clonkChatLog(client.toString(true, true) + " " + message); } if(client.pcName != Config.Auto.Volatile.Clonk.ServerPCName) { Log.clonkUserLog(message, client, isMeMessage); if(client == Session.Clonk.Admin) { checkActivity(Session.Clonk.Admin); } if(client.floodCheck(Config.Clonk.Chat.AntiFlood.Count, Config.Clonk.Chat.AntiFlood.Time, QDateTime(QDate::currentDate(), time))) { kick(client, "Flooding! Maximal " + QString::number(Config.Clonk.Chat.AntiFlood.Count) + " Nachrichten in " + QString::number(Config.Clonk.Chat.AntiFlood.Time) + "s"); } else if(type == Sound) { return false; } else if(!isMeMessage) { QString command = getCommand(message); if(!command.isEmpty()) { bool ret; const QString& txt = cmdErrorText(command, client, ret); if(ret) { return true; } else { respond(client, txt); } } } } return false; } bool CRSM::clientConnected(const ClientInfo& client) { QTimer *timer = new QTimer; connect(timer, &QTimer::timeout, [this, pcName = client.pcName]{ greet(pcName); }); connect(timer, SIGNAL(timeout()), timer, SLOT(deleteLater())); timer->start(1000); if(Session.CountDown != -1 && Session.CountDown <= Config.Clonk.Server.JoinStopCountDown) { control.abortCountdown(); } return false; } bool CRSM::clientRemoved(const ClientInfo& client, const QString& reason) { Q_UNUSED(reason); control.serverMessage(client.nick + " ist ein L34V0R!"); if(client == Session.Clonk.Admin) { Session.Clonk.LeaveAdmins.insert(client, QDateTime::currentDateTime()); afkAdminTimer.stop(); Session.AfkAdmin = false; control.serverMessage("Rundenadmin wurde freigegeben."); Session.Clonk.Admin.clear(); } Session.Clonk.Clients.remove(client.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)) { control.setCountdown(Config.Clonk.Server.EmptyTimer); } return false; } bool CRSM::gameLoading() { setSessionState(CRSMSession::Loading); return false; } bool CRSM::gameStarted() { Stats.AddScenarioStart(Session.Scenario.wishClient, scenarioFileName(Session.Scenario.name)); if(!Session.League) { control.setCommand("maxplayer 0"); } setSessionState(CRSMSession::Running); watchdog(); return false; } bool CRSM::masterserverError(const QString& msg) { 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"; announceInfo(gameRegisterFailMessage.arg(msg)); return false; } bool CRSM::raw(const QString &line) { if(Config.IRC.Use) { foreach(const QString &mess, line.split("\n", Qt::SkipEmptyParts)) foreach(const QString &mod, ircModIOList) { sendIrcMessage(mess, mod, false, true, true); } } out(line + "\n"); return false; } bool CRSM::rawTimed(const QString &line, const QTime &time) { Q_UNUSED(time); Log.clonkLog(line); return false; } bool CRSM::playerRemoved(const QString& name) { Q_UNUSED(name); return false; } void CRSM::readServerOutput() { QString what(processManager->readLine().trimmed()); 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); } what = what.trimmed(); } if(what.isEmpty()) { return; } parser.parseMessage(what); } 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(); watchDogString.clear(); watchDogTimer.stop(); 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(' ', Qt::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(' ', Qt::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(); auto setting = greetingSetting(ClientInfo::ircClient(joinMessage->nick(), joinMessage->channel())); if(setting != GreetingSetting::Disabled) { sendIrcMessage("Hallo, " + message->nick() + "!", joinChannel, false, setting == GreetingSetting::Notice); } if(joinChannel == Config.IRC.IngameChannel && Session.IRC.UseIngameChat) { control.serverMessage("[IRC] " + message->nick() + " hat den Channel betreten."); } 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) { control.serverMessage("[IRC] " + message->nick() + " hat den Channel verlassen."); } } 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(greetingSetting(info) == GreetingSetting::Disabled) { return; } control.serverMessage("Hallo, " + info.nick + "!"); if(Session.Clonk.LeaveAdmins.contains(info) && Session.Clonk.Admin != info) { qint64 timeGone; if((timeGone = Session.Clonk.LeaveAdmins.value(info).secsTo(QDateTime::currentDateTime())) < Config.Clonk.Chat.RegainAdminTime && (!Session.Clonk.Admin.empty() || !Session.IRC.Admin.empty())) { control.serverMessage(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."); } 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; if(conn.name.length() > 1 && conn.name.at(0) == '!') { conn.noLogging = true; } out(conn.name + " logged in on Management-Interface.\n"); managementConnections.insert(sock, conn); if(!conn.noLogging) { replayOutputBuffer(sock); } } } else { if(data.isEmpty()) { continue; } ManagementConnection& conn = managementConnections[sock]; if(data.at(0) == '/') { QString command = data.mid(1).trimmed(); QStringList corrections; if(cmd(command, ClientInfo::managementClient(conn), corrections)) { continue; } else { control.rawCommand(command); } } else { const QString& msg = ClientInfo::managementClient(conn).toString(true) + " " + data + "\n"; control.serverMessage(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; unRegisterIngameChat(); Session.Clonk.Server.pcName = Config.Auto.Volatile.Clonk.ServerPCName; Session.Clonk.Server.nick = Config.Auto.Volatile.Clonk.ServerNick; setSessionState(CRSMSession::Lobby); 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!\n"); scenfile.open(QFile::WriteOnly); scenfile.write("Worlds.c4f/Goldmine.c4s"); scenfile.close(); } scenfile.open(QFile::ReadOnly); QStringList scenlist = QString(scenfile.readAll()).trimmed().split("\n",Qt::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(' ', Qt::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()) { 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', Qt::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.isEmpty() ? 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) { if(Session.IRC.UseIngameChat != activated) { Session.IRC.UseIngameChat = activated; unRegisterIngameChat(); ircSetIngameChannelTopic(); return "Ingamechat wurde " + (Session.IRC.UseIngameChat ? QString("") : QString("de")) + "aktiviert."; } else { return "Ingamechat ist bereits " + (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::setSessionState(CRSMSession::SessionState state) { Session.State = state; ircSetIngameChannelTopic(); } void CRSM::addCommand(const QString &name, CmdFunction func, int interfaces, UserType userType, const QString &shortDescription, const QString &argList, const QString &longDescription) { const QString& nName = name.trimmed().toLower(); cmds.insert(nName, CmdFunctionRef(nName, func, interfaces, userType, shortDescription, argList, longDescription)); } void CRSM::addCommandGroup(const QString &name, int interfaces, UserType userType, const QString &shortDescription, const QString &longDescription, CmdFunction defaultFunc) { const QString& nName = name.trimmed().toLower(); if(!cmdGroups.contains(nName)) cmdGroups.append(nName); 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; } QStringList CRSM::guessCmd(const QString& name, ClientInterface interface) { int minDistance = -1; QString minDistanceCmd; QStringList minDistanceCmds; auto applyDistance = [&name, &minDistance, &minDistanceCmd, &minDistanceCmds](const QString& cmdName) { int distance = Util::damerauLevenshteinDistance(name, cmdName); if(minDistance == -1 || distance < minDistance) { minDistance = distance; minDistanceCmd = cmdName; minDistanceCmds.clear(); } else if(minDistance == distance && !minDistanceCmds.contains(cmdName)) { minDistanceCmds.append(cmdName); } }; for(const auto& cmd : cmds) { if(cmd.interfaces & interface) { applyDistance(cmd.name); } } for(const auto& alias : Config.CRSM.CommandAlias.keys()) { applyDistance(alias); } if(!minDistanceCmds.contains(minDistanceCmd)) { minDistanceCmds.append(minDistanceCmd); } if(minDistanceCmds.length() == 1 && minDistance >= std::min(3, name.length() - 1)) { minDistanceCmds.clear(); } return minDistanceCmds; } CmdFunctionRef* CRSM::findCommand(const QString &cmd, ClientInterface interface, QString &args, QStringList& corrections) { return findCommand(cmd.split(QRegularExpression(R"(\s)"), Qt::KeepEmptyParts), interface, args, corrections); } CmdFunctionRef* CRSM::findCommand(QStringList&& cmd, ClientInterface interface, QString &realCmd, QStringList& corrections) { if(cmd.length() > 0) { QString cmdPart = cmd.join(' ').toLower(); realCmd = cmdPart; substituteCommandAlias(cmdPart); if(cmdExists(cmdPart, interface)) { return &cmds[cmdPart]; } else { removeCommandSuffixes(cmdPart); substituteCommandAlias(cmdPart); if(cmdExists(cmdPart, interface)) { return &cmds[cmdPart]; } else { const QStringList& guesses = guessCmd(cmdPart, interface); cmd.removeLast(); if(const auto result = findCommand(std::move(cmd), interface, realCmd, corrections); result) { return result; } if(guesses.length() == 1) { QString guess = guesses.first(); substituteCommandAlias(guess); if(cmdExists(guess, interface)) { return &cmds[guess]; } } else if(guesses.length() > 1) { corrections.append(guesses); } return nullptr; } } } else { return nullptr; } } bool CRSM::cmd(const QString& cmd, const ClientInfo &client, QStringList& corrections) { Log.commandLog(cmd, client, false); CmdFunctionRef* cmdPtr; QString realCmd = cmd; if((cmdPtr = findCommand(cmd, client.interface, realCmd, corrections)) != 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(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."); addCommandGroup("client", Clonk | IRC | Management, User, "Verwaltet die verbundenen Clients."); addCommand("client list", &CRSM::clientlist, IRC | Management, User, "Listet alle verbundenen Clients auf."); // TODO: optional player list addCommand("client kick", &CRSM::passToClonkPcNameGrouped, Clonk | IRC | Management, Admin, "Kickt den angegebenen Client.", ""); addCommand("client observer", &CRSM::passToClonkPcNameGrouped, Clonk | IRC | Management, Admin, "Der angegebene Client muss zuschauen.", ""); addCommand("client deactivate", &CRSM::passToClonkPcNameGrouped, Clonk | IRC | Management, Admin, "Deaktiviert den angegebenen Client.", ""); addCommand("client activate", &CRSM::passToClonkPcNameGrouped, Clonk | IRC | Management, Admin, "Aktiviert den angegebenen Client.", ""); addCommandGroup("queue", Clonk | IRC | Management, User, "Zeigt die nächsten " + QString::number(Config.Hosting.UserListLength) + " Szenarien auf der Warteliste.", "", &CRSM::queue); addCommand("queue list", &CRSM::queue, Clonk | IRC | Management, User, "Zeigt die nächsten " + QString::number(Config.Hosting.UserListLength) + " Szenarien auf der Warteliste."); addCommand("queue skip", &CRSM::skip, Clonk | IRC | Management, User, "Entfernt das nächste (eigene) Szenario (oder das auf [Warteschlangenposition]) aus der Wunschliste.", "[Warteschlangenposition]"); addCommand("queue clear", &CRSM::clear, Clonk | IRC | Management, Moderator, "Löscht die Wunschliste."); 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("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("abort", &CRSM::passToClonk, Clonk | IRC | Management, Admin, "Bricht einen laufenden Countdown ab."); addCommand("start", &CRSM::passToClonkNumericOrEmpty, Clonk | IRC | Management, Admin, "Startet den Countdown.", "[Countdownzeit in s]"); 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("script", &CRSM::passToClonk, Clonk | IRC | Management, Admin, "Führt das angegebene Script aus.", "