File indexing completed on 2025-01-12 06:47:28
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