20 июня 2023 20.06.23 3 2585

Боль программиста или как я кассеты для DayZ делал

+14

Предисловие

В этом блоге я буду рассказывать о том как я делал программу для создания модификаций — аддонов на мод Radio от Yuki.

Я, хоть и называю себя программиста, однако моя специальность — это мехатроника, и то я учусь XD. Самое сложное, что я делал — это программа для прогнозирования эпидемиологических ситуаций и вот эта программа о которой я сейчас буду писать.

А теперь Рыба!

Шаг 1: Задумка

Играя в DayZ с друзьями на ЛВС, нам ОЧЕНЬ нравилась модификация Radio из-за этой «особой» атмосферы. Она есть и в основной игре, но благодаря модификации многократно усиливалась. Эта модификация добавляла радио и магнитолы (для авто), в которых было можно проигрывать кассеты с музыкой или альбомами.

Но, играя мы задумывались, а можно ли с помощью этого мода, добавить свою музыку? И ответ — да! Можно!

Делается это с помощью создания модификаций — аддонов, которые добавляли кассеты для радио.

Шаг 2: Подгатовка

Я сразу понял, что буду писать программу на Python. В неё я пихаю музыкальные файлы, а на выходе получаю модификацию, которую можно залить на ЛВС сервер и играть.

Первое, что мне нужно было сделать — это… ПРИМЕР модификации. И это было самое сложное в разработке всей программы.

Я это понимал, и заручившись поддержкой опытного мододела Fantom228808, спустя неделю… не смог этого сделать. Не, не модификация — есть, но вот из-за корявой программы AddonBuilder, которая по какой-то причине не могла копировать файлы формата .cpp. Ничего не помогало, ни добавление в исключение, ни смена настроек, ни переустановка.

Хоть у меня не вышло создать полноценную модификацию, но со слов Fantom — всё было нормально. И я приступил к работе.

Шаг 3: Начало

Начал написание программы с кода, который просто проверял специальные директории и выводил её содержание в лист, который я далее буду использовать. Также я добавил код который в случае несуществования нужных директорий — создаст их.

import os

if os.path.isdir("input"):
        pass
    else:
        print("Папка input не найдена! Создание...")
        os.mkdir("input")
    if os.path.isdir("output"):
        pass
    else:
        print("Папка output не найдена! Создание...")
        os.mkdir("output")
files = os.listdir("input\\")

После этого я добавил ввод пользователем и создание «Системы» папок и директорий, для будущего аддона, а также проверку на существование модификации с тем же именем. В случае нахождения таковой я удаляю её функцией rmtree из библиотеки shutil. А ну, код для создания файла скрипта мода, я тоже добавил.


NameOfAddon = input("Название аддона >>> ")   
while True:
        if os.path.isdir(f"output\\{NameOfAddon}"):
            print(f"{NameOfAddon} уже создан, ")
            print("выберите другое название или нажмите ENTER, что бы пересоздать аддон с тем же именем")
            print(" ")
            INPUT_AAA = input("Название аддона >>> ")
            if INPUT_AAA == "":
                shutil.rmtree(f"output\\{NameOfAddon}")
                break
            else:
                NameOfAddon = INPUT_AAA
        else:
            break
os.mkdir(f"output\\{NameOfAddon}")
os.mkdir(f"output\\{NameOfAddon}\\Cassettes")
os.mkdir(f"output\\{NameOfAddon}\\Cassettes\\sounds")
scriptfile = open(f"output\\{NameOfAddon}\\config.cpp", "w", encoding='utf8')

Тут же я вспомнил о debug. И решил добавить соответствующую функцию и систему аргументов запуска.
В debug решил использовать довольно тяжёлую библиотеку rich, для вывода цветного текста.

def debug(text, status = 0):
    global DEBUG
    global LOGGING
    if LOGGING == True:
        global logfile
        match status:
            case 0: state = 'fine '
            case 1: state = 'warn '
            case 2: state = 'error'
        logfile.write(f"[{time.strftime('%H:%M:%S', time.localtime(time.time()))} {state}] {text} \n")
    if DEBUG == True:
        match status:
            case 0: rich.print(f"[{time.strftime('%H:%M:%S', time.localtime(time.time()))}] {text}")
            case 1: rich.print(f"[{time.strftime('%H:%M:%S', time.localtime(time.time()))}][bold][yellow] {text}[/bold][/yellow]")
            case 2: rich.print(f"[{time.strftime('%H:%M:%S', time.localtime(time.time()))}][bold][yellow] {text}[/bold][/yellow]")
    
for aa in range(len(sys.argv)):
            if sys.argv[aa].lower() == "-debug":
                DEBUG = True

Далее, у нас обработка файлов, конвертация и т. д.

Для конвертации мы используем библиотеку pydub, а именно функцию AudioSegment. Я решил написать отдельную функцию на случай, если захочу сделать мультиядерную обработку.

def mp3toogg(file, dist):
    AudioSegment.from_mp3(f"{file}").export(f"{dist}", format='ogg')
    #AudioSegment.from_mp3(Полное имя файла).export(куда сохранять файл, format='какой формат файла')
    os.remove(f"{file}")

Далее я взял список файлов в директории и проверил, какие это файлы. Для упрощения кода, я решил разделять файлы на формат mp3 и ogg, а после обрабатывать их таким образом: если mp3 — переводим в ogg, а если ogg просто перемещаем. Для отслеживания процесса использую библиотеку tqdm, которая добавляет «прогресс бары».

for i in tqdm(range(len(files))):
        error = False
        filename = None
        if files[i].endswith('.mp3'):
            debug(f"for i = {i}", 0)
            filename = files[i].removesuffix('.mp3')
            debug(f"filename = {filename}", 0)
            oggfiles.append(filename)
            if os.path.isfile(f"output\\{NameOfAddon}\\Cassettes\\sounds\\filename.ogg") == False:
                mp3toogg(f"input\\{filename}.mp3", f"output\\{NameOfAddon}\\Cassettes\\sounds\\{filename}.ogg")
            else:
                debug(f'os.path.isfile(f"output\\{NameOfAddon}\\Cassettes\\sounds\\{filename}.ogg") == True', 2)
                error = True
        elif files[i].endswith('.ogg'):
            filename = files[i].removesuffix('.ogg')
            debug(f"filename = {filename}", 0)
            oggfiles.append(filename)
            debug(f"oggfiles = {oggfiles}", 0)
            if not(os.path.exists(f"input\\{syntaxofsymbols(filename)}.ogg")):
                os.rename(f"input\\{oldfilename}.ogg", f"input\\{filename}.ogg")
            else:
                debug(f"Найден повторяющийся файл {filename}.ogg", 1)
            if os.path.isfile(f"output\\{NameOfAddon}\\Cassettes\\sounds\\{filename}.ogg") == False:   
                shutil.move(f"input\\{filename}.ogg", f"output\\{NameOfAddon}\\Cassettes\\sounds\\")
            else:
                debug(f'os.path.isfile(f"output\\{NameOfAddon}\\Cassettes\\sounds\\{syntaxofsymbols(filename)}.ogg") == True', 2)
                os.remove(f"input\\{syntaxofsymbols(filename)}.ogg")
                error = True

Но я тут же столкнулся с проблемой: КИРИЛЛИЦА. Слава богу, существует библиотека transliterate, которая может заменять кириллические буквы в латинские. Помимо этого я также написал функцию для удаления спец. символов.

def syntaxofsymbols(text):
    censoredsymbols = '"', '"', '@', ' ', '.', ',','<','>','?','/',';',':','[',']','{','}','|','#','$','%','^','&','*','(',')','`','~','%','№','+','-', ' '
    debug(f"syntaxofsymbols({text}) called")
    for aa in range(len(censoredsymbols)):
        text = text.replace(censoredsymbols[aa], "")
    debug(f"syntaxofsymbols() returned {text}")
    return text

def symboltest(symbol):
    if symbol.isalpha() or symbol.isnumeric() or symbol == " " or symbol == ","  or symbol == ".":
        debug(f"{symbol} - True")
        return True
    else:
        debug(f"{symbol} - False")
        return False

oggfiles = []
for i in tqdm(range(len(files))):
        error = False
        filename = None
        if files[i].endswith('.mp3'):
            debug(f"for i = {i}", 0)
            filename = files[i].removesuffix('.mp3')
            debug(f"filename = {filename}", 0)
            oldfilename = filename
            filename = transliterate.translit(filename, language_code='ru', reversed=True)
            debug(f"filename = {filename}", 0)
            getVals = list([val for val in filename
                        if symboltest(val) == True])
            filename = "".join(getVals)
            debug(f"filename = {filename}", 0)
            filename = filename.replace("–", "")
            debug(f"filename = {filename}", 0)
            oggfiles.append(filename)
            debug(f"oggfiles = {oggfiles}", 0)
            if not(os.path.exists(f"input\\{syntaxofsymbols(filename)}.mp3")):
                os.rename(f"input\\{oldfilename}.mp3", f"input\\{syntaxofsymbols(filename)}.mp3")
            else:
                debug(f"Найден повторяющийся файл {syntaxofsymbols(filename)}.mp3", 1)
                os.remove(f"input\\{syntaxofsymbols(filename)}.mp3")
            if os.path.isfile(f"output\\{NameOfAddon}\\Cassettes\\sounds\\{syntaxofsymbols(filename)}.ogg") == False:
                mp3toogg(f"input\\{syntaxofsymbols(filename)}.mp3", f"output\\{NameOfAddon}\\Cassettes\\sounds\\{syntaxofsymbols(filename)}.ogg")
            else:
                debug(f'os.path.isfile(f"output\\{NameOfAddon}\\Cassettes\\sounds\\{syntaxofsymbols(filename)}.ogg") == True', 2)
                error = True
        elif files[i].endswith('.ogg'):
            filename = files[i].removesuffix('.ogg')
            debug(f"filename = {filename}", 0)
            oldfilename = filename
            filename = transliterate.translit(filename, language_code='ru', reversed=True)
            getVals = list([val for val in filename
                        if symboltest(val) == True])
            filename = "".join(getVals)
            debug(f"filename = {filename}", 0)
            oggfiles.append(filename)
            debug(f"oggfiles = {oggfiles}", 0)
            if not(os.path.exists(f"input\\{syntaxofsymbols(filename)}.ogg")):
                os.rename(f"input\\{oldfilename}.ogg", f"input\\{syntaxofsymbols(filename)}.ogg")
            else:
                debug(f"Найден повторяющийся файл {syntaxofsymbols(filename)}.ogg", 1)
            if os.path.isfile(f"output\\{NameOfAddon}\\Cassettes\\sounds\\{syntaxofsymbols(filename)}.ogg") == False:   
                shutil.move(f"input\\{syntaxofsymbols(filename)}.ogg", f"output\\{NameOfAddon}\\Cassettes\\sounds\\")
            else:
                debug(f'os.path.isfile(f"output\\{NameOfAddon}\\Cassettes\\sounds\\{syntaxofsymbols(filename)}.ogg") == True', 2)
                os.remove(f"input\\{syntaxofsymbols(filename)}.ogg")
                error = True

Я начал писать конфиг на основе листа oggfiles, в котором находятся данные по всем музыкальным файлам. Конфиг пишу по уже заранее сделанному примеру.

    print("Создание class CfgVechiles")
    scriptfile.write('class CfgVehicles {\n' )
    scriptfile.write('    class YK_Cassette_Base;\n' )
    for j in tqdm(range(len(oggfiles))):
            scriptfile.write(f'    class {NameOfAddon}_{syntaxofsymbols(oggfiles[j])}: YK_Cassette_Base\n' )
            scriptfile.write('        {\n' )
            scriptfile.write('        scope=2;\n')
            scriptfile.write(f'        displayName="Cassette {syntaxofsymbols(oggfiles[j])}";\n' )
            scriptfile.write('        descriptionShort="Created by robot";\n' )
            scriptfile.write("        hiddenSelectionsTextures[]=\n" )
            scriptfile.write("        {\n" )
            scriptfile.write('            "YK_Radio\Cassettes\Clear\data\cassette_co.paa"\n' )
            scriptfile.write("        };\n" )
            scriptfile.write("        class CfgCassette\n" )
            scriptfile.write("        {\n" )
            scriptfile.write(f'            soundSet="{NameOfAddon}_SoundSet_{syntaxofsymbols(oggfiles[j])}";\n' )
            scriptfile.write("        };\n" )
            scriptfile.write("    };\n" )
    print("ГОТОВО!")
    print("Создание class CfgSoundSets")
    scriptfile.write('class CfgSoundSets {\n' )
    scriptfile.write('	class Mods_SoundSet_Base;\n' )
    for u in tqdm(range(len(oggfiles))):
        scriptfile.write(f'	class {NameOfAddon}_SoundSet_{syntaxofsymbols(oggfiles[u])}\n' )
        scriptfile.write('  {\n' )
        scriptfile.write('		soundShaders[]=\n' )
        scriptfile.write('		{\n' )
        scriptfile.write(f'			"{NameOfAddon}_{syntaxofsymbols(oggfiles[u])}_Shader"\n' )
        scriptfile.write('		};\n' )
        scriptfile.write('	};\n' )
    print("ГОТОВО!")
    print("Создание class CfgSoundShaders")
    scriptfile.write('class CfgSoundShaders {\n' )
    scriptfile.write('	class YK_Cassette_SoundShader_Base;\n' )
    for t in tqdm(range(len(oggfiles))):
        scriptfile.write(f'	class {NameOfAddon}_{syntaxofsymbols(oggfiles[t])}_Shader: YK_Cassette_SoundShader_Base\n' )
        scriptfile.write('  {\n' )
        scriptfile.write('		samples[]=\n' )
        scriptfile.write('		{\n' )
        scriptfile.write('			{\n' )
        scriptfile.write(f'				"{NameOfAddon}\Cassettes\sounds\{syntaxofsymbols(oggfiles[t])}.ogg",\n' )
        scriptfile.write('				1\n' )
        scriptfile.write('			}\n' )
        scriptfile.write('		};\n' )
        scriptfile.write('	};\n' )
    scriptfile.close()

Но, тут мне дурость ударила в голову, и я решил сделать код, который будет создавать types.xml. Но из-за излишней сложности, использовать спец. библиотеку для создания xml, я не решился.

    f = open(f"output\\{NameOfAddon}_types.xml", "w", encoding='utf8')
    for t in tqdm(range(len(oggfiles))):
        xmlstrpart = f'''   <type name="{NameOfAddon}_{syntaxofsymbols(oggfiles[t])}">
            <nominal>2</nominal>
            <lifetime>21600</lifetime>
            <restock>7200</restock>
            <min>1</min>
            <quantmin>-1</quantmin>
            <quantmax>-1</quantmax>
            <cost>100</cost>
            <flags count_in_cargo="0" count_in_hoarder="0" count_in_map="1" count_in_player="0" crafted="0" deloot="0"/>
            <category name="tools"/>
            <tag name="shelves"/>
            <usage name="Town"/>
            <usage name="Village"/>
            <usage name="School"/>
        </type>'''
        f.write(xmlstrpart)
    f.close()

И-и-и код готов! Теперь программа может создать полноценный аддон! Единственное, что нужно сделать пользователю так-это создать модификацию через AddonBuilder и DsUtils(> 5 минут)!
Финальный код на момент 3 шага:

def mp3toogg(file, dist):
    AudioSegment.from_mp3(f"{file}").export(f"{dist}", format='ogg')
    os.remove(f"{file}")

def syntaxofsymbols(text):
    censoredsymbols = '"', '"', '@', ' ', '.', ',','<','>','?','/',';',':','[',']','{','}','|','#','$','%','^','&','*','(',')','`','~','%','№','+','-', ' '
    debug(f"syntaxofsymbols({text}) called")
    for aa in range(len(censoredsymbols)):
        text = text.replace(censoredsymbols[aa], "")
    debug(f"syntaxofsymbols() returned {text}")
    return text

def symboltest(symbol):
    if symbol.isalpha() or symbol.isnumeric() or symbol == " " or symbol == ","  or symbol == ".":
        debug(f"{symbol} - True")
        return True
    else:
        debug(f"{symbol} - False")
        return False

def debug(text, status = 0):
    global DEBUG
    if DEBUG == True:
        match status:
            case 0: rich.print(f"[{time.strftime('%H:%M:%S', time.localtime(time.time()))}] {text}")
            case 1: rich.print(f"[{time.strftime('%H:%M:%S', time.localtime(time.time()))}][bold][yellow] {text}[/bold][/yellow]")
            case 2: rich.print(f"[{time.strftime('%H:%M:%S', time.localtime(time.time()))}][bold][yellow] {text}[/bold][/yellow]")
    
for aa in range(len(sys.argv)):
            if sys.argv[aa].lower() == "-debug":
                DEBUG = True
NameOfAddon = input("Название аддона >>> ")   
while True:
        if os.path.isdir(f"output\\{NameOfAddon}"):
            print(f"{NameOfAddon} уже создан, ")
            print("выберите другое название или нажмите ENTER, что бы пересоздать аддон с тем же именем")
            print(" ")
            INPUT_AAA = input("Название аддона >>> ")
            if INPUT_AAA == "":
                shutil.rmtree(f"output\\{NameOfAddon}")
                break
            else:
                NameOfAddon = INPUT_AAA
        else:
            break
os.mkdir(f"output\\{NameOfAddon}")
debug(f"output\\{NameOfAddon}",0)
os.mkdir(f"output\\{NameOfAddon}\\Cassettes")
debug(f"output\\{NameOfAddon}\\Cassettes",0)
os.mkdir(f"output\\{NameOfAddon}\\Cassettes\\sounds")
debug(f"output\\{NameOfAddon}\\Cassettes\\sounds",0)
scriptfile = open(f"output\\{NameOfAddon}\\config.cpp", "w", encoding='utf8')
debug(f"scriptfile = output\\{NameOfAddon}\\config.cpp, 'w', encoding='utf8'", 0)
files = os.listdir("input\\")
debug(f"Files: \n{files}",1)
oggfiles = []
for i in tqdm(range(len(files))):
        error = False
        filename = None
        if files[i].endswith('.mp3'):
            debug(f"for i = {i}", 0)
            filename = files[i].removesuffix('.mp3')
            debug(f"filename = {filename}", 0)
            oldfilename = filename
            filename = transliterate.translit(filename, language_code='ru', reversed=True)
            debug(f"filename = {filename}", 0)
            getVals = list([val for val in filename
                        if symboltest(val) == True])
            filename = "".join(getVals)
            debug(f"filename = {filename}", 0)
            filename = filename.replace("–", "")
            debug(f"filename = {filename}", 0)
            oggfiles.append(filename)
            debug(f"oggfiles = {oggfiles}", 0)
            if not(os.path.exists(f"input\\{syntaxofsymbols(filename)}.mp3")):
                os.rename(f"input\\{oldfilename}.mp3", f"input\\{syntaxofsymbols(filename)}.mp3")
            else:
                debug(f"Найден повторяющийся файл {syntaxofsymbols(filename)}.mp3", 1)
                os.remove(f"input\\{syntaxofsymbols(filename)}.mp3")
            if os.path.isfile(f"output\\{NameOfAddon}\\Cassettes\\sounds\\{syntaxofsymbols(filename)}.ogg") == False:
                mp3toogg(f"input\\{syntaxofsymbols(filename)}.mp3", f"output\\{NameOfAddon}\\Cassettes\\sounds\\{syntaxofsymbols(filename)}.ogg")
            else:
                debug(f'os.path.isfile(f"output\\{NameOfAddon}\\Cassettes\\sounds\\{syntaxofsymbols(filename)}.ogg") == True', 2)
                error = True
        elif files[i].endswith('.ogg'):
            filename = files[i].removesuffix('.ogg')
            debug(f"filename = {filename}", 0)
            oldfilename = filename
            filename = transliterate.translit(filename, language_code='ru', reversed=True)
            getVals = list([val for val in filename
                        if symboltest(val) == True])
            filename = "".join(getVals)
            debug(f"filename = {filename}", 0)
            oggfiles.append(filename)
            debug(f"oggfiles = {oggfiles}", 0)
            if not(os.path.exists(f"input\\{syntaxofsymbols(filename)}.ogg")):
                os.rename(f"input\\{oldfilename}.ogg", f"input\\{syntaxofsymbols(filename)}.ogg")
            else:
                debug(f"Найден повторяющийся файл {syntaxofsymbols(filename)}.ogg", 1)
            if os.path.isfile(f"output\\{NameOfAddon}\\Cassettes\\sounds\\{syntaxofsymbols(filename)}.ogg") == False:   
                shutil.move(f"input\\{syntaxofsymbols(filename)}.ogg", f"output\\{NameOfAddon}\\Cassettes\\sounds\\")
            else:
                debug(f'os.path.isfile(f"output\\{NameOfAddon}\\Cassettes\\sounds\\{syntaxofsymbols(filename)}.ogg") == True', 2)
                os.remove(f"input\\{syntaxofsymbols(filename)}.ogg")
                error = True    

print("Создание class CfgVechiles")
scriptfile.write('class CfgVehicles {\n' )
scriptfile.write('    class YK_Cassette_Base;\n' )
for j in tqdm(range(len(oggfiles))):
            scriptfile.write(f'    class {NameOfAddon}_{syntaxofsymbols(oggfiles[j])}: YK_Cassette_Base\n' )
            scriptfile.write('        {\n' )
            scriptfile.write('        scope=2;\n')
            scriptfile.write(f'        displayName="Cassette {syntaxofsymbols(oggfiles[j])}";\n' )
            scriptfile.write('        descriptionShort="Created by robot";\n' )
            scriptfile.write("        hiddenSelectionsTextures[]=\n" )
            scriptfile.write("        {\n" )
            scriptfile.write('            "YK_Radio\Cassettes\Clear\data\cassette_co.paa"\n' )
            scriptfile.write("        };\n" )
            scriptfile.write("        class CfgCassette\n" )
            scriptfile.write("        {\n" )
            scriptfile.write(f'            soundSet="{NameOfAddon}_SoundSet_{syntaxofsymbols(oggfiles[j])}";\n' )
            scriptfile.write("        };\n" )
            scriptfile.write("    };\n" )
print("ГОТОВО!")
print("Создание class CfgSoundSets")
scriptfile.write('class CfgSoundSets {\n' )
scriptfile.write('	class Mods_SoundSet_Base;\n' )
for u in tqdm(range(len(oggfiles))):
        scriptfile.write(f'	class {NameOfAddon}_SoundSet_{syntaxofsymbols(oggfiles[u])}\n' )
        scriptfile.write('  {\n' )
        scriptfile.write('		soundShaders[]=\n' )
        scriptfile.write('		{\n' )
        scriptfile.write(f'			"{NameOfAddon}_{syntaxofsymbols(oggfiles[u])}_Shader"\n' )
        scriptfile.write('		};\n' )
        scriptfile.write('	};\n' )
print("ГОТОВО!")
print("Создание class CfgSoundShaders")
scriptfile.write('class CfgSoundShaders {\n' )
scriptfile.write('	class YK_Cassette_SoundShader_Base;\n' )
for t in tqdm(range(len(oggfiles))):
        scriptfile.write(f'	class {NameOfAddon}_{syntaxofsymbols(oggfiles[t])}_Shader: YK_Cassette_SoundShader_Base\n' )
        scriptfile.write('  {\n' )
        scriptfile.write('		samples[]=\n' )
        scriptfile.write('		{\n' )
        scriptfile.write('			{\n' )
        scriptfile.write(f'				"{NameOfAddon}\Cassettes\sounds\{syntaxofsymbols(oggfiles[t])}.ogg",\n' )
        scriptfile.write('				1\n' )
        scriptfile.write('			}\n' )
        scriptfile.write('		};\n' )
        scriptfile.write('	};\n' )
f = open(f"output\\{NameOfAddon}_types.xml", "w", encoding='utf8')
for t in tqdm(range(len(oggfiles))):
        xmlstrpart = f'''   <type name="{NameOfAddon}_{syntaxofsymbols(oggfiles[t])}">
            <nominal>2</nominal>
            <lifetime>21600</lifetime>
            <restock>7200</restock>
            <min>1</min>
            <quantmin>-1</quantmin>
            <quantmax>-1</quantmax>
            <cost>100</cost>
            <flags count_in_cargo="0" count_in_hoarder="0" count_in_map="1" count_in_player="0" crafted="0" deloot="0"/>
            <category name="tools"/>
            <tag name="shelves"/>
            <usage name="Town"/>
            <usage name="Village"/>
            <usage name="School"/>
        </type>'''
        f.write(xmlstrpart)
f.close()
scriptfile.close() 

Шаг 4: Боль

Тут возникла проблема: как мне проверить работоспособность программы, если dayztools поломан? И в этом мне помогла программа PackPBO, которая каким-то магическим образом паковала .pbo файлы без ошибок и проблем.

Найдя github этой проги, я посмотрел её код и обнаружил, то что она просто запускает AddonBuilder, командой через консоль windows. Сказать то что я выпил чаю — это ничего не сказать. Ну, махнув рукой я начал переносить код C++ в Python (собственно, то почему этот пункт называется боль :)). Главная проблема была в том, что в очень по сути простой программе, разработчик решил многократно усложнить её добавив кучу функций, которые вызываются один раз за весь код, без которых можно было обойтись.

И так вышел такой алгоритм:

  • создаём конфигурационный файл исключений
  • меняем рабочую директорию на директорию dayztools
  • выполняем команду
  • меняем директорию обратно

Вышел такой код:

print(f"Запоковка {NameOfAddon} в PBO")
debug(f"Запоковка {NameOfAddon} в PBO")
debug(f"dayztooldir - {dayztooldir}")
includefile = open(f"output\\{NameOfAddon}_include.txt", "w", encoding='utf8')
includefile.write("*.emat;*.edds;*.ptc;*.c;*.imageset;*.layout;*.ogg;*.paa;*.rvmat;")
includefile.close()
debug(f'chdir "{dayztooldir}\\Bin\\AddonBuilder\\"')
os.chdir(f"{dayztooldir}\\Bin\\AddonBuilder")
debug(f'AddonBuilder.exe "{WORKDIR}output\\{NameOfAddon}" "{WORKDIR}output\\{NameOfAddon}" -clear -include="{WORKDIR}output\\{NameOfAddon}_include.txt"')
os.system(f'AddonBuilder.exe "{WORKDIR}output\\{NameOfAddon}" "{WORKDIR}output\\{NameOfAddon}" -clear -include="{WORKDIR}output\\{NameOfAddon}_include.txt"')
os.chdir(WORKDIR)

Значения Workdir, обозначаем так:

WORKDIR = os.path.abspath(__file__).replace(os.path.basename(__file__), '')

А вот dayztoolsdir — интереснее, я решил также запихнуть конфиг, в который мы будем пихать, то что хотим сохранить. Ну и создаём аргумент -dtdir, данные которые записываем в dayztooldir, и если мы так сделали, меняем DAYZTOOLSDIRARGV на True.
Все операции с конфигами делаю через библиотеку configparser.

if os.path.exists("config.ini"):
        configfile = configparser.ConfigParser()
        configfile.read('config.ini')
        debug(configfile)
        if DAYZTOOLSDIRARGV == False:
            dayztooldir = configfile["MAIN"]["dayztoolsdir"]
            debug(dayztooldir)
        if  not(os.path.exists(f"{dayztooldir}\\Bin\\AddonBuilder\\AddonBuilder.exe") and os.path.exists(f"{dayztooldir}\\Bin\\DsUtils\\DSCreateKey.exe") and os.path.exists(f"{dayztooldir}\\Bin\\DsUtils\\DSSignFile.exe") and os.path.exists(f"{dayztooldir}\\Bin\\ImageToPAA\\ImageToPAA.exe")):
            print(f"файлы dayztools не найдены или повреждены, пожалуйста введите новой путь к файлам DayZTools")
            debug(f"{dayztooldir}\\Bin\\AddonBuilder\\AddonBuilder.exe - " + str(os.path.exists(f'{dayztooldir}\\Bin\\AddonBuilder\\AddonBuilder.exe')))
            debug(f"{dayztooldir}\\Bin\\DsUtils\\DSCreateKey.exe - " + str(os.path.exists(f'{dayztooldir}\\Bin\\DsUtils\\DSCreateKey.exe')))
            debug(f"{dayztooldir}\\Bin\\DsUtils\\DSSignFile.exe - " + str(os.path.exists(f'{dayztooldir}\\Bin\\DsUtils\\DSSignFile.exe')))
            debug(f"{dayztooldir}\\Bin\\DsUtils\\DSSignFile.exe - " + str(os.path.exists(f'{dayztooldir}\\Bin\\DsUtils\\DSSignFile.exe')))
            debug(f"{dayztooldir}\\Bin\\ImageToPAA\\ImageToPAA.exe - " + str(os.path.exists(f'{dayztooldir}\\Bin\\ImageToPAA\\ImageToPAA.exe')))
            config = configparser.ConfigParser()
            while True:
                dayztooldir = str(input("Путь к папке Dayz Tools >>>"))
                debug(f"{dayztooldir}\\Bin\\AddonBuilder\\AddonBuilder.exe - " + os.path.exists(f'{dayztooldir}\\Bin\\AddonBuilder\\AddonBuilder.exe'))
                debug(f"{dayztooldir}\\Bin\\DsUtils\\DSCreateKey.exe - " + os.path.exists(f'{dayztooldir}\\Bin\\DsUtils\\DSCreateKey.exe'))
                debug(f"{dayztooldir}\\Bin\\DsUtils\\DSSignFile.exe - " + os.path.exists(f'{dayztooldir}\\Bin\\DsUtils\\DSSignFile.exe'))
                debug(f"{dayztooldir}\\Bin\\DsUtils\\DSSignFile.exe - " + os.path.exists(f'{dayztooldir}\\Bin\\DsUtils\\DSSignFile.exe'))
                debug(f"{dayztooldir}\\Bin\\ImageToPAA\\ImageToPAA.exe - " + os.path.exists(f'{dayztooldir}\\Bin\\ImageToPAA\\ImageToPAA.exe'))
                if  os.path.exists(f"{dayztooldir}\\Bin\\AddonBuilder\\AddonBuilder.exe") and os.path.exists(f"{dayztooldir}\\Bin\\DsUtils\\DSCreateKey.exe") and os.path.exists(f"{dayztooldir}\\Bin\\DsUtils\\DSSignFile.exe") and os.path.exists(f"{dayztooldir}\\Bin\\ImageToPAA\\ImageToPAA.exe"):
                    config['MAIN'] = {'dayztoolsdir': dayztooldir}
                    with open('config.ini', 'w') as configfile:
                        config.write(configfile)
                        break
                else:
                    print(f"файлы dayztools не найдены или повреждены, пожалуйста повторите попытку")
    else:
        config = configparser.ConfigParser()
        while True:
            dayztooldir = str(input("Путь к папке Dayz Tools >>>"))
            if  os.path.exists(f"{dayztooldir}\\Bin\\AddonBuilder\\AddonBuilder.exe") and os.path.exists(f"{dayztooldir}\\Bin\\DsUtils\\DSCreateKey.exe") and os.path.exists(f"{dayztooldir}\\Bin\\DsUtils\\DSSignFile.exe") and os.path.exists(f"{dayztooldir}\\Bin\\ImageToPAA\\ImageToPAA.exe"):
                config['MAIN'] = {'dayztoolsdir': dayztooldir}
                with open('config.ini', 'w') as configfile:
                    config.write(configfile)
                    break
            else:
                print(f"файлы dayztools не найдены или повреждены, пожалуйста повторите попытку")

Но кажется раз уж у меня готов .pbo аддон, и папки все созданы, то что ещё нужно? ПРАВИЛЬНО! Ключи!

Для модификаций в arma и dayz должны быть созданы специальные электронные ключи (Я не знаю зачем, но надо). Без которых модификация не будет восприниматься сервером. И тут я уже писал код с нуля… Пью чай~…

НО! Спасибо Bohemia, за документацию! На основании данных о программе DsUtils и экспериментируя с ней через консоль, я смог написать код, который автоматически создаёт ключ, регистрирует аддон, а после всё подчищает.

Но для идентификации, я решил создавать специальные ID, с помощью такого не большого алгоритма:

    UnikID = ""
    for x in range(5):UnikID = UnikID + str(int(time.time()))[random.randint(0, len(str(int(time.time()))) - 1)]
    UnikID = str(hex(int(UnikID)))[2:]

Здесь, я беру 5 рандомных цифр взятых из значения времени на ПК (в сек.), а после перевожу это в hex и убираю первых 2 символа.

И по итогу вышел такой код для регистрации ключей:

    debug("Создание директорий") 
    os.remove(f"output\\{NameOfAddon}_{UnikID}_include.txt")
    os.mkdir(f"output\\@{NameOfAddon}")
    os.mkdir(f"output\\@{NameOfAddon}\\Keys")
    os.mkdir(f"output\\@{NameOfAddon}\\Addons")

    debug("Перемещение файлов")
    shutil.move(f"output\\{NameOfAddon}_{UnikID}_types.xml", f"output\\@{NameOfAddon}")
    shutil.move(f"output\\{NameOfAddon}\\{NameOfAddon}.pbo", f"output\\@{NameOfAddon}\\Addons")
    shutil.rmtree(f"output\\{NameOfAddon}")

    debug("Создание ключа")
    os.chdir(f"{dayztooldir}\\Bin\\DsUtils")
    os.system(f'DSCreateKey.exe {UnikID}')
    shutil.copy(f"{dayztooldir}\\Bin\\DsUtils\\{UnikID}.bikey", f"{WORKDIR}output\\@{NameOfAddon}\\Keys")

    debug("Присвоение ключа")
    os.system(f'DSSignFile.exe {UnikID}.biprivatekey "{WORKDIR}output\\@{NameOfAddon}\\Addons\\{NameOfAddon}.pbo"')
    if os.path.exists(f"{dayztooldir}\\Bin\\DsUtils\\{UnikID}.bikey"):
        debug(f"os.remove(f'{dayztooldir}\\Bin\\DsUtils\\{UnikID}.bikey')")
        os.remove(f"{dayztooldir}\\Bin\\DsUtils\\{UnikID}.bikey")
    if os.path.exists(f"{dayztooldir}\\Bin\\DsUtils\\{UnikID}.biprivatekey"):
        debug(f"os.remove(f'{dayztooldir}\\Bin\\DsUtils\\{UnikID}.biprivatekey')")
        os.remove(f"{dayztooldir}\\Bin\\DsUtils\\{UnikID}.biprivatekey")

Шаг 5: Конец не всегда бывает простым

Я со спокойной душой начал дополнять код:

1. Логи! Я дополнил функцию debug и добавил аргумент -logging.

def debug(text, status = 0):
    global DEBUG
    global LOGGING
    if LOGGING == True:
        global logfile
        match status:
            case 0: state = 'fine '
            case 1: state = 'warn '
            case 2: state = 'error'
        logfile.write(f"[{time.strftime('%H:%M:%S', time.localtime(time.time()))} {state}] {text} \n")
    if DEBUG == True:
        match status:
            case 0: rich.print(f"[{time.strftime('%H:%M:%S', time.localtime(time.time()))}] {text}")
            case 1: rich.print(f"[{time.strftime('%H:%M:%S', time.localtime(time.time()))}][bold][yellow] {text}[/bold][/yellow]")
            case 2: rich.print(f"[{time.strftime('%H:%M:%S', time.localtime(time.time()))}][bold][yellow] {text}[/bold][/yellow]")

2. Режимы ввода информации. Теперь ввод может быть как заглушками, так и рукописным текстом или по meta* данным mp3 файлов.

Выбор режима:

    print("Режим ввода информации")
    print('1. Автоматическое')
    print("2. Ручное")
    print("3. Данные из mp3")
    print("4. С помощью конфига") # пока не реализовал :)
    print(" ")
    if ARGVMODE == False: # ARGVMODE - аргумент 
        Mode = str(input("Режим ввода >>> ")) 
    if Mode != "1" and Mode != "2" and Mode != "3" and Mode != "4":
        print("Режим ввода некорректен! Автоматический режим установлен")
        Mode = "1"
    os.system("cls")

Чтение meta данных происходит через библиотеку eye3d:

if Mode == "3":
                audiofile = eyed3.load(f"input\\{oldfilename}.mp3")
                title = audiofile.tag.title
                album = audiofile.tag.album
                artist = audiofile.tag.artist
                debug(f'''title = {title}
                    album = {album}
                    artist = {artist}
                    ''', 0)
                title = title.replace("/", ", ")
                album = album.replace("/", ", ")
                artist = artist.replace("/", ", ")
                debug(f'''title = {title}
                    album = {album}
                    artist = {artist}
                    ''', 0)
                title = transliterate.translit(title, language_code='ru', reversed=True)
                getVals = list([val for val in title
                            if symboltest(val) == True])
                title = "".join(getVals)
                album = transliterate.translit(album, language_code='ru', reversed=True)
                getVals = list([val for val in album
                            if symboltest(val) == True])
                album = "".join(getVals)
                artist = transliterate.translit(artist, language_code='ru', reversed=True)
                getVals = list([val for val in artist
                            if symboltest(val) == True])
                artist = "".join(getVals)
                debug(f'''title = {title}
                    album = {album}
                    artist = {artist}
                    ''', 0)
                metadata.append([title, artist, album])
                debug(f"metadata - {metadata}"

И тут произошло неожиданное, я обнаружил, что этот чайник Yuki обновил свою модификацию и добавил возможность создавать музыкальные альбомы! И тут я, включая режим садомазохиста, продолжил писать код дальше, добавив такие куски кода:

elif os.path.isdir(f"input\\{files[i]}"):
            filesinplaylist = os.listdir(f"input\\{files[i]}")
            debug(f"filesinplaylist = {filesinplaylist}")
            print(f"Создание Playlist {files[i]}")
            toplaylist = []
            for t in tqdm(range(len(filesinplaylist))):
                    if filesinplaylist[t].endswith('.mp3'):
                        filename = filesinplaylist[t].removesuffix('.mp3')
                        debug(f"filename - {filename}")
                        oldfilename = filename
                        filename = transliterate.translit(filename, language_code='ru', reversed=True)
                        getVals = list([val for val in filename
                                    if symboltest(val) == True])
                        filename = "".join(getVals)
                        debug(f"filename - {filename}")
                        if not(os.path.exists(f"input\\{syntaxofsymbols(filename)}.mp3")):
                            os.rename(f"input\\{oldfilename}.mp3", f"input\\{syntaxofsymbols(filename)}.mp3")
                        else:
                            debug(f"Найден повторяющийся файл {syntaxofsymbols(filename)}.mp3", 1)
                            os.remove(f"input\\{syntaxofsymbols(filename)}.mp3")
                        if os.path.isfile(f"output\\{NameOfAddon}\\Cassettes\\sounds\\{syntaxofsymbols(filename)}_playlist{files[i]}.ogg") == False:   
                            mp3toogg(f"input\\{files[i]}\\{syntaxofsymbols(filename)}.mp3", f"output\\{NameOfAddon}\\Cassettes\\sounds\\{syntaxofsymbols(filename)}_playlist{files[i]}.ogg")
                            toplaylist.append(f"{syntaxofsymbols(filename)}_playlist{files[i]}")
                            debug(f"toplaylist - {toplaylist}")
                        else:
                            debug(f'os.path.isfile(f"output\\{NameOfAddon}\\Cassettes\\sounds\\{syntaxofsymbols(filename)}_playlist{files[i]}.ogg") == True', 2)
                            error = True
                    elif filesinplaylist[t].endswith('.ogg'):
                        filename = filesinplaylist[t].removesuffix('.ogg')
                        debug(f"filename - {filename}")
                        oldfilename = filename
                        filename = transliterate.translit(filename, language_code='ru', reversed=True)
                        getVals = list([val for val in filename
                                    if symboltest(val) == True])
                        filename = "".join(getVals)
                        debug(f"filename - {filename}")
                        if not(os.path.exists(f"input\\{syntaxofsymbols(filename)}.ogg")):
                            os.rename(f"input\\{oldfilename}.ogg", f"input\\{syntaxofsymbols(filename)}.ogg")
                        else:
                            debug(f"Найден повторяющийся файл {syntaxofsymbols(filename)}.ogg", 1)
                        if os.path.isfile(f"output\\{NameOfAddon}\\Cassettes\\sounds\\{syntaxofsymbols(filename)}_playlist{files[i]}.ogg") == False:   
                            shutil.move(f"input\\{files[i]}\\{syntaxofsymbols(filename)}_playlist{files[i]}.ogg", f"output\\{NameOfAddon}\\Cassettes\\sounds\\")
                            toplaylist.append(f"{syntaxofsymbols(filename)}_playlist{files[i]}")
                            debug(f"toplaylist - {toplaylist}")
                        else:
                            os.remove(f"input\\{files[i]}\\{syntaxofsymbols(filename)}_playlist{files[i]}.ogg")
                            debug(f'os.path.isfile(f"output\\{NameOfAddon}\\Cassettes\\sounds\\{syntaxofsymbols(filename)}_playlist{files[i]}.ogg") == True', 2)
                            error = True
            playlists.append(dict(Name=f"{files[i]}", Playlist=toplaylist))
            debug(f"playlists - {playlists}")
            os.rmdir(f"input\\{files[i]}")

if len(playlists) > 0:
        for p in range(len(playlists)):
            scriptfile.write(f'    class {NameOfAddon}_{playlists[p].get("Name")}_Collection: YK_Cassette_Base\n' )
            scriptfile.write('        {\n' )
            scriptfile.write('        scope=2;\n')
            scriptfile.write(f'        displayName="{playlists[p].get("Name")} Collection";\n' )
            scriptfile.write(f'        descriptionShort=" ";\n' )
            scriptfile.write("        hiddenSelectionsTextures[]=\n" )
            scriptfile.write("        {\n" )
            scriptfile.write('            "YK_Radio\Cassettes\Clear\data\cassette_co.paa"\n' )
            scriptfile.write("        };\n" )
            scriptfile.write("        class CfgCassette\n" )
            scriptfile.write("        {\n" )
            scriptfile.write("             isPlaylist=1;\n" )
            scriptfile.write('             soundSets[]=\n' )
            scriptfile.write("             {\n" )
            for k in range(len(playlists[p].get("Playlist"))):
                if k == len(playlists[p].get("Playlist")) - 1:
                    scriptfile.write(f'            "{NameOfAddon}_SoundSet_{playlists[p].get("Name")}_{playlists[p].get("Playlist")[k]}"\n' )
                else:
                    scriptfile.write(f'            "{NameOfAddon}_SoundSet_{playlists[p].get("Name")}_{playlists[p].get("Playlist")[k]}",\n' )
            scriptfile.write("             };\n" )
            scriptfile.write("        };\n" )
            scriptfile.write("    };\n" )


if len(playlists) > 0:
        for b in tqdm(range(len(playlists))):
            for h in tqdm(range(len(playlists[b].get("Playlist")))):
                scriptfile.write(f'	class {NameOfAddon}_SoundSet_{playlists[b].get("Name")}_{playlists[b].get("Playlist")[h]}\n' )
                scriptfile.write('  {\n' )
                scriptfile.write('		soundShaders[]=\n' )
                scriptfile.write('		{\n' )
                scriptfile.write(f'			"{NameOfAddon}_{playlists[b].get("Name")}_{playlists[b].get("Playlist")[h]}_Shader"\n' )
                scriptfile.write('		};\n' )
                scriptfile.write('	};\n' )
    scriptfile.write('};\n' )


if len(playlists) > 0:
        for s in tqdm(range(len(playlists))):
            for a in tqdm(range(len(playlists[s].get("Playlist")))):
                scriptfile.write(f'	class {NameOfAddon}_{playlists[s].get("Name")}_{playlists[s].get("Playlist")[a]}_Shader: YK_Cassette_SoundShader_Base\n' )
                scriptfile.write('  {\n' )
                scriptfile.write('		samples[]=\n' )
                scriptfile.write('		{\n' )
                scriptfile.write('			{\n' )
                scriptfile.write(f'				"{NameOfAddon}\Cassettes\sounds\{playlists[s].get("Playlist")[a]}.ogg",\n' )
                scriptfile.write('				1\n' )
                scriptfile.write('			}\n' )
                scriptfile.write('		};\n' )
                scriptfile.write('	};\n' )
    scriptfile.write('};\n' )

for e in tqdm(range(len(playlists))):
            xmlstrpart = f'''   <type name="{NameOfAddon}_{playlists[e].get("Name")}_Collection">
            <nominal>2</nominal>
            <lifetime>21600</lifetime>
            <restock>7200</restock>
            <min>1</min>
            <quantmin>-1</quantmin>
            <quantmax>-1</quantmax>
            <cost>100</cost>
            <flags count_in_cargo="0" count_in_hoarder="0" count_in_map="1" count_in_player="0" crafted="0" deloot="0"/>
            <category name="tools"/>
            <tag name="shelves"/>
            <usage name="Town"/>
            <usage name="Village"/>
            <usage name="School"/>
            </type>'''
            f.write(xmlstrpart)

На это ушла куча времени, но была вещь, занимающая ещё больше времени — конвертация! Конвертирование одного файла длилось около 6 сек. И ладно, когда файлов 10. А если 100? Или 1000? А что, если ещё больше? Поэтому я решил кидать обработку медиа файлов на отдельные ядра, дабы это проходило, хоть капельку быстрее.

Единственной подходящей библиотекой оказалось multiprocessing, поскольку threading, хоть и создаёт параллельные процессы, но обрабатываются они одним ядром.

Для проверки функции этой библиотеки я написал отдельную программу:

import multiprocessing
import time
from tqdm import tqdm

MULTICPU = False
FORSICLES = 200

def task(x, placeholder):
    for u in range(FORSICLES):
        hex((u * x) ** (u * x))
            
if __name__ == "__main__" and MULTICPU == True:
        start_time = time.process_time()
        jobs = []
        for x in tqdm(range(FORSICLES)):
            p = multiprocessing.Process(target=task, args=(5 * x, None))
            jobs.append(p)
            p.start()
        for job in tqdm(jobs):
            job.join()
        print("MULTICORE")
        print(f"--- {(time.process_time() - start_time)} seconds ---")
        
if __name__ == "__main__" and MULTICPU == False:
        start_time = time.process_time()
        jobs = []
        for x in tqdm(range(FORSICLES)):
            for y in range(FORSICLES):
                hex((y * x) ** (y * x))
        print("SINGCORE                                                                           ")
        print(f"--- {(time.process_time() - start_time)} seconds ---")

Теперь подробнее:

Для создания параллельных процессов, я использую эту строчку: p = multiprocessing.Process (target=task, args=(5 * x, None)), а после создания процесса, сохраняю его в лист и запускаю: jobs.append (p), p.start ()

После запуска всех процессов, я делаю задержку до тех пор, пока все процессы не будут выполнены:

for job in tqdm(jobs):
            job.join()

Сравнение скорости выполнения кода:

Стоит учесть, что программа рассчитывает то, сколько времени занял алгоритм, без учёта зависаний :).

После тестирования, данной функции. Я вписал её в программу практически в голом ввиде:

process = []

if os.path.isfile(f"output\\{NameOfAddon}\\Cassettes\\sounds\\{syntaxofsymbols(filename)}.ogg") == False:                
    p = multiprocessing.Process(target=mp3toogg, args = (f"input\\{syntaxofsymbols(filename)}.mp3", f"output\\{NameOfAddon}\\Cassettes\\sounds\\{syntaxofsymbols(filename)}.ogg"))
    process.append(p)
    p.start()

for dp in process:
        dp.join()

Шаг 6: Завершение.

И на данном этапе, я считаю, что программа готова, для выпуска в релиз. Однако ещё есть функционал, который я хочу добавить.
На данный момент вы можете найти прошлую версию моей программы на github (в скором времени будет залита новая версия), в которой нет альбомов, запаковки в .pbo, обработки meta данных и т. д.

Надеюсь, что данный блог помог разобраться кому-нибудь с тем, как работают те или иные функции, как делать параллельные процессы и аргументы запуска и т. д.

Спасибо fantom228808 за помощь в разработке прототипа и тестирование.
Спасибо NF за разъяснения по поводу альбомов.
Спасибо Jens Heukers за разработку PackPBO, который я использовал в качестве примера.
И спасибо всем читателям за прочтение!

Программа: DayZRadioAddonCreator

* Деятельность организации «Meta» признана экстремистской и запрещена на территории Российской Федерации


DayZ

Платформы
PC | PS4 | PS5 | XONE | XBOXSX
Жанр
Дата выхода
13 декабря 2018
670
3.3
315 оценок
Моя оценка

Лучшие комментарии

Спасибо за вашу работу. А есть возможность сделать видео запись как должно сие чудо работать для «умных» :)). потому как я все вроде бы установил (ffmpeg,dayztools,piton 311) и после Run module я получаю «Traceback (most recent call last)» и потом много красного текста :(

Хмм… Странно, лучше обратись в личные сообщение, ко мне я отправлю методы как связаться со мной, либо же напиши полный вывод программы т.к. ошибка Traceback, может быть связана вообще с чем угодно и нужно читать, именно описание ошибки.

Очень годная статья, особенно, что ты объяснешь дополнения своего кода. Крутая работа, желаю дальнейшей удачи в модах.

Читай также