Предисловие
В этом блоге я буду рассказывать о том как я делал программу для создания модификаций — аддонов на мод 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» признана экстремистской и запрещена на территории Российской Федерации
Лучшие комментарии
Спасибо за вашу работу. А есть возможность сделать видео запись как должно сие чудо работать для «умных» :)). потому как я все вроде бы установил (ffmpeg,dayztools,piton 311) и после Run module я получаю «Traceback (most recent call last)» и потом много красного текста :(
Хмм… Странно, лучше обратись в личные сообщение, ко мне я отправлю методы как связаться со мной, либо же напиши полный вывод программы т.к. ошибка Traceback, может быть связана вообще с чем угодно и нужно читать, именно описание ошибки.
Ну я такого не помню
Кста, зацени новую версию проги, я там полноценный редактор на PyQT замутил, только уже для FoX
В этом радио от Yuki на сколько я помню лежал скрипт для Баша который даже создавал конфиг
Очень годная статья, особенно, что ты объяснешь дополнения своего кода. Крутая работа, желаю дальнейшей удачи в модах.