Skip to content
Snippets Groups Projects
tlevel.cpp 38 KiB
Newer Older
  • Learn to ignore specific revisions
  • SeeLook's avatar
    SeeLook committed
    /***************************************************************************
    
     *   Copyright (C) 2011-2021 by Tomasz Bojczuk                             *
    
     *   seelook@gmail.com                                                     *
    
    SeeLook's avatar
    SeeLook committed
     *                                                                         *
     *   This program is free software; you can redistribute it and/or modify  *
     *   it under the terms of the GNU General Public License as published by  *
     *   the Free Software Foundation; either version 3 of the License, or     *
     *   (at your option) any later version.                                   *
     *                                                                         *
     *   This program is distributed in the hope that it will be useful,       *
     *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
     *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
     *   GNU General Public License for more details.                          *
     *                                                                         *
     *  You should have received a copy of the GNU General Public License      *
     *  along with this program.  If not, see <http://www.gnu.org/licenses/>.  *
     ***************************************************************************/
    
    #include "tlevel.h"
    
    SeeLook's avatar
    SeeLook committed
    #include <music/ttune.h>
    #include <taudioparams.h>
    
    #include <tscoreparams.h>
    
    #include <QtCore/qdebug.h>
    
    #include <QtCore/qfile.h>
    #include <QtCore/qvariant.h>
    
    #include <QtCore/qxmlstream.h>
    #include <QtWidgets/qapplication.h>
    #include <QtWidgets/qmessagebox.h>
    
    SeeLook's avatar
    SeeLook committed
    
    /*static-------------------------------------------------------------------------------------------*/
    
    SeeLook's avatar
    SeeLook committed
     * 2. 0x95121703 (02.12.2013)
    
     *     - support for instrument types and guessing an instrument from previous version
    
     *     - instrument enum is casting directly to quint8: Tinstrument::NoInstrument is 0
    
     * 3. 0x95121705 (22.06.2014) - XML stream - universal version
    
     * 4. 0x95121707 (05.02.2018) - rhythms, new instruments, melodies in Music XML
    
     *
     * 5. 0x95121709 (09.06.2021) - compression and ukulele support
     *
    
    SeeLook's avatar
    SeeLook committed
     */
    
    const qint32 Tlevel::levelVersion = 0x95121701;
    
    const qint32 Tlevel::currentVersion = 0x95121709; // Version 5
    
    SeeLook's avatar
    SeeLook committed
    
    
    int Tlevel::levelVersionNr(qint32 ver)
    {
        if ((ver - levelVersion) % 2)
            return -1; // invalid when rest of division is 1
        return ((ver - levelVersion) / 2) + 1;
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    bool Tlevel::isLevelVersion(quint32 ver)
    {
        if (levelVersionNr(ver) <= levelVersionNr(currentVersion))
            return true;
        else
            return false;
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    bool Tlevel::couldBeLevel(qint32 ver)
    {
        int givenVersion = levelVersionNr(ver);
        if (givenVersion >= 1 && givenVersion <= 127)
            return true;
        else
            return false;
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    /**
     * TlevelSelector context of translate() is used for backward compatibility with translations
     */
    
    void Tlevel::fileIOerrorMsg(QFile &f)
    {
        if (!f.fileName().isEmpty())
            QMessageBox::critical(nullptr, QLatin1String(" "), QApplication::translate("TlevelSelector", "Cannot open file\n %1 \n for reading").arg(f.fileName()));
        else
            QMessageBox::critical(nullptr, QLatin1String(" "), QApplication::translate("TlevelSelector", "No file name specified"));
    
    void Tlevel::fretFromXml(QXmlStreamReader &xml, char &fr, Tlevel::EerrorType &err)
    {
        fr = (char)QVariant(xml.readElementText()).toInt();
        if (fr < 0 || fr > 24) { // max frets number
            fr = 0;
            qDebug() << "[Tlevel] Fret number in" << xml.name() << "was wrong but fixed";
            err = Tlevel::e_levelFixed;
        }
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    void Tlevel::skipCurrentXmlKey(QXmlStreamReader &xml)
    {
        qDebug() << "[Tlevel] Unrecognized key:" << xml.name();
        xml.skipCurrentElement();
    
    /*end static--------------------------------------------------------------------------------------*/
    
    Tlevel::Tlevel()
        : hasInstrToFix(false)
    {
        // level parameters
        name = QObject::tr("master of masters");
        desc = QObject::tr("All possible options are turned on");
        bool hasGuitar = GLOB->instrument().isGuitar();
        // QUESTIONS
        questionAs = TQAtype(true, true, GLOB->instrument().type() != Tinstrument::NoInstrument, true);
        answersAs[0] = TQAtype(true, true, GLOB->instrument().type() != Tinstrument::NoInstrument, true);
        answersAs[1] = TQAtype(true, true, GLOB->instrument().type() != Tinstrument::NoInstrument, true);
        answersAs[2] = TQAtype(true, true, GLOB->instrument().isGuitar(), false);
        answersAs[3] = TQAtype(true, true, GLOB->instrument().type() != Tinstrument::NoInstrument, true);
        requireOctave = true;
        requireStyle = true;
    
        /**
         * variables isNoteLo, isNoteHi and isFretHi are not used - it has no sense.
         *  Since version 0.8.90 isNoteLo and isNoteHi are merged into Tclef.
         *  It can store multiple clefs (maybe in unknown future it will be used)
         *  0 - no clef and up to 15 different clefs.
         */
    
        clef = Tclef(GLOB->scoreParams->clef);
    
        instrument = GLOB->instrument().type();
        onlyLowPos = false;
        onlyCurrKey = false;
        intonation = GLOB->audioParams->intonation;
        // ACCIDENTALS
        withSharps = true;
        withFlats = true;
        withDblAcc = true;
        useKeySign = true;
        isSingleKey = false;
        loKey = TkeySignature(-7);
        hiKey = TkeySignature(7); // key range (7b to 7#)
        manualKey = true;
        forceAccids = true;
        showStrNr = hasGuitar;
        // MELODIES
        melodyLen = 1;
        endsOnTonic = true;
        requireInTempo = true;
        howGetMelody = e_randFromRange;
        randOrderInSet = true;
        repeatNrInSet = 1;
        //   notesList is clean here
        // RHYTHMS
        basicRhythms = 0;
        dotsRhythms = 0;
        meters = 0;
        rhythmDiversity = 5;
        barNumber = 4;
        variableBarNr = true;
        useRests = false;
        useTies = false;
        // RANGE - for non guitar Tglobals will returns scale determined by clef
        loNote = GLOB->loString();
        hiNote = Tnote(GLOB->hiString().chromatic() + GLOB->GfretsNumber);
        loFret = 0;
        hiFret = GLOB->GfretsNumber;
        for (int i = 0; i < 6; i++) {
            if (i <= GLOB->Gtune()->stringNr())
                usedStrings[i] = true;
            else
                usedStrings[i] = false;
        }
    }
    
    bool getLevelFromStream(QDataStream &in, Tlevel &lev, qint32 ver)
    {
        bool ok = true;
        in >> lev.name >> lev.desc;
        in >> lev.questionAs;
        in >> lev.answersAs[0] >> lev.answersAs[1] >> lev.answersAs[2] >> lev.answersAs[3];
        in >> lev.withSharps >> lev.withFlats >> lev.withDblAcc;
        quint8 sharedByte;
        in >> lev.useKeySign >> sharedByte;
        lev.isSingleKey = (bool)(sharedByte % 2);
        lev.intonation = sharedByte / 2;
        ok = getKeyFromStream(in, lev.loKey);
        ok = getKeyFromStream(in, lev.hiKey);
        in >> lev.manualKey >> lev.forceAccids;
        in >> lev.requireOctave >> lev.requireStyle;
        // RANGE
        ok = getNoteFromStream(in, lev.loNote);
        ok = getNoteFromStream(in, lev.hiNote);
        /** Merged to quint16 since version 0.8.90 */
        quint16 testClef;
        in >> testClef;
        qint8 lo, hi;
        in >> lo >> hi;
        if (lo < 0 || lo > 24) { // max frets number
            lo = 0;
            ok = false;
        }
        if (hi < 0 || hi > 24) { // max frets number
            hi = GLOB->GfretsNumber;
            ok = false;
        }
        lev.loFret = char(lo);
        lev.hiFret = char(hi);
        /** Previously is was bool type */
        quint8 instr;
        in >> instr;
        in >> lev.usedStrings[0] >> lev.usedStrings[1] >> lev.usedStrings[2] >> lev.usedStrings[3] >> lev.usedStrings[4] >> lev.usedStrings[5];
        in >> lev.onlyLowPos >> lev.onlyCurrKey >> lev.showStrNr;
        if (ver == lev.levelVersion) { // first version of level file structure
            lev.clef = lev.fixClef(testClef); // determining/fixing a clef from first version
            lev.instrument = lev.fixInstrument(instr); // determining/fixing an instrument type
        } else {
            lev.clef = Tclef((Tclef::EclefType)testClef);
            lev.instrument = (Tinstrument::Etype)instr;
        }
        lev.melodyLen = 1; // Those parameters was deployed in XML files
        lev.endsOnTonic = false; // By settings those values their will be ignored
        lev.requireInTempo = false;
        return ok;
    }
    
    Tlevel::EerrorType Tlevel::qaTypeFromXml(QXmlStreamReader &xml)
    {
        TQAtype qa;
        EerrorType er = e_level_OK;
        int id = qa.fromXml(xml);
        if (id == -1) {
            questionAs = qa;
            if (!questionAs.isOnScore() && !questionAs.isName() && !questionAs.isOnInstr() && !questionAs.isSound()) {
                qDebug() << "There are not any questions in a level. It makes no sense.";
                return e_otherError;
            }
        } else if (id >= 0 && id < 4) {
            answersAs[id] = qa;
            // verify every answersAs context and set corresponding questionAs to false when all were unset (false)
            if (questionAs.isOnScore() && (!answersAs[0].isOnScore() && !answersAs[0].isName() && !answersAs[0].isOnInstr() && !answersAs[0].isSound())) {
                er = e_levelFixed;
                questionAs.setOnScore(false);
            }
            if (questionAs.isName() && (!answersAs[1].isOnScore() && !answersAs[1].isName() && !answersAs[1].isOnInstr() && !answersAs[1].isSound())) {
                er = e_levelFixed;
                questionAs.setAsName(false);
            }
            if (questionAs.isOnInstr() && (!answersAs[2].isOnScore() && !answersAs[2].isName() && !answersAs[2].isOnInstr() && !answersAs[2].isSound())) {
                er = e_levelFixed;
                questionAs.setOnInstr(false);
            }
            if (questionAs.isSound() && (!answersAs[3].isOnScore() && !answersAs[3].isName() && !answersAs[3].isOnInstr() && !answersAs[3].isSound())) {
                er = e_levelFixed;
                questionAs.setOnScore(false);
            }
        }
        return er;
    }
    
    Tlevel::EerrorType Tlevel::loadFromXml(QXmlStreamReader &xml)
    {
        EerrorType er = e_level_OK;
    
        if (xml.name() != QLatin1String("level")) {
            qDebug() << "[Tlevel] There is no 'level' key in that XML";
            return e_noLevelInXml;
        }
        name = xml.attributes().value(QLatin1String("name")).toString();
        if (name.isEmpty()) {
            qDebug() << "[Tlevel] Level key has empty 'name' attribute";
    
        }
        melodySet.clear();
        randOrderInSet = true;
        repeatNrInSet = 1;
    
        while (xml.readNextStartElement()) {
            if (xml.name() == QLatin1String("description"))
                desc = xml.readElementText();
            else if (xml.name() == QLatin1String("nameTR"))
                name = QApplication::translate("Levels", xml.readElementText().toLocal8Bit());
            else if (xml.name() == QLatin1String("descriptionTR"))
                desc = QApplication::translate("Levels", xml.readElementText().toLocal8Bit());
            // QUESTIONS
            else if (xml.name() == QLatin1String("questions")) {
                while (xml.readNextStartElement()) {
                    if (xml.name() == QLatin1String("qaType")) {
                        er = qaTypeFromXml(xml);
                        if (er == e_otherError)
                            return er;
                    } else if (xml.name() == QLatin1String("requireOctave"))
                        requireOctave = QVariant(xml.readElementText()).toBool();
                    else if (xml.name() == QLatin1String("requireStyle"))
                        requireStyle = QVariant(xml.readElementText()).toBool();
                    else if (xml.name() == QLatin1String("showStrNr"))
                        showStrNr = QVariant(xml.readElementText()).toBool();
                    else if (xml.name() == QLatin1String("clef")) {
                        clef.setClef(Tclef::EclefType(QVariant(xml.readElementText()).toInt()));
                        if (clef.name().isEmpty()) { // when clef has improper/unsupported value its name returns empty string
                            qDebug() << "[Tlevel] Level had wrong/undefined clef. It was fixed to treble dropped.";
                            clef.setClef(Tclef::Treble_G_8down);
                            er = e_levelFixed;
                        }
                    } else if (xml.name() == QLatin1String("instrument")) {
                        instrument = Tinstrument::Etype(QVariant(xml.readElementText()).toInt());
                        if (Tinstrument::staticName(instrument).isEmpty()) {
                            qDebug() << "[Tlevel] Level had wrong instrument type. It was fixed to classical guitar.";
                            instrument = Tinstrument::ClassicalGuitar;
                            er = e_levelFixed;
                        }
                    } else if (xml.name() == QLatin1String("onlyLowPos"))
                        onlyLowPos = QVariant(xml.readElementText()).toBool();
                    else if (xml.name() == QLatin1String("onlyCurrKey"))
                        onlyCurrKey = QVariant(xml.readElementText()).toBool();
                    else if (xml.name() == QLatin1String("intonation"))
                        intonation = QVariant(xml.readElementText()).toInt();
                    else
                        skipCurrentXmlKey(xml);
                }
            } else if (xml.name() == QLatin1String("melodies")) {
                // Melodies
                while (xml.readNextStartElement()) {
                    if (xml.name() == QLatin1String("melodyLength"))
                        melodyLen = qBound(1, QVariant(xml.readElementText()).toInt(), 100);
    
                    else if (xml.name() == QLatin1String("endsOnTonic"))
    
                        endsOnTonic = QVariant(xml.readElementText()).toBool();
                    else if (xml.name() == QLatin1String("requireInTempo"))
                        requireInTempo = QVariant(xml.readElementText()).toBool();
                    else if (xml.name() == QLatin1String("randType"))
                        howGetMelody = static_cast<EhowGetMelody>(xml.readElementText().toInt());
                    else if (xml.name() == QLatin1String("keyOfrandList")) {
                        xml.readNextStartElement();
                        keyOfrandList.fromXml(xml);
                        xml.skipCurrentElement();
                    } else if (xml.name() == QLatin1String("noteList")) {
                        notesList.clear();
                        while (xml.readNextStartElement()) {
                            if (xml.name() == QLatin1String("n")) {
                                notesList << Tnote();
                                notesList.last().fromXml(xml);
                                if (!notesList.last().isValid()) // skip empty notes
                                    notesList.removeLast();
                            } else
                                skipCurrentXmlKey(xml);
                        }
                    } else if (xml.name() == QLatin1String("randOrderInSet"))
                        randOrderInSet = QVariant(xml.readElementText()).toBool();
                    else if (xml.name() == QLatin1String("repeatNrInSet")) {
                        auto ris = QVariant(xml.readElementText()).toInt();
                        if (ris < 1 || ris > 15) {
                            ris = qBound(1, ris, 15);
                            qDebug() << "[Tlevel] value of melody repeats was wrong and fixed to" << ris;
                        }
                        repeatNrInSet = static_cast<quint8>(ris);
                    } else if (xml.name() == QLatin1String("melody")) {
                        auto t = xml.attributes().value(QLatin1String("title")).toString();
                        auto c = xml.attributes().value(QLatin1String("composer")).toString();
                        melodySet << Tmelody();
                        melodySet.last().fromXml(xml); // TODO: no validation here, could be vulnerable
                        melodySet.last().setTitle(t);
                        melodySet.last().setComposer(c);
                    } else
                        skipCurrentXmlKey(xml);
    
            } else if (xml.name() == QLatin1String("accidentals")) {
                while (xml.readNextStartElement()) {
                    //           qDebug() << "accidentals->" << xml.name();
                    if (xml.name() == QLatin1String("withSharps"))
                        withSharps = QVariant(xml.readElementText()).toBool();
                    else if (xml.name() == QLatin1String("withFlats"))
                        withFlats = QVariant(xml.readElementText()).toBool();
                    else if (xml.name() == QLatin1String("withDblAcc"))
                        withDblAcc = QVariant(xml.readElementText()).toBool();
                    else if (xml.name() == QLatin1String("useKeySign"))
                        useKeySign = QVariant(xml.readElementText()).toBool();
                    else if (xml.name() == QLatin1String("loKey")) {
                        xml.readNextStartElement();
                        loKey.fromXml(xml);
                        xml.skipCurrentElement();
                    } else if (xml.name() == QLatin1String("hiKey")) {
                        xml.readNextStartElement();
                        hiKey.fromXml(xml);
                        xml.skipCurrentElement();
                    } else if (xml.name() == QLatin1String("isSingleKey"))
                        isSingleKey = QVariant(xml.readElementText()).toBool();
                    else if (xml.name() == QLatin1String("manualKey"))
                        manualKey = QVariant(xml.readElementText()).toBool();
                    else if (xml.name() == QLatin1String("forceAccids"))
                        forceAccids = QVariant(xml.readElementText()).toBool();
                    else
                        skipCurrentXmlKey(xml);
    
            } else if (xml.name() == QLatin1String("rhythms")) {
                // RHYTHMS
                while (xml.readNextStartElement()) {
                    if (xml.name() == QLatin1String("meters"))
                        meters = static_cast<quint16>(QVariant(xml.readElementText()).toInt());
                    else if (xml.name() == QLatin1String("basic"))
                        basicRhythms = static_cast<quint32>(QVariant(xml.readElementText()).toUInt());
                    else if (xml.name() == QLatin1String("dots"))
                        dotsRhythms = static_cast<quint32>(QVariant(xml.readElementText()).toUInt());
                    else if (xml.name() == QLatin1String("diversity"))
                        rhythmDiversity = static_cast<quint8>(QVariant(xml.readElementText()).toInt());
                    else if (xml.name() == QLatin1String("bars")) {
                        variableBarNr = xml.attributes().value(QLatin1String("variable")) == QLatin1String("true");
                        barNumber = static_cast<quint8>(QVariant(xml.readElementText()).toInt());
                    } else if (xml.name() == QLatin1String("randomBars"))
                        variableBarNr = QVariant(xml.readElementText()).toBool();
                    else if (xml.name() == QLatin1String("rests"))
                        useRests = QVariant(xml.readElementText()).toBool();
                    else if (xml.name() == QLatin1String("ties"))
                        useTies = QVariant(xml.readElementText()).toBool();
                    else
                        skipCurrentXmlKey(xml);
                }
            } else if (xml.name() == QLatin1String("range")) {
                // RANGE
                while (xml.readNextStartElement()) {
                    if (xml.name() == QLatin1String("loFret"))
                        fretFromXml(xml, loFret, er);
                    else if (xml.name() == QLatin1String("hiFret"))
                        fretFromXml(xml, hiFret, er);
                    else if (xml.name() == QLatin1String("loNote"))
                        loNote.fromXml(xml);
                    else if (xml.name() == QLatin1String("hiNote"))
                        hiNote.fromXml(xml);
                    else if (xml.name() == QLatin1String("useString")) {
                        int id = xml.attributes().value(QLatin1String("number")).toInt();
                        if (id > 0 && id < 7)
                            usedStrings[id - 1] = QVariant(xml.readElementText()).toBool();
    
        }
        if (name.size() > 35) {
            name = name.left(35);
            qDebug() << "[Tlevel] Name of a level was reduced to 35 characters:" << name;
        }
        if (canBeInstr() && fixFretRange() == e_levelFixed) {
            er = e_levelFixed;
            qDebug() << "[Tlevel] Lowest fret in the range was bigger than the highest one. Fixed";
        }
        if (useKeySign && !isSingleKey && fixKeyRange() == e_levelFixed) {
            er = e_levelFixed;
            qDebug() << "[Tlevel] Lowest key in the range was higher than the highest one. Fixed";
        }
        if (loNote.note() == 0 || hiNote.note() == 0) {
            qDebug() << "[Tlevel] Note range is wrong.";
            return e_otherError;
        } else if (fixNoteRange() == e_levelFixed) {
            er = e_levelFixed;
            qDebug() << "[Tlevel] Lowest note in the range was higher than the highest one. Fixed";
        }
        if (notesList.isEmpty()) {
            if (howGetMelody == e_randFromList) {
                qDebug() << "[Tlevel] list of notes is empty but e_randFromList is set";
                howGetMelody = e_randFromRange;
    
        } else {
            if (howGetMelody == e_randFromRange) {
                qDebug() << "[Tlevel] has list of notes but e_randFromRange is set. List will be cleaned";
                notesList.clear();
    
        }
        if (howGetMelody == e_melodyFromSet) {
            if (melodySet.isEmpty()) {
                qDebug() << "[Tlevel] is melody set but list of melodies is empty. Level corrupted!!!";
                er = e_otherError;
    
        } else {
            if (!randOrderInSet) {
                qDebug() << "[Tlevel] is not a melody set but question order is set. Back to random.";
                randOrderInSet = true;
                er = e_levelFixed;
            }
            if (repeatNrInSet > 1) {
                qDebug() << "[Tlevel] is not a melody set but number of repeats was set. Fixed!";
                repeatNrInSet = 1;
                er = e_levelFixed;
            }
        }
        if (xml.hasError()) {
            qDebug() << "[Tlevel] level has error" << xml.errorString() << xml.lineNumber();
            return e_otherError;
        }
        return er;
    }
    
    void Tlevel::writeToXml(QXmlStreamWriter &xml)
    {
        xml.writeStartElement(QLatin1String("level"));
        bool nameTr = false, descTr = false;
        if (name.startsWith(QLatin1String("tr("))) {
            nameTr = true;
            name = name.mid(3);
        }
        if (desc.startsWith(QLatin1String("tr("))) {
            descTr = true;
            desc = desc.mid(3);
        }
    
        xml.writeAttribute(QLatin1String("name"), name);
    
            xml.writeTextElement(QLatin1String("nameTR"), name);
    
            xml.writeTextElement(QLatin1String("descriptionTR"), desc);
    
            xml.writeTextElement(QLatin1String("description"), desc);
        // QUESTIONS
    
        xml.writeStartElement(QLatin1String("questions"));
    
        questionAs.toXml(-1, xml);
        for (int i = 0; i < 4; i++)
    
        xml.writeTextElement(QLatin1String("requireOctave"), QVariant(requireOctave).toString());
        xml.writeTextElement(QLatin1String("requireStyle"), QVariant(requireStyle).toString());
        xml.writeTextElement(QLatin1String("showStrNr"), QVariant(showStrNr).toString());
        xml.writeTextElement(QLatin1String("clef"), QVariant(static_cast<int>(clef.type())).toString());
        xml.writeTextElement(QLatin1String("instrument"), QVariant(static_cast<int>(instrument)).toString());
        xml.writeTextElement(QLatin1String("onlyLowPos"), QVariant(onlyLowPos).toString());
        xml.writeTextElement(QLatin1String("onlyCurrKey"), QVariant(onlyCurrKey).toString());
        xml.writeTextElement(QLatin1String("intonation"), QVariant(intonation).toString());
    
        xml.writeStartElement(QLatin1String("accidentals"));
    
        xml.writeTextElement(QLatin1String("withSharps"), QVariant(withSharps).toString());
        xml.writeTextElement(QLatin1String("withFlats"), QVariant(withFlats).toString());
        xml.writeTextElement(QLatin1String("withDblAcc"), QVariant(withDblAcc).toString());
        xml.writeTextElement(QLatin1String("useKeySign"), QVariant(useKeySign).toString());
        xml.writeStartElement(QLatin1String("loKey"));
        loKey.toXml(xml);
        xml.writeEndElement(); // loKey
        xml.writeStartElement(QLatin1String("hiKey"));
        hiKey.toXml(xml);
        xml.writeEndElement(); // hiKey
        xml.writeTextElement(QLatin1String("isSingleKey"), QVariant(isSingleKey).toString());
        xml.writeTextElement(QLatin1String("manualKey"), QVariant(manualKey).toString());
        xml.writeTextElement(QLatin1String("forceAccids"), QVariant(forceAccids).toString());
    
        xml.writeStartElement(QLatin1String("melodies"));
    
        xml.writeTextElement(QLatin1String("melodyLength"), QVariant(melodyLen).toString());
        xml.writeTextElement(QLatin1String("endsOnTonic"), QVariant(endsOnTonic).toString());
        xml.writeTextElement(QLatin1String("requireInTempo"), QVariant(requireInTempo).toString());
        if (howGetMelody != e_randFromRange) { // write it only when needed
    
            xml.writeTextElement(QLatin1String("randType"), QVariant(static_cast<quint8>(howGetMelody)).toString());
            if (howGetMelody == e_randFromList) {
    
                xml.writeStartElement(QLatin1String("keyOfrandList"));
    
                xml.writeEndElement(); // keyOfrandList
                xml.writeStartElement(QLatin1String("noteList"));
                for (int n = 0; n < notesList.count(); ++n)
    
                    notesList[n].toXml(xml, QLatin1String("n")); // XML note wrapped into <n> tag
    
            } else if (howGetMelody == e_melodyFromSet) {
    
                xml.writeTextElement(QLatin1String("randOrderInSet"), QVariant(randOrderInSet).toString());
                xml.writeTextElement(QLatin1String("repeatNrInSet"), QVariant(repeatNrInSet).toString());
    
                for (int m = 0; m < melodySet.count(); ++m) {
    
                    xml.writeStartElement(QLatin1String("melody"));
                    Tmelody &mel = melodySet[m];
                    if (!mel.title().isEmpty())
                        xml.writeAttribute(QLatin1String("title"), mel.title());
                    if (!mel.composer().isEmpty())
                        xml.writeAttribute(QLatin1String("composer"), mel.composer());
                    melodySet[m].toXml(xml);
                    xml.writeEndElement(); // melody
    
        if (useRhythms()) { // store only when enabled
    
            xml.writeStartElement(QLatin1String("rhythms"));
    
            xml.writeTextElement(QLatin1String("meters"), QVariant(meters).toString());
            xml.writeTextElement(QLatin1String("basic"), QVariant(basicRhythms).toString());
            xml.writeTextElement(QLatin1String("dots"), QVariant(dotsRhythms).toString());
            xml.writeTextElement(QLatin1String("diversity"), QVariant(rhythmDiversity).toString());
            xml.writeStartElement(QLatin1String("bars"));
    
            xml.writeAttribute(QLatin1String("variable"), QVariant(variableBarNr).toString());
            xml.writeCharacters(QVariant(barNumber).toString());
    
            xml.writeEndElement(); // bars
            xml.writeTextElement(QLatin1String("rests"), QVariant(useRests).toString());
            xml.writeTextElement(QLatin1String("ties"), QVariant(useTies).toString());
    
            xml.writeEndElement(); // rhythms
    
        xml.writeStartElement(QLatin1String("range"));
    
        xml.writeTextElement(QLatin1String("loFret"), QVariant(static_cast<qint8>(loFret)).toString());
        xml.writeTextElement(QLatin1String("hiFret"), QVariant(static_cast<qint8>(hiFret)).toString());
        loNote.toXml(xml, QLatin1String("loNote"));
        hiNote.toXml(xml, QLatin1String("hiNote"));
        for (int i = 0; i < 6; i++) {
    
            xml.writeStartElement(QLatin1String("useString"));
            xml.writeAttribute(QLatin1String("number"), QVariant(i + 1).toString());
    
            xml.writeCharacters(QVariant(usedStrings[i]).toString());
    
        xml.writeEndElement(); // level
    }
    
    bool Tlevel::saveToFile(Tlevel &level, const QString &levelFile)
    {
        QFile file(levelFile);
        if (file.open(QIODevice::WriteOnly)) {
            QDataStream out(&file);
            out.setVersion(QDataStream::Qt_5_9);
            out << currentVersion;
            QByteArray arrayXML;
            QXmlStreamWriter xml(&arrayXML);
    
            //       xml.setAutoFormatting(true);
            //       xml.setAutoFormattingIndent(2);
            xml.writeStartDocument();
            xml.writeComment(
                QStringLiteral("\nXML file of Nootka exam level.\n"
                               "https://nootka.sourceforge.io\n"
                               "It is strongly recommended to do not edit this file manually.\n"
                               "Use Nootka level creator instead!\n"));
            level.writeToXml(xml);
            xml.writeEndDocument();
    
            out << qCompress(arrayXML);
    
            file.close();
            return true;
        } else
            return false;
    
    // #################################################################################################
    // ###################      FIXES FOR LEVEL CONTENT     ############################################
    // #################################################################################################
    
    void Tlevel::convFromDropedBass()
    {
        if (clef.type() == Tclef::Bass_F_8down)
            clef.setClef(Tclef::Bass_F);
    
        loNote.riseOctaveUp();
        hiNote.riseOctaveUp();
        if (!notesList.isEmpty()) {
            for (int n = 0; n < notesList.count(); ++n)
                notesList[n].riseOctaveUp();
        }
        if (!melodySet.isEmpty()) {
            for (int m = 0; m < melodySet.count(); ++m) {
                auto mel = &melodySet[m];
                if (mel->clef() == Tclef::Bass_F_8down)
                    mel->setClef(Tclef::Bass_F);
                for (int n = 0; n < mel->length(); ++n)
                    mel->note(n)->p().riseOctaveUp();
            }
        }
    }
    
    SeeLook's avatar
    SeeLook committed
    
    
    Tclef Tlevel::fixClef(quint16 cl)
    {
    
        if (cl == 0) // For backward compatibility - 'no clef' never occurs
    
            return Tclef(Tclef::Treble_G_8down); // and versions before 0.8.90 kept here 0
    
            Tnote lowest(6, -2, 0);
            if (canBeInstr() || loNote.chromatic() < lowest.chromatic())
                return Tclef(Tclef::Treble_G_8down); // surely: 1 = e_treble_G was not intended here
            else
                return Tclef(Tclef::Treble_G);
    
        }
        if (cl != 2 && cl != 4 && cl != 8 && cl != 16 && cl != 32 && cl != 64 && cl != 128) {
    
            qDebug() << "[Tlevel] Fixed clef type. Previous value was:" << cl;
    
            return Tclef(Tclef::Treble_G_8down); // some previous mess - when levels didn't' support clefs
    
    SeeLook's avatar
    SeeLook committed
        }
    
        return Tclef((Tclef::EclefType)cl);
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    Tinstrument::Etype Tlevel::fixInstrument(quint8 instr)
    {
        // Value 255 comes from transition version 0.8.90 - 0.8.95 and means no instrument,
        // however it is invalid because it ignores guitarists and doesn't play exams/exercises on proper instrument
    
            if (canBeInstr() || canBeSound()) {
    
                hasInstrToFix = true;
                return GLOB->instrument().type();
            } else // instrument has no matter
                return Tinstrument::NoInstrument;
    
            // Values 0 and 1 occur in versions before 0.8.90 where an instrument doesn't exist
    
            if (canBeInstr() || canBeSound())
    
                return Tinstrument::ClassicalGuitar;
            else
                return Tinstrument::NoInstrument;
        } else if (instr < 4) { // simple cast to detect an instrument
    
            return (Tinstrument::Etype)instr;
    
            qDebug() << "[Tlevel]  Tlevel::instrument had some stupid value. FIXED";
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    Tinstrument::Etype Tlevel::detectInstrument(Tinstrument::Etype currInstr)
    {
        if (canBeInstr()) { // it has to be some kind of guitar
            if (currInstr != Tinstrument::NoInstrument)
                return currInstr;
            else // if current instrument isn't guitar force classical
                return Tinstrument::ClassicalGuitar;
        } else if (canBeSound()) // prefer current instrument for it
    
        else // no guitar & no sound - instrument type really has no matter
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    // ###################### HELPERS ################################################################
    
    /**
     * Checking is any question enabled first and then checking appropriate answer type.
     * Despite of level creator disables all questions with empty answers (set to false)
    
     * better check this again to avoid further problems.
    
    bool Tlevel::canBeScore() const
    {
        if (questionAs.isOnScore() || (questionAs.isName() && answersAs[TQAtype::e_asName].isOnScore())
            || (questionAs.isOnInstr() && answersAs[TQAtype::e_onInstr].isOnScore()) || (questionAs.isSound() && answersAs[TQAtype::e_asSound].isOnScore()))
            return true;
        else
            return false;
    }
    
    bool Tlevel::canBeName() const
    {
        if (questionAs.isName() || (questionAs.isOnScore() && answersAs[TQAtype::e_onScore].isName())
            || (questionAs.isOnInstr() && answersAs[TQAtype::e_onInstr].isName()) || (questionAs.isSound() && answersAs[TQAtype::e_asSound].isName()))
            return true;
        else
            return false;
    }
    
    bool Tlevel::canBeInstr() const
    {
        if (questionAs.isOnInstr() || (questionAs.isName() && answersAs[TQAtype::e_asName].isOnInstr())
            || (questionAs.isOnScore() && answersAs[TQAtype::e_onScore].isOnInstr()) || (questionAs.isSound() && answersAs[TQAtype::e_asSound].isOnInstr()))
            return true;
        else
            return false;
    }
    
    bool Tlevel::canBeSound() const
    {
        if (questionAs.isSound() || (questionAs.isName() && answersAs[TQAtype::e_asName].isSound())
            || (questionAs.isOnInstr() && answersAs[TQAtype::e_onInstr].isSound()) || (questionAs.isOnScore() && answersAs[TQAtype::e_onScore].isSound()))
            return true;
        else
            return false;
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    /**
     * To be sure, a melody is possible we checking not only notes number
     * but question-answer types as well, even if creator doesn't allow for wrong sets.
     */
    
    bool Tlevel::canBeMelody() const
    {
        if (melodyLen > 1
            && ((questionAs.isOnScore() && answersAs[TQAtype::e_onScore].isSound()) || (questionAs.isSound() && answersAs[TQAtype::e_asSound].isOnScore())
                || (questionAs.isSound() && answersAs[TQAtype::e_asSound].isSound())))
            return true;
        else
            return false;
    
    /**
     * Checking questions would be skipped because Level creator avoids selecting answer without question.
     * Unfortunately built-in levels are not so perfect.
     */
    
    bool Tlevel::answerIsNote() const
    {
        if ((questionAs.isOnScore() && answersAs[TQAtype::e_onScore].isOnScore()) || (questionAs.isName() && answersAs[TQAtype::e_asName].isOnScore())
            || (questionAs.isOnInstr() && answersAs[TQAtype::e_onInstr].isOnScore()) || (questionAs.isSound() && answersAs[TQAtype::e_asSound].isOnScore()))
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    bool Tlevel::answerIsName() const
    {
        if ((questionAs.isOnScore() && answersAs[TQAtype::e_onScore].isName()) || (questionAs.isName() && answersAs[TQAtype::e_asName].isName())
            || (questionAs.isOnInstr() && answersAs[TQAtype::e_onInstr].isName()) || (questionAs.isSound() && answersAs[TQAtype::e_asSound].isName()))
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    bool Tlevel::answerIsGuitar() const
    {
        if ((questionAs.isOnScore() && answersAs[TQAtype::e_onScore].isOnInstr()) || (questionAs.isName() && answersAs[TQAtype::e_asName].isOnInstr())
            || (questionAs.isOnInstr() && answersAs[TQAtype::e_onInstr].isOnInstr()) || (questionAs.isSound() && answersAs[TQAtype::e_asSound].isOnInstr()))
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    bool Tlevel::answerIsSound() const
    {
        if ((questionAs.isOnScore() && answersAs[TQAtype::e_onScore].isSound()) || (questionAs.isName() && answersAs[TQAtype::e_asName].isSound())
            || (questionAs.isOnInstr() && answersAs[TQAtype::e_onInstr].isSound()) || (questionAs.isSound() && answersAs[TQAtype::e_asSound].isSound()))
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    bool Tlevel::inScaleOf(int loNoteNr, int hiNoteNr) const
    {
        int loNr = loNote.chromatic();
        int hiNr = hiNote.chromatic();
        if (loNr >= loNoteNr && loNr <= hiNoteNr && hiNr >= loNoteNr && hiNr <= hiNoteNr)
            return true;
        else
            return false;
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    bool Tlevel::inScaleOf()
    {
        return inScaleOf(GLOB->loString().chromatic(), GLOB->hiNote().chromatic());
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    bool Tlevel::adjustFretsToScale(char &loF, char &hiF)
    {
        if (!inScaleOf()) // when note range is not in an instrument scale
            return false; // get rid - makes no sense to further check
    
        int lowest = GLOB->GfretsNumber, highest = 0;
        for (int no = loNote.chromatic(); no <= hiNote.chromatic(); no++) {
            if (!withFlats && !withSharps)
                if (Tnote(no).alter()) // skip note with accidental when not available in the level
                    continue;
            int tmpLow = GLOB->GfretsNumber;
            for (int st = 0; st < GLOB->Gtune()->stringNr(); st++) {
                if (!usedStrings[st])
                    continue;
                int diff = no - GLOB->Gtune()->str(GLOB->strOrder(st) + 1).chromatic();
                if (diff >= 0 && diff <= static_cast<int>(GLOB->GfretsNumber)) { // found
                    lowest = qMin<int>(lowest, diff);
                    tmpLow = qMin<int>(tmpLow, diff);
                }
            }
            highest = qMax<int>(highest, tmpLow);
    
        loF = static_cast<char>(lowest);
        hiF = static_cast<char>(highest);
        return true;
    
    SeeLook's avatar
    SeeLook committed
    }
    
    
    SeeLook's avatar
    SeeLook committed
    /**
     * FIXME
     * It is possible that first melody in the list will be without meter whether another one will have one
     */
    
    bool Tlevel::useRhythms() const
    {
        return canBeMelody() && ((meters && (dotsRhythms || basicRhythms)) || (isMelodySet() && melodySet.first().meter()->meter() != Tmeter::NoMeter));
    
    int Tlevel::keysInRange() const
    {
        if (!useKeySign || isSingleKey)
            return 1;
        if (hiKey.value() - loKey.value() < 0) {
            qDebug() << "[Tlevel] FIXME! Key range is invalid!";
            return 1;
        }
        return hiKey.value() - loKey.value() + 1;
    
    Tlevel::EerrorType Tlevel::fixFretRange()
    {
        if (loFret > hiFret) {
            char tmpFret = loFret;
            loFret = hiFret;
            hiFret = tmpFret;
            return e_levelFixed;
        }
        return e_level_OK;
    
    Tlevel::EerrorType Tlevel::fixNoteRange()
    {
        if (loNote.chromatic() > hiNote.chromatic()) {
            Tnote tmpNote = loNote;
            loNote = hiNote;
            hiNote = tmpNote;
            return e_levelFixed;
        }
        return e_level_OK;
    
    Tlevel::EerrorType Tlevel::fixKeyRange()
    {
        if (loKey.value() > hiKey.value()) {
            char tmpKey = loKey.value();
            loKey = hiKey;
            hiKey = TkeySignature(tmpKey);
            return e_levelFixed;
        }
        return e_level_OK;