File indexing completed on 2024-04-21 15:08:22

0001 /***************************************************************************
0002                           cmsp.cpp  -  MUD Sound Protocol
0003                              -------------------
0004     begin                : Ne mar 16 2003
0005     copyright            : (C) 2003 by Tomas Mecir
0006     email                : kmuddy@kmuddy.com
0007  ***************************************************************************/
0008 
0009 /***************************************************************************
0010  *                                                                         *
0011  *   This program is free software; you can redistribute it and/or modify  *
0012  *   it under the terms of the GNU General Public License as published by  *
0013  *   the Free Software Foundation; either version 2 of the License, or     *
0014  *   (at your option) any later version.                                   *
0015  *                                                                         *
0016  ***************************************************************************/
0017 
0018 #include "cmsp.h"
0019 
0020 #include "cprofilesettings.h"
0021 #include "cdownloader.h"
0022 #include "csoundplayer.h"
0023 
0024 #include <stdlib.h>
0025 
0026 #include <QDir>
0027 #include <QStandardPaths>
0028 
0029 #include <KLocalizedString>
0030 
0031 using namespace std;
0032 
0033 cMSP::cMSP (int sess) : cActionBase ("msp", sess)
0034 {
0035   //random number generator is initialized in KMuddy::KMuddy
0036   downloader = new cDownloader (this);
0037 
0038   // retrieve soundPlayer and musicPlayer
0039   soundPlayer = dynamic_cast<cSoundPlayer *>(object ("soundplayer", 0));
0040   midiPlayer = dynamic_cast<cSoundPlayer *>(object ("musicplayer", 0));
0041   
0042   mspenabled = false;
0043   mspallowed = true;
0044   dloadallowed = false;
0045   //reset() must NOT be called here - it will be called in cTelnet::connectIt
0046 }
0047 
0048 cMSP::~cMSP ()
0049 {
0050   delete downloader;
0051 }
0052 
0053 void cMSP::reset (const QString &serverName)
0054 {
0055   mspenabled = false;
0056   mspallowed = true;
0057   dloadallowed = false;
0058   state = 1;
0059   cachedString = "";
0060   triggerContents = "";
0061   defaultURL = QString();
0062   localdir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/sounds/" + serverName;
0063   downloader->reset ();
0064 }
0065 
0066 void cMSP::enableMSP ()
0067 {
0068   mspenabled = true;
0069 }
0070 
0071 void cMSP::disableMSP ()
0072 {
0073   mspenabled = false;
0074   state = 1;
0075   cachedString = "";
0076   triggerContents = "";
0077 }
0078 
0079 string cMSP::parseServerOutput (const string &output)
0080 {
0081   /*
0082   Explanation of parser states:
0083   0 = no MSP tags allowed here; any that come shall not be caught
0084   1 = MSP tag could begin now; machine enters this state after \n has been
0085       received
0086   2 = got first !
0087   3 = got second !
0088   4-8 = got (state-3)th letter of word SOUND
0089   9-13 = got (state-8)th letter of word MUSIC
0090   14 = got opening parenthesis, now reading parameters; if we get \n while
0091       in this state, we discard that sound trigger (unfinished); we jump
0092       back to 0 or 1 when the trigger is complete (depending on mid-line
0093       setting)
0094   */
0095 
0096   //support mid-line triggers if necessary
0097   bool allowmidline = false;
0098   cProfileSettings *sett = settings();
0099   if (sett) allowmidline = sett->getBool ("midline-msp");
0100   if (allowmidline && (state == 0))
0101     state = 1;  //no zero-state when mid-line support is ON
0102   //state to enter after the MSP trigger; this is to allow multiple sound
0103   //triggers on one line (if mid-line triggers are supported)
0104   int endingstate = allowmidline ? 1 : 0;
0105 
0106   string newoutput;
0107   int len = output.length ();
0108   string SOUND = "SOUND";
0109   string MUSIC = "MUSIC";
0110   for (int i = 0; i < len; i++)
0111   {
0112     char ch = output[i];
0113     switch (state)
0114     {
0115       case 0:
0116         //going to state 1 is handled after the switch statement
0117         newoutput += ch;
0118         break;
0119       case 1:
0120         if (ch == '!')
0121         {
0122           state = 2;
0123           cachedString = "!";
0124         }
0125         else
0126         {
0127           if ((!allowmidline) && (!(QChar(ch).isSpace ()))) //not a sound trigger
0128             state = 0;
0129           newoutput += ch;
0130         }
0131         break;
0132       case 2:
0133         cachedString += ch;
0134         if (ch == '!')
0135           state = 3;
0136         else
0137         {
0138           state = endingstate;
0139           newoutput += cachedString;
0140           cachedString = "";
0141         }
0142         break;
0143       case 3:
0144         cachedString += ch;
0145         if (ch == SOUND[0])
0146           state = 4;
0147         else
0148           if (ch == MUSIC[0])
0149             state = 9;
0150           else
0151           {
0152             state = endingstate;
0153             newoutput += cachedString;
0154             cachedString = "";
0155           }
0156         break;
0157       case 4:
0158       case 5:
0159       case 6:
0160       case 7:
0161         cachedString += ch;
0162         if (ch == SOUND[state - 3])
0163           state++;
0164         else
0165         {
0166           state = endingstate;
0167           newoutput += cachedString;
0168           cachedString = "";
0169         }
0170         break;
0171       case 8:
0172         cachedString += ch;
0173         if (ch == '(')
0174         {
0175           state = 14;
0176           inSOUND = true;
0177           cachedString = "";
0178           triggerContents = "";
0179         }
0180         else
0181           if (ch != ' ')    //allow spaces between SOUND and (
0182           {
0183             state = endingstate;
0184             newoutput += cachedString;
0185             cachedString = "";
0186           }
0187         break;
0188       case 9:
0189       case 10:
0190       case 11:
0191       case 12:
0192         cachedString += ch;
0193         if (ch == MUSIC[state - 8])
0194           state++;
0195         else
0196         {
0197           state = endingstate;
0198           newoutput += cachedString;
0199           cachedString = "";
0200         }
0201         break;
0202       case 13:
0203         cachedString += ch;
0204         if (ch == '(')
0205         {
0206           state = 14;
0207           inSOUND = false;
0208           cachedString = "";
0209           triggerContents = "";
0210         }
0211         else
0212           if (ch != ' ')    //allow spaces between SOUND and (
0213           {
0214             state = endingstate;
0215             newoutput += cachedString;
0216             cachedString = "";
0217           }
0218         break;
0219       case 14:
0220         if ((ch == '\n') || (ch == '\r'))
0221         //unfinished trigger! - IGNORE IT!
0222         {
0223           cachedString = "";
0224           triggerContents = "";
0225           state = 1;
0226           newoutput += ch;
0227           invokeEvent ("message", sess(), "MSP: unfinished sound trigger!");
0228         }
0229         else
0230           if (ch == ')')
0231           {
0232             state = endingstate;
0233             //we negotiate and parse MSP stuff even if we're told not
0234             //to do so; we therefore need to look if the user wants to hear
0235             //sounds
0236             if (mspallowed && ((!sett) || sett->getBool ("use-msp")))
0237               parseTrigger (QString (triggerContents.c_str()), inSOUND);
0238             triggerContents = "";
0239           }
0240           else
0241             triggerContents += ch;
0242         break;
0243     }
0244     //go to state 1 if we're in state 0 and end-of-line comes
0245     //this must be placed outside the switch statement, because we can enter
0246     //state 0 when an unfinished trigger is received; and if it's terminated
0247     //by \n, we would end up going through the next line in state 0, ignoring
0248     //a sound trigger on that line (if any)
0249     if ((state == 0) && ((ch == '\n') || (ch == '\r')))
0250       state = 1;
0251   }
0252   return newoutput;
0253 }
0254 
0255 QString cMSP::nextToken (QString &from)
0256 {
0257   //strip whitespaces
0258   from = from.trimmed ();
0259   if (from.length() == 0)
0260     return QString();
0261   
0262   //return value...
0263   QString ret;
0264   //'=' sign first
0265   if (from[0] == '=')
0266   {
0267     ret = "=";
0268     from = from.remove (0, 1);
0269   }
0270   else
0271   {
0272     //space and = are separators
0273     ret = from.section (' ', 0, 0, QString::SectionSkipEmpty);
0274     ret = ret.section ('=', 0, 0, QString::SectionSkipEmpty);
0275     int len = ret.length();
0276     from = from.remove (0, len);
0277   }
0278   from = from.trimmed ();
0279   return ret;
0280 }
0281 
0282 void cMSP::corruptedTrigger (const QString &reason)
0283 {
0284   invokeEvent ("message", sess(), i18n ("MSP: Received corrupted sound trigger!"));
0285   invokeEvent ("message", sess(), i18n ("MSP: Problem was: %1", reason));
0286 }
0287 
0288 void cMSP::parseTrigger (const QString &seq, bool isSOUND)
0289 {
0290   QString fName = QString(), type, url;
0291   int volume = 100, repeats = 1, priority = isSOUND ? 50 : 1;
0292   int parserState = 0;
0293   QString paramName, paramValue;
0294 
0295   QString trigger = seq;
0296   while (trigger.length() > 0)
0297   {
0298     QString token = nextToken (trigger);
0299     if (fName.length() == 0)  //no fName yet => this token is fName
0300       fName = token;
0301     else
0302     {
0303       if (parserState == 0)
0304       {
0305         paramName = token;
0306         parserState++;
0307       }
0308       else
0309       if (parserState == 1)
0310       {
0311         if (token == QString("="))
0312           parserState++;
0313         else
0314         {
0315           corruptedTrigger (i18n ("Parameter names must be followed by '='."));
0316           return;
0317         }
0318       }
0319       else
0320       if (parserState == 2)
0321       {
0322         paramValue = token;
0323         parserState = 0;
0324 
0325         //only if param name length is exactly 1; all params in MSP 0.3
0326         //should have this length; longer params are ignored, but not
0327         //reported as error, as they could be valid in later MSP versions
0328         if (paramName.length() == 1)
0329         {
0330           bool ok;
0331           int number = 0;
0332 
0333           char ch = paramName[0].toUpper().toLatin1();
0334           //we'll need this with these params
0335           if ((ch == 'V') || (ch == 'L') || (ch == 'P') || (ch == 'C'))
0336           {
0337             number = paramValue.toInt (&ok);
0338             if (!ok)
0339             {
0340               corruptedTrigger (i18n ("Parameter %1 requires a numeric argument", QChar(ch)));
0341               return;
0342             }
0343           }
0344 
0345           //fill in appropriate variable
0346 
0347           if (ch == 'V')
0348           {
0349             if ((number < 0) || (number > 100))
0350             {
0351               corruptedTrigger (i18n ("Value of param V is out of 0-100 range."));
0352               return;
0353             }
0354             volume = number;
0355           }
0356           if (ch == 'L')
0357           {
0358             if (((number < 1) && (number != -1)) || (number > 100))
0359             {
0360               corruptedTrigger (i18n ("Value of param L is out of -1;1-100 range."));
0361               return;
0362             }
0363             repeats = number;
0364           }
0365           if (ch == 'T')
0366             type = paramValue;
0367           if (ch == 'U')
0368             url = paramValue;
0369           if ((ch == 'P') && (isSOUND))
0370           {
0371             if ((number < 0) || (number > 100))
0372             {
0373               corruptedTrigger (i18n ("Value of param P is out of 0-100 range."));
0374               return;
0375             }
0376             priority = number;
0377           }
0378           if ((ch == 'C') && (!isSOUND))
0379           {
0380             if ((number != 0) && (number != 1))
0381             {
0382               corruptedTrigger (i18n ("Value of param C must be 0 or 1."));
0383               return;
0384             }
0385             priority = number;
0386           }
0387         }
0388       }
0389     }
0390   }
0391   if (parserState != 0)   //unfinished sequence!!!
0392   {
0393     corruptedTrigger (i18n ("Received unfinished sequence."));
0394     return;
0395   }
0396 
0397   //trigger parsed successfully, so process the request
0398   processRequest (isSOUND, fName, volume, repeats, priority, type, url);
0399 }
0400 
0401 void cMSP::processRequest (bool isSOUND, QString fName, int volume, int repeats, int priority,
0402       QString type, QString url)
0403 {
0404   //determine file name
0405   //special case - name is OFF
0406   if (fName.toLower() == "off")
0407   {
0408     if (url.length() > 0)
0409       //if U param is given here, we're setting the default URL; this is not
0410       //in official standard, but it's in zMUD, so I implement it as well
0411       defaultURL = url;
0412     else
0413       //off received - turn sound/music off
0414       if (isSOUND)
0415         soundOff ();
0416       else
0417         musicOff ();
0418     return;
0419   }
0420 
0421   //apply default URL, if we have one and if it's needed
0422   if ((url.length() == 0) && (defaultURL.length() > 0))
0423     url = defaultURL;
0424 
0425   //sometimes the URL already includes the file name...
0426   if (url.right (fName.length()) == fName)
0427     url = url.remove (url.length() - fName.length(), fName.length());
0428 
0429   //if no extension is specified, assume .wav or .mid, depending on trigger
0430   //type (SOUND/MUSIC)
0431   QString filename = fName.section ('/', -1, -1, QString::SectionSkipEmpty);
0432   if (!(filename.contains (".")))
0433     fName += isSOUND ? QString(".wav") : QString(".mid");
0434 
0435   //try to find that file in MUD-specific directory and in directories
0436   //specified in config
0437 
0438   QString file = findFile (fName);
0439   if (file == QString())  //failed to find that file :(
0440   {
0441     //no file found... will need to download it, if possible
0442     if ((url.length() > 0) && dloadallowed)
0443     {
0444       if (downloader->downloading())
0445       {
0446         //ignore requests for multiple downloading; the second file will
0447         //be downloaded when it's requested again
0448         invokeEvent ("message", sess(),
0449           i18n ("MSP: Multiple downloads not supported, request ignored."));
0450         return;
0451       }
0452       else
0453       {
0454         //store download parameters
0455         dl_fName = fName;
0456         dl_type = type;
0457         dl_url = url;
0458         dl_volume = volume;
0459         dl_repeats = repeats;
0460         dl_priority = priority;
0461         dl_issound = isSOUND;
0462         downloadFile ();
0463       }
0464     }
0465     else
0466       if (!url.isEmpty())
0467         //inform about failed request
0468         invokeEvent ("message", sess(),
0469             i18n ("MSP: downloading of sounds disabled - request ignored."));
0470     //not found & no url available or dloads not allowed -> NO SOUND
0471   }
0472   else
0473   {
0474     //great, file is here - play it!
0475     if (isSOUND)
0476       playSound (file, volume, repeats, priority);
0477     else
0478       playMusic (file, volume, repeats, (priority == 1));
0479   }
0480 }
0481 
0482 void cMSP::soundOff ()
0483 {
0484   soundPlayer->stop ();
0485 }
0486 
0487 void cMSP::musicOff ()
0488 {
0489   midiPlayer->stop ();
0490 }
0491 
0492 void cMSP::playSound (const QString &path, int volume, int repeats, int priority)
0493 {
0494   if (soundPlayer->isPlaying())
0495   {
0496     //soundPlayer is currently playing; we'll see who has greater priority...
0497     if (soundPlayer->curPriority() >= priority)
0498       //he should continue...
0499       return;
0500     //our priority is greater - we will play, so let's stop old sound!
0501     soundPlayer->stop ();
0502   }
0503 
0504   //ok, now go and play :)
0505   soundPlayer->setFileName (path);
0506   soundPlayer->setPriority (priority);
0507   soundPlayer->setRepeatsCount (repeats);
0508   soundPlayer->setVolume (volume);
0509   soundPlayer->play ();
0510 }
0511 
0512 void cMSP::playMusic (const QString &path, int volume, int repeats, bool continueIfRerequested)
0513 {
0514   if (midiPlayer->isPlaying())
0515   {
0516     //midiPlayer is currently playing...
0517 
0518     //see what's being played...
0519     if (midiPlayer->fileName() == path)
0520     {
0521       if (midiPlayer->curPriority () == 1)
0522       {
0523         //continue playing, but with new params
0524         midiPlayer->setRepeatsCount (repeats);
0525         midiPlayer->setPriority (continueIfRerequested ? 1 : 0);
0526         midiPlayer->setVolume (volume);
0527         midiPlayer->forceUpdateParams ();
0528       }
0529       else
0530       {
0531         //restart playing
0532         midiPlayer->stop ();
0533       }
0534     }
0535     else
0536     {
0537       //if he's playing something different, he should stop and play new music
0538       midiPlayer->stop ();
0539     }
0540   }
0541 
0542   //ok, now go and play :)
0543   midiPlayer->setFileName (path);
0544   midiPlayer->setPriority (continueIfRerequested ? 1 : 0);
0545   midiPlayer->setRepeatsCount (repeats);
0546   midiPlayer->setVolume (volume);
0547   midiPlayer->play ();
0548 }
0549 
0550 void cMSP::downloadFile ()
0551 {
0552   downloader->download (dl_url + "/" + dl_fName, localdir + "/" + dl_fName);
0553 }
0554 
0555 void cMSP::downloadCompleted ()
0556 {
0557   QString file = localdir + "/" + dl_fName;
0558   if (dl_issound)
0559     playSound (file, dl_volume, dl_repeats, dl_priority);
0560   else
0561     playMusic (file, dl_volume, dl_repeats, (dl_priority == 1));
0562 }
0563 
0564 void cMSP::downloadFailed (const QString &reason)
0565 {
0566   invokeEvent ("message", sess(), "MSP: " + reason);
0567 }
0568 
0569 QString cMSP::getFileName (QString where, QString what)
0570 {
0571   QString fName = what.section ("/", -1, -1, QString::SectionSkipEmpty);
0572   QString attempt1 = where + "/" + what;
0573   QString attempt2 = where + fName;
0574   //directory is path without last part (file name)
0575   QString dir1 = attempt1.section ("/", 0, -2, QString::SectionSkipEmpty | QString::SectionIncludeLeadingSep);
0576   QString dir2 = attempt2.section ("/", 0, -2, QString::SectionSkipEmpty | QString::SectionIncludeLeadingSep);
0577   QStringList fileList;
0578   QString retString;
0579   
0580   QDir d1 (dir1, fName);
0581   retString = dir1;
0582   fileList = d1.entryList ();
0583   if (fileList.count() == 0)
0584   {
0585     //no files in dir1
0586     QDir d2 (dir2, fName);
0587     retString = dir2;
0588     fileList = d2.entryList ();
0589   }
0590   int count = fileList.count();
0591   if (count > 0)
0592   {
0593     //found!!! Return (count+1)th file name
0594     int randomNumber = random() % count;
0595     QStringList::iterator it;
0596     for (it = fileList.begin(); it != fileList.end(); ++it)
0597       if ((randomNumber--) == 0)
0598         break;
0599     return retString + "/" + *it;
0600   }
0601   return QString();
0602 }
0603 
0604 QString cMSP::findFile (const QString &path)
0605 {
0606   //okay, let's work...
0607   QString file;
0608   QStringList dirlist;
0609   QStringList::iterator it;
0610   
0611   //first of all, have a look in profile directory
0612   file = getFileName (localdir, path);
0613   if (file != QString())
0614     return file;
0615     
0616   //nothing... check profile-specific sound directories
0617   cProfileSettings *sett = settings ();
0618   if (sett)
0619   {
0620     int cnt = sett->getInt ("sound-dir-count");
0621     for (int i = 1; i <= cnt; ++i)
0622     {
0623       file = getFileName (sett->getString ("sound-dir-"+QString::number(i)), path);
0624       if (!file.isEmpty())
0625         return file;
0626     }
0627   }
0628   
0629   //still nothing... check global sound directories
0630   for (it = globaldirs.begin(); it != globaldirs.end(); ++it)
0631   {
0632     file = getFileName (*it, path);
0633     if (file != QString())
0634       return file;
0635   }
0636   
0637   //nothing :(
0638   return QString();
0639 }
0640