Compare commits

...

29 Commits

Author SHA1 Message Date
Jaka Perovšek e03c67245f cleaned up imports 2024-10-18 01:19:16 +02:00
Jaka Perovšek 3743dfbdaa mostly cosmetic 2024-10-18 01:13:14 +02:00
Jaka Perovšek fd0e5ddc3c logging, threading and file watchers, finally it's going well. 2024-10-18 01:05:39 +02:00
Jaka Perovšek 841fd1cbfa logging to widget works! 2024-10-13 23:23:12 +02:00
Jaka Perovšek d6bcdb9ecc moving the deployment functions into a class to avoid juggling dirs as arguments 2024-10-13 20:30:51 +02:00
Jaka Perovšek a15c9bc220 async process + loggging 2024-10-05 20:31:22 +02:00
Jaka Perovšek 24279524c7 Added console widget, removed the debug dialog, some refactoring 2024-10-05 16:52:21 +02:00
Jaka Perovšek c31e2f2b88 function to check if all the songs are there - temporary 2024-10-05 16:33:45 +02:00
Jaka Perovšek ccf1b2b53f fixed the probability spinbox look and refactored the code slightly 2024-10-05 12:35:13 +02:00
Jaka Perovšek e489737f5f Exception for loading from file caught too 2024-10-05 12:21:52 +02:00
Jaka Perovšek ed9834ea37 cosmetic 2024-10-05 12:17:59 +02:00
Jaka Perovšek 4814eacbad caught an error when applying the yaml code and displey it in a debug dialog 2024-10-05 12:17:48 +02:00
Jaka Perovšek 3036478f1d applying yaml code to widget function works now 2024-10-05 11:41:07 +02:00
Jaka Perovšek a3e6c32ae9 Auto find barotrauma install path function 2024-10-04 00:40:16 +02:00
Jaka Perovšek e8ffff4739 Label Link widget instead of setting every label individually 2024-10-03 18:09:49 +02:00
Jaka Perovšek bdc9270ba8 update of .gitignore 2024-10-03 15:36:23 +02:00
Jaka Perovšek ad974b3573 Replaced dirs in deploy with variables instead of hardcoded ones. Separated the update and install parts in the gui. Started assembling the options functionality. 2024-10-03 15:36:09 +02:00
Jaka Perovšek cc94838ad3 fixed the function for different kinds of paths 2024-09-28 00:59:19 +02:00
Jaka Perovšek a40fd85822 set dir working 2024-09-28 00:54:48 +02:00
Jaka Perovšek 73ae09c438 Made the main directory the working directory, some refactoring, added open folder function to the dir widget 2024-09-28 00:28:45 +02:00
Jaka Perovšek 66cda77233 Download ffmpeg basic functionality done. Custom folders to be implemented 2024-05-11 00:12:37 +02:00
Jaka Perovšek 93d7e0dce2 Local mod name based on username 2024-05-10 22:30:08 +02:00
Jaka Perovšek 2f6b1a8a33 Added detect screen resolution function 2024-05-10 22:22:48 +02:00
Jaka Perovšek 2e68a5eaf2 Some work on time clipping, but I'm not sure if it works because it was a long time since I did that. 2024-05-10 22:12:22 +02:00
Jaka Perovšek 6981f710be Moved the tape editor widgets to another file 2023-12-15 23:39:35 +01:00
Jaka Perovšek 1fce5f2d69 Generating code from a validator + saving file 2023-12-15 22:32:19 +01:00
Jaka Perovšek ca878de953 new installer work in progress 2023-12-15 21:12:33 +01:00
Jaka Perovšek fa6e4a7316 hardcode ffmpeg version because the function returned errors 2023-12-15 20:44:29 +01:00
Jaka Perovšek 906f3c073c cosmetic xml 2023-12-15 20:44:10 +01:00
55 changed files with 2922 additions and 605 deletions

2
.gitignore vendored
View File

@ -9,3 +9,5 @@ __pycache__
utils utils
barotrauma-sunken-tapes barotrauma-sunken-tapes
barotrauma-sunken-tapes.zip barotrauma-sunken-tapes.zip
brainstorming.svg
poetry.lock

View File

@ -92,7 +92,7 @@ def get_ffmpeg_version():
ffmpeg_version = fp.text ffmpeg_version = fp.text
del fp del fp
return ffmpeg_version return "6.0"
def fetch_and_cut_song(tape, ffmpeg_version): def fetch_and_cut_song(tape, ffmpeg_version):

309
gui/deploy.py 100644
View File

@ -0,0 +1,309 @@
import jinja2 as j2
import yaml
import shutil
import requests
import os
import os.path
import sys
import time
import logging
import certifi
import math
import vdf
from PIL import Image
from mutagen.oggvorbis import OggVorbis
from distutils.dir_util import copy_tree
from pathlib import Path
from subprocess import PIPE, STDOUT, CalledProcessError, CompletedProcess, Popen
def rmfulldir(dirpath):
try:
shutil.rmtree(dirpath)
except FileNotFoundError:
pass
def download_via_requests(url_source, file_name):
response = requests.get(url_source, stream=True, verify=certifi.where())
with open(file_name, 'wb') as out_file:
shutil.copyfileobj(response.raw, out_file)
del response
def get_ffmpeg_version():
url_ffmpeg_version = "https://www.gyan.dev/ffmpeg/builds/release-version"
fp = requests.get(url_ffmpeg_version, verify=certifi.where())
ffmpeg_version = fp.text
del fp
return ffmpeg_version
def stream_command(args, *, stdout_handler=logging.info, check=True, text=True,
stdout=PIPE, stderr=STDOUT, **kwargs):
"""Mimic subprocess.run, while processing the command output in real time."""
with Popen(args, text=text, stdout=stdout, stderr=stderr, **kwargs) as process:
for line in process.stdout:
stdout_handler(line[:-1])
return_code = process.poll()
if check and return_code:
raise CalledProcessError(return_code, process.args)
return CompletedProcess(process.args, return_code)
class Deployer:
def __init__(self, logging_handler: logging.Handler = None):
logging.basicConfig(stream=sys.stdout, level=logging.INFO,
format='[%(asctime)s| %(levelname)s| %(processName)s] %(message)s')
self.logger = logging.getLogger()
if logging_handler is not None:
self.logger.addHandler(logging_handler)
self.utils_dir = "./utils"
self.build_dir = "./build"
self.music_dir = "./build/music"
self.source_dir = "./source"
self.install_dir = self.find_install_dir()
def find_install_dir(self):
self.logger.info("Trying to find Barotraumma install folder")
library_folders = Path("C:\Program Files (x86)\Steam\steamapps\libraryfolders.vdf")
game_id = "602960"
data = vdf.load(open(library_folders))
install_path = ""
for (id, folder) in data["libraryfolders"].items():
if game_id in folder["apps"]:
install_path = Path(folder["path"]) / Path("steamapps\common\Barotrauma")
self.logger.info(f"Barotrauma is installed in {install_path.as_posix()}")
return install_path.as_posix()
def download_ffmpeg(self, clean=False):
if clean:
rmfulldir(f"{self.utils_dir}/ffmpeg-" + get_ffmpeg_version() + "-full_build")
url_ffmpeg_source = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z"
out_ffmpeg_archive = f"{self.utils_dir}/ffmpeg-release-full.7z.exe"
self.download_and_extract(url_ffmpeg_source, out_ffmpeg_archive)
def download_git(self, clean=False):
if clean:
rmfulldir(f"{self.utils_dir}/git")
url_git_source = ("https://github.com/git-for-windows/git/releases/download/"
"v2.46.2.windows.1/PortableGit-2.46.2-64-bit.7z.exe")
out_git_archive = f"{self.utils_dir}/PortableGit-2.46.2-64-bit.7z.exe"
self.download_and_extract(url_git_source, out_git_archive, subdir="/git")
def download_and_extract(self, url_source, out_archive, subdir=""):
self.logger.info(f"Downloading {url_source}. This may take a while.")
download_via_requests(url_source, out_archive)
time.sleep(0.7)
self.logger.info("Download complete.")
self.logger.info(f"Extracting {out_archive} with 7z")
extract = [f"{self.utils_dir}/7z/7za.exe", "x", out_archive, "-o" f"{self.utils_dir}{subdir}"]
stream_command(extract, stdout_handler=self.logger.info)
time.sleep(0.7)
self.logger.info("Removing " + out_archive)
os.remove(out_archive)
def update(self):
self.logger.info(f"Checking for updates via git pull.")
pull = [f"{self.utils_dir}/git/bin/git.exe", "pull"]
stream_command(pull, stdout_handler=self.logger.info)
def fetch_and_cut_song(self, tape, ffmpeg_version):
python_executable = f"{self.utils_dir}/python-3.9.7-embed-amd64/python.exe"
script = "./fetch_song.py"
if type(tape["source"]) == str:
shutil.copy(tape["source"], f"{self.music_dir}/{tape['identifier']}.ogg")
else:
fetch = [python_executable, script, tape["source"]["url"], "-x", "--audio-format", "vorbis",
"--audio-quality", "0", "-o", f"{self.build_dir}/tmp_music/{tape['identifier']}.ogg"]
# this is done in a separate python script because subprocess.call makes sure that the
# download process is finished before trying to cut the song
stream_command(fetch, stdout_handler=self.logger.info)
time.sleep(0.1)
cut = [f"{self.utils_dir}/ffmpeg-" + ffmpeg_version + "-full_build/bin/ffmpeg.exe",
"-y", "-ss", f"{tape['source']['start']}",
"-i", f"{self.build_dir}/tmp_music/{tape['identifier']}.ogg", "-acodec", "libvorbis",
"-ac", "1", "-af", f"volume={tape['source']['volume']}dB",
f"{self.music_dir}/{tape['identifier']}.ogg"]
if tape["source"]["end"] != -1:
cut = cut[:-7] + ["-to", f"{tape['source']['end']}"] + cut[-7:]
try:
stream_command(cut, stdout_handler=self.logger.info)
except FileNotFoundError:
self.logger.error("ffmpeg not in utils directory.")
return
walkman = [f"{self.utils_dir}/ffmpeg-" + ffmpeg_version + "-full_build/bin/ffmpeg.exe",
"-i", f"{self.music_dir}/{tape['identifier']}.ogg",
"-af", f"highpass=f=300,lowpass=f=12000",
f"{self.music_dir}/{tape['identifier']}-walkman.ogg"]
try:
stream_command(walkman, stdout_handler=self.logger.info)
except FileNotFoundError:
self.logger.error("ffmpeg not in utils directory.")
return
def assemble_png_images(self, tapes, outfile: str, resize=None):
img_names = [f"{self.source_dir}/images/{tape['identifier']}.png" for tape in tapes]
images = [Image.open(x) for x in img_names]
if resize is not None:
for i, (im, tape) in enumerate(zip(images, tapes)):
im.thumbnail((128, 82), Image.ANTIALIAS)
if resize == (64, 41) and tape["icon_resize"] == "blur":
im.thumbnail(resize, Image.ANTIALIAS)
else:
im.thumbnail(resize, Image.NEAREST)
images[i] = im
widths, heights = zip(*(i.size for i in images))
columns = int((len(images)) ** 0.5)
rows = int(math.ceil(len(images) / columns))
total_width = max(widths) * columns
max_height = max(heights) * rows
new_im = Image.new('RGBA', (total_width, max_height))
for i, im in enumerate(images):
x_offset = max(widths) * (i % columns)
y_offset = max(heights) * math.floor(i / columns)
new_im.paste(im, (x_offset, y_offset))
new_im.save(f"{self.build_dir}/{outfile}.png")
def prepare_music(self, data):
try:
os.mkdir(f'{self.music_dir}')
except FileExistsError:
pass
ffmpeg_version = get_ffmpeg_version()
self.logger.info("Downloading and cutting the songs.")
for i, tape in enumerate(data):
if not (os.path.exists(f"{self.music_dir}/{tape['identifier']}.ogg") or os.path.exists(
f"{self.music_dir}/{tape['identifier']}-walkman.ogg")):
self.logger.info(f"{i + 1}/{len(data)} Downloading: {tape['name']}")
self.fetch_and_cut_song(tape, ffmpeg_version)
else:
self.logger.info(f"{i + 1}/{len(data)} Already exists: {tape['name']}")
self.logger.info(f"Removing temporary music folder.")
rmfulldir(f"{self.build_dir}/tmp_music/")
self.logger.info(f"Copying the sound effects to build.")
copy_tree(f"{self.source_dir}/sound_effects", f"{self.build_dir}/sound_effects")
def prepare_images(self, data):
self.logger.info(f"Assembling covers and icons into png files.")
self.assemble_png_images(data, "covers")
self.assemble_png_images(data, "icons", resize=(64, 41))
self.assemble_png_images(data, "sprites", resize=(33, 21))
self.logger.info(f"Copying other images.")
shutil.copy(f"{self.source_dir}/images/players_icons.png", f"{self.build_dir}/players_icons.png")
shutil.copy(f"{self.source_dir}/images/players_sprites.png", f"{self.build_dir}/players_sprites.png")
shutil.copy(f"{self.source_dir}/images/PreviewImage.png", f"{self.build_dir}/PreviewImage.png")
def build_xml_code(self, data, config):
self.logger.info(f"Calculate the value that lets you use the songs n-times.")
song_lengths = [OggVorbis(f"{self.music_dir}/{tape['identifier']}.ogg").info.length
for tape in data]
use_lengths = [song_length * n for song_length, n in
zip(song_lengths, [tape["no_of_uses"] for tape in data])]
condition_delta = [f"{1 / use_length:0.5f}" for use_length in use_lengths]
affliction_delta = [100 / song_length for song_length in song_lengths]
columns = int((len(data)) ** 0.5)
positions = [{"column": i % columns, "row": math.floor(i / columns)} for i in range(len(data))]
self.logger.info(f"Creating jinja environment.")
# create jinja2 environment
j2env = j2.Environment(loader=j2.FileSystemLoader(Path(".")))
j2env.globals.update(zip=zip)
# load the template file
template0 = j2env.get_template(f"{self.source_dir}/filelist_template.xml")
template1 = j2env.get_template(f"{self.source_dir}/sunken_tapes_template.xml")
template2 = j2env.get_template(f"{self.source_dir}/sunken_tapes_style_template.xml")
self.logger.info(f"Rendering the xml files.")
with open(f"{self.build_dir}/filelist.xml", "w+", encoding="utf8") as output_file:
# render the template
output_file.write(template0.render(config=config, tapes=data))
with open(f"{self.build_dir}/{config['slug']}.xml", "w+", encoding="utf8") as output_file:
# render the template
output_file.write(template1.render(tapes=data, config=config,
condition_delta=condition_delta,
affliction_delta=affliction_delta,
song_lengths=song_lengths,
positions=positions))
with open(f"{self.build_dir}/{config['slug']}_style.xml", "w+", encoding="utf8") as output_file:
# render the template
output_file.write(template2.render(tapes=data, config=config, positions=positions))
def deploy(self, local_mod_name: str, config):
try:
os.mkdir(self.build_dir)
except FileExistsError:
self.logger.info(f"Removing old XML files in {self.build_dir}/:")
for f in Path(self.build_dir).glob("*.xml"):
self.logger.info(f" {f}")
os.remove(f)
pass
self.logger.info("Reading tapes.yaml")
data_file = Path(f"{self.source_dir}/tapes.yaml")
# load yaml file
with data_file.open(encoding='utf-8') as fr:
data = yaml.load(fr, Loader=yaml.SafeLoader)
self.prepare_music(data)
self.prepare_images(data)
self.build_xml_code(data, config)
mod_directory = f"{self.install_dir}/LocalMods/{local_mod_name}/"
self.logger.info(f"Removing the old installed mod directory {mod_directory}.")
rmfulldir(mod_directory)
self.logger.info(f"Copying the new build.")
if Path(f"{self.install_dir}").is_dir():
copy_tree(self.build_dir, mod_directory)
else:
raise FileNotFoundError(
f"{self.install_dir} does not exist. Set up the correct Barotrauma installation directory")
self.logger.info(f"Done!")

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M22 11V3h-7v3H9V3H2v8h7V8h2v10h4v3h7v-8h-7v3h-2V8h2v3z"/></svg>

After

Width:  |  Height:  |  Size: 192 B

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="24"
viewBox="0 0 24 24"
width="24"
version="1.1"
id="svg6"
sodipodi:docname="ic_arrow_left_alt_24px.svg"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
<metadata
id="metadata12">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs10" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="734"
inkscape:window-height="480"
id="namedview8"
showgrid="false"
inkscape:zoom="9.8333333"
inkscape:cx="12"
inkscape:cy="12"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="0"
inkscape:current-layer="svg6" />
<path
d="M0 0h24v24H0z"
fill="none"
id="path2" />
<path
d="M 7.99,13 H 20 V 11 H 7.99 V 8 L 4,12 7.99,16 Z"
id="path4"
inkscape:connector-curvature="0" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M16.01 11H4v2h12.01v3L20 12l-3.99-4z"/></svg>

After

Width:  |  Height:  |  Size: 174 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 6v3l4-4-4-4v3c-4.42 0-8 3.58-8 8 0 1.57.46 3.03 1.24 4.26L6.7 14.8c-.45-.83-.7-1.79-.7-2.8 0-3.31 2.69-6 6-6zm6.76 1.74L17.3 9.2c.44.84.7 1.79.7 2.8 0 3.31-2.69 6-6 6v-3l-4 4 4 4v-3c4.42 0 8-3.58 8-8 0-1.57-.46-3.03-1.24-4.26z"/></svg>

After

Width:  |  Height:  |  Size: 368 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path clip-rule="evenodd" d="M0 0h24v24H0z" fill="none"/><path d="M22.7 19l-9.1-9.1c.9-2.3.4-5-1.5-6.9-2-2-5-2.4-7.4-1.3L9 6 6 9 1.6 4.7C.4 7.1.9 10.1 2.9 12.1c1.9 1.9 4.6 2.4 6.9 1.5l9.1 9.1c.4.4 1 .4 1.4 0l2.3-2.3c.5-.4.5-1.1.1-1.4z"/></svg>

After

Width:  |  Height:  |  Size: 326 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M38 6H10c-2.21 0-4 1.79-4 4v28c0 2.21 1.79 4 4 4h28c2.21 0 4-1.79 4-4V10c0-2.21-1.79-4-4-4zM20 34L10 24l2.83-2.83L20 28.34l15.17-15.17L38 16 20 34z"/></svg>

After

Width:  |  Height:  |  Size: 248 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M38 10v28H10V10h28m0-4H10c-2.21 0-4 1.79-4 4v28c0 2.21 1.79 4 4 4h28c2.21 0 4-1.79 4-4V10c0-2.21-1.79-4-4-4z"/></svg>

After

Width:  |  Height:  |  Size: 209 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>

After

Width:  |  Height:  |  Size: 239 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M38 12.83L35.17 10 24 21.17 12.83 10 10 12.83 21.17 24 10 35.17 12.83 38 24 26.83 35.17 38 38 35.17 26.83 24z"/></svg>

After

Width:  |  Height:  |  Size: 210 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM14 13v4h-4v-4H7l5-5 5 5h-3z"/></svg>

After

Width:  |  Height:  |  Size: 318 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M6 34.5V42h7.5l22.13-22.13-7.5-7.5L6 34.5zm35.41-20.41c.78-.78.78-2.05 0-2.83l-4.67-4.67c-.78-.78-2.05-.78-2.83 0l-3.66 3.66 7.5 7.5 3.66-3.66z"/></svg>

After

Width:  |  Height:  |  Size: 244 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M12 38c0 2.21 1.79 4 4 4h16c2.21 0 4-1.79 4-4V14H12v24zM38 8h-7l-2-2H19l-2 2h-7v4h28V8z"/></svg>

After

Width:  |  Height:  |  Size: 188 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/></svg>

After

Width:  |  Height:  |  Size: 264 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M18 32.34L9.66 24l-2.83 2.83L18 38l24-24-2.83-2.83z"/></svg>

After

Width:  |  Height:  |  Size: 152 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M6 34.5V42h7.5l22.13-22.13-7.5-7.5L6 34.5zm35.41-20.41c.78-.78.78-2.05 0-2.83l-4.67-4.67c-.78-.78-2.05-.78-2.83 0l-3.66 3.66 7.5 7.5 3.66-3.66z"/></svg>

After

Width:  |  Height:  |  Size: 244 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M24 4C12.96 4 4 12.95 4 24s8.96 20 20 20 20-8.95 20-20S35.04 4 24 4zm2 30h-4v-4h4v4zm0-8h-4V14h4v12z"/></svg>

After

Width:  |  Height:  |  Size: 201 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"/></svg>

After

Width:  |  Height:  |  Size: 430 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm-1 4l6 6v10c0 1.1-.9 2-2 2H7.99C6.89 23 6 22.1 6 21l.01-14c0-1.1.89-2 1.99-2h7zm-1 7h5.5L14 6.5V12z"/></svg>

After

Width:  |  Height:  |  Size: 274 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M10 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/></svg>

After

Width:  |  Height:  |  Size: 229 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M40 12H24l-4-4H8c-2.21 0-3.98 1.79-3.98 4L4 36c0 2.21 1.79 4 4 4h32c2.21 0 4-1.79 4-4V16c0-2.21-1.79-4-4-4zm0 24H8V16h32v20z"/></svg>

After

Width:  |  Height:  |  Size: 225 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3.01L14.5 12l4.5 6H5l3.5-4.5z"/></svg>

After

Width:  |  Height:  |  Size: 258 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-2 10H7v-2h10v2z"/></svg>

After

Width:  |  Height:  |  Size: 194 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/></g><g><g><polygon points="17.59,18 19,16.59 14.42,12 19,7.41 17.59,6 11.59,12"/><polygon points="11,18 12.41,16.59 7.83,12 12.41,7.41 11,6 5,12"/></g></g></svg>

After

Width:  |  Height:  |  Size: 323 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/></g><g><g><polygon points="6.41,6 5,7.41 9.58,12 5,16.59 6.41,18 12.41,12"/><polygon points="13,6 11.59,7.41 16.17,12 11.59,16.59 13,18 19,12"/></g></g></svg>

After

Width:  |  Height:  |  Size: 320 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M3 3h18v2H3z"/></svg>

After

Width:  |  Height:  |  Size: 152 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M6 19h12v2H6z"/></svg>

After

Width:  |  Height:  |  Size: 153 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M30.83 14.83L28 12 16 24l12 12 2.83-2.83L21.66 24z"/></svg>

After

Width:  |  Height:  |  Size: 151 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M20 12l-2.83 2.83L26.34 24l-9.17 9.17L20 36l12-12z"/></svg>

After

Width:  |  Height:  |  Size: 151 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 4H5c-1.11 0-2 .9-2 2v12c0 1.1.89 2 2 2h4v-2H5V8h14v10h-4v2h4c1.1 0 2-.9 2-2V6c0-1.1-.89-2-2-2zm-7 6l-4 4h3v6h2v-6h3l-4-4z"/></svg>

After

Width:  |  Height:  |  Size: 263 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 19H5V5h7V3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/></svg>

After

Width:  |  Height:  |  Size: 268 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><rect fill="none" height="24" width="24" x="0" y="0"/><path d="M12,2C6.48,2,2,6.48,2,12c0,5.52,4.48,10,10,10s10-4.48,10-10C22,6.48,17.52,2,12,2z M13.88,11.54l-4.96,4.96l-1.41-1.41 l4.96-4.96L10.34,8l5.65,0.01L16,13.66L13.88,11.54z"/></svg>

After

Width:  |  Height:  |  Size: 356 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M28 20H4v4h24v-4zm0-8H4v4h24v-4zm8 16v-8h-4v8h-8v4h8v8h4v-8h8v-4h-8zM4 32h16v-4H4v4z"/></svg>

After

Width:  |  Height:  |  Size: 185 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/></g><g><g/><g><path d="M17,19.22H5V7h7V5H5C3.9,5,3,5.9,3,7v12c0,1.1,0.9,2,2,2h12c1.1,0,2-0.9,2-2v-7h-2V19.22z"/><path d="M19,2h-2v3h-3c0.01,0.01,0,2,0,2h3v2.99c0.01,0.01,2,0,2,0V7h3V5h-3V2z"/><rect height="2" width="8" x="7" y="9"/><polygon points="7,12 7,14 15,14 15,12 12,12"/><rect height="2" width="8" x="7" y="15"/></g></g></svg>

After

Width:  |  Height:  |  Size: 496 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/></g><g><path d="M16,9V4l1,0c0.55,0,1-0.45,1-1v0c0-0.55-0.45-1-1-1H7C6.45,2,6,2.45,6,3v0 c0,0.55,0.45,1,1,1l1,0v5c0,1.66-1.34,3-3,3h0v2h5.97v7l1,1l1-1v-7H19v-2h0C17.34,12,16,10.66,16,9z" fill-rule="evenodd"/></g></svg>

After

Width:  |  Height:  |  Size: 379 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M36.79 21.2C33.11 17.97 28.29 16 23 16c-9.3 0-17.17 6.06-19.92 14.44L7.81 32c2.1-6.39 8.1-11 15.19-11 3.91 0 7.46 1.44 10.23 3.77L26 32h18V14l-7.21 7.2z"/></svg>

After

Width:  |  Height:  |  Size: 253 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>

After

Width:  |  Height:  |  Size: 340 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z"/></svg>

After

Width:  |  Height:  |  Size: 280 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M6 10h4V6c-2.21 0-4 1.79-4 4zm0 16h4v-4H6v4zm8 16h4v-4h-4v4zM6 18h4v-4H6v4zM26 6h-4v4h4V6zm12 0v4h4c0-2.21-1.79-4-4-4zM10 42v-4H6c0 2.21 1.79 4 4 4zm-4-8h4v-4H6v4zM18 6h-4v4h4V6zm4 36h4v-4h-4v4zm16-16h4v-4h-4v4zm0 16c2.21 0 4-1.79 4-4h-4v4zm0-24h4v-4h-4v4zm0 16h4v-4h-4v4zm-8 8h4v-4h-4v4zm0-32h4V6h-4v4zM14 34h20V14H14v20zm4-16h12v12H18V18z"/></svg>

After

Width:  |  Height:  |  Size: 441 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24" viewBox="0 0 24 24" width="24"><g><rect fill="none" height="24" width="24"/><path d="M20.41,8.41l-4.83-4.83C15.21,3.21,14.7,3,14.17,3H5C3.9,3,3,3.9,3,5v14c0,1.1,0.9,2,2,2h14c1.1,0,2-0.9,2-2V9.83 C21,9.3,20.79,8.79,20.41,8.41z M7,7h7v2H7V7z M17,17H7v-2h10V17z M17,13H7v-2h10V13z"/></g></svg>

After

Width:  |  Height:  |  Size: 376 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M25 16c-5.29 0-10.11 1.97-13.8 5.2L4 14v18h18l-7.23-7.23C17.54 22.44 21.09 21 25 21c7.09 0 13.09 4.61 15.19 11l4.73-1.56C42.17 22.06 34.3 16 25 16z"/></svg>

After

Width:  |  Height:  |  Size: 248 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"><path d="M24 9C14 9 5.46 15.22 2 24c3.46 8.78 12 15 22 15 10.01 0 18.54-6.22 22-15-3.46-8.78-11.99-15-22-15zm0 25c-5.52 0-10-4.48-10-10s4.48-10 10-10 10 4.48 10 10-4.48 10-10 10zm0-16c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6z"/></svg>

After

Width:  |  Height:  |  Size: 328 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 0 24 24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M13 13v8h8v-8h-8zM3 21h8v-8H3v8zM3 3v8h8V3H3zm13.66-1.31L11 7.34 16.66 13l5.66-5.66-5.66-5.65z"/></svg>

After

Width:  |  Height:  |  Size: 232 B

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="24"
viewBox="0 0 24 24"
width="24"
version="1.1"
id="svg6"
sodipodi:docname="ic_widgets_grey_24px.svg"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
<metadata
id="metadata12">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs10" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1379"
id="namedview8"
showgrid="false"
inkscape:zoom="9.8333333"
inkscape:cx="12"
inkscape:cy="12"
inkscape:window-x="0"
inkscape:window-y="32"
inkscape:window-maximized="1"
inkscape:current-layer="svg6" />
<path
d="M0 0h24v24H0z"
fill="none"
id="path2" />
<path
d="M13 13v8h8v-8h-8zM3 21h8v-8H3v8zM3 3v8h8V3H3zm13.66-1.31L11 7.34 16.66 13l5.66-5.66-5.66-5.65z"
id="path4"
style="fill:#808080" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="24"
viewBox="0 0 24 24"
width="24"
version="1.1"
id="svg6"
sodipodi:docname="ic_widgets_red_24px.svg"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)">
<metadata
id="metadata12">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs10" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="2560"
inkscape:window-height="1411"
id="namedview8"
showgrid="false"
inkscape:zoom="9.8333333"
inkscape:cx="-4.0677967"
inkscape:cy="12"
inkscape:window-x="2560"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg6" />
<path
d="M0 0h24v24H0z"
fill="none"
id="path2" />
<path
d="M13 13v8h8v-8h-8zM3 21h8v-8H3v8zM3 3v8h8V3H3zm13.66-1.31L11 7.34 16.66 13l5.66-5.66-5.66-5.65z"
id="path4"
style="fill:#ff0000" />
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

608
gui/installer.py 100644
View File

@ -0,0 +1,608 @@
import sys
import getpass
import logging
from threading import Thread
from tape_editor_widgets import *
from widgets import LabelWebLink
from deploy import Deployer, get_ffmpeg_version
from PIL import ImageGrab
from pathlib import Path
import yaml
class OptionsWidget(QWidget):
def __init__(self, parent: QWidget):
super().__init__()
self.parent = parent
self.logging_handler = SignalHandler()
self.deployer = Deployer(logging_handler=self.logging_handler)
options = [("Buffs", "Some songs affect the characters with strengthen\nand haste afflictions."),
("De-buffs", "Some songs affect the characters with psychosis \naffliction."),
("Walkmans", "Include personal music players (walkmans)"),
("Durability", "Playing and throwing cassettes damages them."),
("Repair", "Crafting mechanics to repair broken tapes by \nduplicating them with error correction.")]
# Name options
self.name_line_edit = QLineEdit()
self.name_line_edit.setText(f"{getpass.getuser()} mixtape")
self.name_hbox = QHBoxLayout()
self.name_hbox.addWidget(self.name_line_edit)
self.name_gbox = QGroupBox("Local mod")
self.name_gbox.setCheckable(True)
self.name_gbox.setLayout(self.name_hbox)
# resolution
self.width_spinbox = QSpinBox()
self.width_spinbox.setRange(100, 100000)
self.width_spinbox.setPrefix("Width: ")
self.width_spinbox.setSuffix(" px")
self.height_spinbox = QSpinBox()
self.height_spinbox.setRange(100, 100000)
self.height_spinbox.setPrefix("Height: ")
self.height_spinbox.setSuffix(" px")
self.detect_pushbutton = QPushButton("Detect screen resolution")
self.detect_pushbutton.clicked.connect(self.update_screen_res)
self.update_screen_res()
self.resolution_hbox = QVBoxLayout()
self.resolution_hbox.addWidget(self.width_spinbox)
self.resolution_hbox.addWidget(self.height_spinbox)
self.resolution_hbox.addWidget(self.detect_pushbutton)
self.tape_covers_gbox = QGroupBox("Tape covers")
self.tape_covers_gbox.setCheckable(True)
self.tape_covers_gbox.setLayout(self.resolution_hbox)
# Checkbox options
self.cbox_buffs = TelegraphingCheckbox(options[0][0], options[0][1])
self.cbox_debuffs = TelegraphingCheckbox(options[1][0], options[1][1])
self.cbox_walkmans = TelegraphingCheckbox(options[2][0], options[2][1])
self.cbox_durability = TelegraphingCheckbox(options[3][0], options[3][1])
self.cbox_repair = TelegraphingCheckbox(options[4][0], options[4][1])
self.label_options_description = QLabel()
self.label_options_description.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
self.label_options_description.setAlignment(Qt.AlignmentFlag.AlignLeft)
self.label_options_description.setAlignment(Qt.AlignmentFlag.AlignTop)
self.options_vbox = QVBoxLayout()
for cbox in [self.cbox_buffs,
self.cbox_debuffs,
self.cbox_walkmans,
self.cbox_durability,
self.cbox_repair]:
self.options_vbox.addWidget(cbox)
cbox.hover.connect(self.label_options_description.setText)
cbox.toggle()
self.options_hbox = QHBoxLayout()
self.options_hbox.addLayout(self.options_vbox)
self.options_hbox.addWidget(self.label_options_description)
self.options_gbox = QGroupBox("Other options")
self.options_gbox.setLayout(self.options_hbox)
# Update widget
self.update_widget = UpdateWidget(self, self.deployer)
# Install widget
self.install_widget = InstallWidget(self, self.deployer)
# Console widget
self.console_widget = ConsoleWidget(self)
self.logging_handler.emitter.log_message.connect(self.console_widget.console.appendPlainText)
layout = QVBoxLayout(self)
layout.addWidget(self.name_gbox)
layout.addWidget(self.tape_covers_gbox)
layout.addWidget(self.options_gbox)
layout.addWidget(self.update_widget)
layout.addWidget(self.install_widget)
layout.addWidget(self.console_widget)
layout.addStretch()
# def update_label_description(self):
def update_screen_res(self):
img = ImageGrab.grab()
self.width_spinbox.setValue(img.size[0])
self.height_spinbox.setValue(img.size[1])
@property
def options(self):
return {"name": self.name_line_edit.text(),
"tape_covers": self.tape_covers_gbox.isChecked(),
"resoultion_x": self.width_spinbox.value(),
"resolution_y": self.height_spinbox.value(),
"buffs": self.cbox_buffs.isChecked(),
"debuffs": self.cbox_debuffs.isChecked(),
"walkmans": self.cbox_walkmans.isChecked(),
"durability": self.cbox_durability.isChecked(),
"repair": self.cbox_repair.isChecked()}
# TODO: implement random local mod name
class CodeEditorWidget(QWidget):
def __init__(self, parent: QWidget):
super().__init__()
self.parent = parent
self.tapes_yaml = Path("./source/tapes.yaml")
self.filename_string = f"<tt style=\"font-family: consolas;\"><b>{self.tapes_yaml.name}</b></tt>"
self.tapes_file_watcher = QFileSystemWatcher()
self.tapes_file_watcher.addPath(self.tapes_yaml.as_posix())
self.tapes_file_watcher.fileChanged.connect(self.external_file_changed)
self.external_editor_pushbutton = QPushButton()
self.external_editor_pushbutton.setIcon(QIcon("./gui/icons/ic_open_in_new_24px.svg"))
self.external_editor_pushbutton.setToolTip("Edit in external editor")
self.external_editor_pushbutton.clicked.connect(self.open_file)
self.reload_code_pushbutton = QPushButton()
self.reload_code_pushbutton.setIcon(QIcon("./gui/icons/ic_refresh_24px.svg"))
self.reload_code_pushbutton.setToolTip("Reload the external file")
self.reload_code_pushbutton.clicked.connect(self.load_file)
self.save_code_pushbutton = QPushButton()
self.save_code_pushbutton.setIcon(QIcon("./gui/icons/ic_save_24px.svg"))
self.save_code_pushbutton.setToolTip("Save setup")
self.save_code_pushbutton.clicked.connect(self.save_file)
self.transfer_to_validator_pushbutton = QPushButton()
self.transfer_to_validator_pushbutton.setIcon(QIcon("./gui/icons/ic_keyboard_double_arrow_right_24px.svg"))
self.transfer_to_validator_pushbutton.setToolTip("Apply the code.")
self.transfer_to_validator_pushbutton.clicked.connect(self.apply_code)
self.status_same = f"<b>Status:</b> yaml setup matches {self.filename_string} file"
self.status_external_file_changed = f"<b>Status:</b> {self.filename_string} file was edited externally"
self.status_editor_code_changed = "<b>Status:</b> yaml setup not yet saved"
self.status_label = QLabel(self.status_same)
self.editor = CodeEditor()
self.editor.setFont("Consolas")
self.editor.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
self.editor.setMinimumWidth(400)
self.editor.textChanged.connect(self.yaml_setup_changed)
self.button_hbox = QHBoxLayout()
self.button_hbox.addWidget(self.external_editor_pushbutton)
self.button_hbox.addWidget(self.reload_code_pushbutton)
self.button_hbox.addWidget(self.save_code_pushbutton)
self.button_hbox.addWidget(self.status_label)
self.button_hbox.addStretch()
self.button_hbox.addWidget(self.transfer_to_validator_pushbutton)
# self.button_hbox.addStretch()
self.editor_vbox = QVBoxLayout()
self.editor_vbox.addLayout(self.button_hbox)
self.editor_vbox.addWidget(self.editor)
self.editor_gbox = QGroupBox("tapes.yaml editor")
self.editor_gbox.setLayout(self.editor_vbox)
layout = QVBoxLayout(self)
layout.addWidget(self.editor_gbox)
self.load_file()
def load_file(self):
with open(self.tapes_yaml, encoding="utf-8") as fp:
text = fp.read()
self.editor.setPlainText(text)
def save_file(self):
with open(self.tapes_yaml, "w+", encoding="utf-8") as fp:
fp.write(self.editor.toPlainText())
def open_file(self):
os.startfile(self.tapes_yaml)
def check_if_setups_match(self):
with open(self.tapes_yaml, encoding="utf-8") as fp:
external_text = fp.read()
contents_match = external_text == self.editor.toPlainText()
self.save_code_pushbutton.setDisabled(contents_match)
self.reload_code_pushbutton.setDisabled(contents_match)
if contents_match:
self.status_label.setText(self.status_same)
return True
else:
return False
def external_file_changed(self):
if not self.check_if_setups_match():
self.status_label.setText(self.status_external_file_changed)
def yaml_setup_changed(self):
if not self.check_if_setups_match():
self.status_label.setText(self.status_editor_code_changed)
def apply_code(self):
try:
tapes = yaml.safe_load(self.editor.toPlainText())
self.parent.validation_widget.load_tapes(tapes)
except yaml.scanner.ScannerError as e:
logging.error(f"Error applying yaml code:\n\n{str(e)}")
class TapeEditWidget(QWidget):
def __init__(self, tape: dict, number: int | None = None):
super().__init__()
self.tape = tape
self.label_nr = QLabel(f"#{number}")
self.label_nr.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.label_nr.setStyleSheet("background-color: #cfd8dc;")
change_font_size(self.label_nr, 1.41)
self.identifier = IdentifierEditWidget(self.tape)
self.cover = CoverEditWidget(self.tape)
self.edit_name = NameEditWidget(self.tape)
self.edit_source = SourceEditWidget(self.tape)
self.process = ProcessEditWidget(self.tape)
self.economy = EconomyEditWidget(self.tape)
self.spawn = SpawnEditWidget(self.tape)
self.afflictions = AfflictionsEditWidget(self.tape)
self.crafting = CraftingEditWidget(self.tape)
layout = QGridLayout(self)
layout.setContentsMargins(0, 0, 0, 5)
for i, (text, widget) in enumerate([(f"", self.identifier),
("Cover", self.cover),
("Name", self.edit_name),
("Source", self.edit_source),
("Process", self.process),
("Economy", self.economy),
("Spawn", self.spawn),
("Crafting", self.crafting),
("Afflictions", self.afflictions),
("Note", QPlainTextEdit(self.tape["note"]))]):
label = self.label_nr if i == 0 else QLabel(text)
if i == 0:
layout.addWidget(label, i, 0)
else:
label_layout = QVBoxLayout()
label_layout.addWidget(label)
label_layout.addStretch()
layout.addLayout(label_layout, i, 0)
layout.addWidget(widget, i, 1)
class TapeListWidget(QWidget):
def __init__(self):
super().__init__()
self.layout = QVBoxLayout(self)
self.layout.setContentsMargins(1, 0, 0, 0)
def append(self, widget):
self.layout.addWidget(widget)
def clear(self):
for i, widget in enumerate(self.children()):
if i > 0:
self.layout.removeWidget(widget)
widget.deleteLater()
class ValidationWidget(QWidget):
def __init__(self, parent: QWidget):
super().__init__()
self.parent = parent
self.transfer_to_code_pushbutton = QPushButton()
self.transfer_to_code_pushbutton.setIcon(QIcon("./gui/icons/ic_keyboard_double_arrow_left_24px.svg"))
self.transfer_to_code_pushbutton.setToolTip("Update the code")
self.transfer_to_code_pushbutton.clicked.connect(self.save_tapes)
self.jump_spinbox = QSpinBox()
# self.search_lineedit = QLineEdit()
# self.search_lineedit.setPlaceholderText("Search ...")
self.buttons_hbox = QHBoxLayout()
self.buttons_hbox.addWidget(self.transfer_to_code_pushbutton)
self.buttons_hbox.addWidget(QLabel("Navigate to"))
self.buttons_hbox.addWidget(self.jump_spinbox)
self.buttons_hbox.addStretch()
# self.buttons_hbox.addWidget(self.search_lineedit)
self.tape_list_widget = TapeListWidget()
self.scroll = QScrollArea()
self.scroll.setWidget(self.tape_list_widget)
self.scroll.setWidgetResizable(True)
self.scroll.setFrameStyle(QFrame.Shape.NoFrame)
self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.validation_vbox = QVBoxLayout()
self.validation_vbox.addLayout(self.buttons_hbox)
self.validation_vbox.addWidget(self.scroll)
self.validation_gbox = QGroupBox("Data validation")
self.validation_gbox.setLayout(self.validation_vbox)
self.jump_spinbox.valueChanged.connect(self.jump_to_tape)
layout = QVBoxLayout(self)
layout.addWidget(self.validation_gbox)
self.setMinimumWidth(500)
self.setMaximumWidth(500)
self.tapes = None
self.load_tapes_from_file()
def load_tapes_from_file(self):
try:
with open("./source/tapes.yaml", encoding="utf-8") as fp:
tapes = yaml.safe_load(fp)
self.load_tapes(tapes)
except yaml.scanner.ScannerError as e:
logging.error(f"Error applying yaml code:\n\n{str(e)}")
def load_tapes(self, tapes):
self.tapes = tapes
self.tape_list_widget.clear()
for i, tape in enumerate(self.tapes):
self.tape_list_widget.append(TapeEditWidget(tape, i))
self.jump_spinbox.setValue(0)
self.jump_spinbox.setMaximum(i)
def save_tapes(self):
yaml_text = yaml.safe_dump(self.tapes, sort_keys=False, allow_unicode=True).replace("\n-", "\n\n-")
self.parent.editor_widget.editor.setPlainText(yaml_text)
def jump_to_tape(self, tape_number):
scrollbar = self.scroll.verticalScrollBar()
division = (scrollbar.maximum() + scrollbar.pageStep()) / len(self.tapes)
overflow_ratio = (scrollbar.pageStep() + 5) / division
step = scrollbar.maximum() / (len(self.tapes) - overflow_ratio)
scrollbar.setValue(tape_number * step)
class UpdateWidget(QGroupBox):
def __init__(self, parent: QWidget, deployer: Deployer):
super().__init__("Update")
self.parent = parent
self.deployer = deployer
hr_html_string = "<hr style=\"background-color:gray;\">"
self.grid = QGridLayout(self)
self.git_checkbox = QCheckBox()
self.git_checkbox.setDisabled(True)
self.git_label = QLabel("Download and unpack git")
self.git_label_link = LabelWebLink("https://github.com/git-for-windows/git/releases/download/"
"v2.46.2.windows.1/PortableGit-2.46.2-64-bit.7z.exe")
self.git_label_link.break_at("download")
self.git_pushbutton = QPushButton("Download")
self.git_pushbutton.clicked.connect(self.download_git_action)
self.git_dir = DirWidget("./utils")
self.grid.addWidget(self.git_checkbox, 0, 0)
self.grid.addWidget(self.git_label, 0, 1)
self.grid.addWidget(self.git_pushbutton, 0, 2)
self.grid.addWidget(self.git_label_link, 1, 1)
self.grid.addWidget(self.git_dir, 2, 1)
self.git_update_checkbox = QCheckBox()
self.git_update_checkbox.setDisabled(True)
self.git_update_label = QLabel("Download update from git")
self.git_update_label_link = LabelWebLink("https://git.kompot.si/jaka/barotrauma-sunken-tapes")
self.git_update_pushbutton = QPushButton("Update")
self.git_update_pushbutton.clicked.connect(self.update_action)
self.grid.addWidget(self.git_update_checkbox, 10, 0)
self.grid.addWidget(self.git_update_label, 10, 1)
self.grid.addWidget(self.git_update_label_link, 11, 1)
self.grid.addWidget(self.git_update_pushbutton, 10, 2)
self.grid.addWidget(QLabel(hr_html_string), 12, 0, 1, 3)
self.git_dir_watcher = QFileSystemWatcher()
self.git_dir_watcher.addPath(Path(self.git_dir.directory).as_posix())
self.git_dir_watcher.directoryChanged.connect(self.does_git_exist)
self.does_git_exist()
def download_git_action(self):
Thread(target=self.deployer.download_git).start()
def does_git_exist(self):
executable_path = Path("git/bin/git.exe")
if (self.git_dir.directory / executable_path).exists():
exists = True
else:
exists = False
self.git_pushbutton.setEnabled(not exists)
self.git_checkbox.setChecked(exists)
self.git_update_pushbutton.setEnabled(exists)
return exists
def update_action(self):
Thread(target=self.deployer.update).start()
class ConsoleWidget(QGroupBox):
def __init__(self, parent: QWidget):
super().__init__("Info")
self.parent = parent
self.console = QPlainTextEdit(self)
self.console.setReadOnly(True)
self.console.setFont("Consolas")
self.console.setMinimumHeight(100)
layout = QVBoxLayout(self)
layout.addWidget(self.console)
class InstallWidget(QGroupBox):
def __init__(self, parent: QWidget, deployer: Deployer):
super().__init__("Install steps")
self.parent = parent
self.deployer = deployer
self.grid = QGridLayout(self)
hr_html_string = "<hr style=\"background-color:gray;\">"
self.ffmpeg_checkbox = QCheckBox()
self.ffmpeg_checkbox.setDisabled(True)
self.ffmpeg_label = QLabel("Download and unpack ffmpeg")
self.ffmpeg_label_link = LabelWebLink("https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-full.7z")
self.ffmpeg_pushbutton = QPushButton("Download")
self.ffmpeg_pushbutton.clicked.connect(self.download_ffmpeg_action)
self.ffmpeg_dir = DirWidget(self.deployer.utils_dir)
self.grid.addWidget(self.ffmpeg_checkbox, 0, 0)
self.grid.addWidget(self.ffmpeg_label, 0, 1)
self.grid.addWidget(self.ffmpeg_pushbutton, 0, 2)
self.grid.addWidget(self.ffmpeg_label_link, 1, 1)
self.grid.addWidget(self.ffmpeg_dir, 2, 1)
self.grid.addWidget(QLabel(hr_html_string), 9, 0, 1, 3)
self.songs_download_checkbox = QCheckBox()
self.songs_download_checkbox.setDisabled(True)
self.songs_download_label = QLabel("Download songs and process them")
self.songs_download_dir = DirWidget(self.deployer.music_dir)
self.songs_download_pushbutton = QPushButton("Download")
self.songs_download_pushbutton.clicked.connect(self.are_songs_ready)
self.grid.addWidget(self.songs_download_checkbox, 20, 0)
self.grid.addWidget(self.songs_download_label, 20, 1)
self.grid.addWidget(self.songs_download_pushbutton, 20, 2)
self.grid.addWidget(self.songs_download_dir, 21, 1)
self.grid.addWidget(QLabel(hr_html_string), 22, 0, 1, 3)
self.compile_checkbox = QCheckBox()
self.compile_checkbox.setDisabled(True)
self.compile_label = QLabel("Compile mod according to the settings")
self.compile_dir = DirWidget(self.deployer.build_dir)
self.compile_pushbutton = QPushButton("Compile")
self.grid.addWidget(self.compile_checkbox, 30, 0)
self.grid.addWidget(self.compile_label, 30, 1)
self.grid.addWidget(self.compile_pushbutton, 30, 2)
self.grid.addWidget(self.compile_dir, 31, 1)
self.grid.addWidget(QLabel(hr_html_string), 32, 0, 1, 3)
self.install_checkbox = QCheckBox()
self.install_checkbox.setDisabled(True)
self.install_label = QLabel("Copy the files to the install directory")
self.install_dir = DirWidget(self.deployer.install_dir)
self.install_pushbutton = QPushButton("Install")
self.grid.addWidget(self.install_checkbox, 40, 0)
self.grid.addWidget(self.install_label, 40, 1)
self.grid.addWidget(self.install_pushbutton, 40, 2)
self.grid.addWidget(self.install_dir, 41, 1)
self.grid.addWidget(QLabel(hr_html_string), 42, 0, 1, 3)
self.grid.setRowStretch(50, 20)
self.utils_dir_watcher = QFileSystemWatcher()
self.utils_dir_watcher.addPath(Path(self.ffmpeg_dir.directory).as_posix())
self.utils_dir_watcher.directoryChanged.connect(self.does_ffmpeg_exist)
self.does_ffmpeg_exist()
def does_ffmpeg_exist(self):
executable_path = Path("ffmpeg-" + get_ffmpeg_version() + "-full_build/bin/ffmpeg.exe")
if (self.ffmpeg_dir.directory / executable_path).exists():
exists = True
else:
exists = False
self.ffmpeg_pushbutton.setEnabled(not exists)
self.ffmpeg_checkbox.setChecked(exists)
self.songs_download_pushbutton.setEnabled(exists)
return exists
def download_ffmpeg_action(self):
Thread(target=self.deployer.download_ffmpeg).start()
def are_songs_ready(self):
tapes = self.parent.parent.validation_widget.tapes
files = Path(self.songs_download_dir.directory).glob("*.ogg")
files = [f.stem for f in files]
for tape in tapes:
tape_name = tape["identifier"]
if not tape_name in files:
return False
if not f"{tape_name}-walkman" in files and self.parent.options["walkmans"]:
return False
return True
class MainWidget(QWidget):
def __init__(self):
super().__init__()
layout = QHBoxLayout(self)
self.options_widget = OptionsWidget(self)
self.editor_widget = CodeEditorWidget(self)
self.validation_widget = ValidationWidget(self)
layout.addWidget(self.options_widget)
layout.addWidget(self.editor_widget)
layout.addWidget(self.validation_widget)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Sunken Tapes installer")
self.quit_shortcut = QShortcut(QKeySequence('Ctrl+Q'), self)
self.quit_shortcut.activated.connect(QApplication.instance().quit)
self.setCentralWidget(MainWidget())
class QSignaler(QObject):
log_message = Signal(str)
class SignalHandler(logging.Handler):
"""Logging handler to emit QtSignal with log record text."""
def __init__(self, *args, **kwargs):
super(SignalHandler, self).__init__(*args, **kwargs)
self.emitter = QSignaler()
def emit(self, log_record: logging.LogRecord):
self.emitter.log_message.emit(f"{log_record.message}")
# When the line below is enabled, logging is immediate/otherwise events
# on the queue will be processed when the slot has finished.
# QtGui.qApp.processEvents()
def main():
os.chdir("..")
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()

View File

@ -0,0 +1,331 @@
from widgets import *
def change_font_size(label: QLabel | QLineEdit, factor: float) -> QLabel:
f = label.font()
f.setPointSizeF(f.pointSizeF() * factor)
label.setFont(f)
return label
class IdentifierEditWidget(QLineEdit):
def __init__(self, tape: dict):
super().__init__()
self.tape = tape
self.setFont("Consolas")
self.setStyleSheet("font-weight: bold; background-color: #cfd8dc;")
self.setTextMargins(2, 0, 2, 0)
self.setFrame(QFrame.Shape.NoFrame)
self.setText(self.tape["identifier"])
change_font_size(self, 1.41)
self.load()
self.textChanged.connect(self.save)
def load(self):
self.setText(self.tape["identifier"])
def save(self):
self.tape["identifier"] = self.text()
class CoverEditWidget(QLabel):
def __init__(self, tape: dict):
super().__init__()
self.tape = tape
pix = QPixmap(f"./source/images/{self.tape['identifier']}.png")
self.pixmap = pix.scaled(350, 350,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation)
self.resize(350, 0)
self.setPixmap(self.pixmap)
class NameEditWidget(QLineEdit):
def __init__(self, tape: dict):
super().__init__()
self.tape = tape
self.load()
self.textChanged.connect(self.save)
def load(self):
self.setText(self.tape["name"])
def save(self):
self.tape["name"] = self.text()
class SourceEditWidget(UrlWidget):
def __init__(self, tape: dict):
super().__init__()
self.tape = tape
self.load()
self.url_lineedit.textChanged.connect(self.save)
def load(self):
self.url_lineedit.setText(self.tape["source"])
def save(self):
self.tape["source"] = self.url_lineedit.text()
class ProcessEditWidget(QWidget):
def __init__(self, tape: dict):
super().__init__()
self.tape = tape
self.cbox_clip_start = QCheckBox("Clip at start")
self.cbox_clip_end = QCheckBox("Clip at end")
self.cbox_volume_factor = QCheckBox("Volume factor")
self.clip_start = TimeEditWithoutWheel()
self.clip_start.setDisplayFormat("hh:mm:ss.z")
self.clip_end = TimeEditWithoutWheel()
self.clip_end.setDisplayFormat("hh:mm:ss.z")
self.volume_factor = SpinBoxWithoutWheel()
self.volume_factor.setSuffix(" dB")
self.volume_factor.setMinimum(-30)
self.volume_factor.setMaximum(30)
layout = QGridLayout(self)
layout.setContentsMargins(5, 0, 5, 0)
layout.setColumnStretch(3, 1)
layout.addWidget(self.cbox_clip_start, 0, 0)
layout.addWidget(self.cbox_clip_end, 1, 0)
layout.addWidget(self.cbox_volume_factor, 2, 0)
layout.addWidget(self.clip_start, 0, 1)
layout.addWidget(self.clip_end, 1, 1)
layout.addWidget(self.volume_factor, 2, 1)
self.load()
self.clip_start.timeChanged.connect(self.save)
self.clip_end.timeChanged.connect(self.save)
self.volume_factor.valueChanged.connect(self.save)
self.cbox_clip_start.toggled.connect(self.save)
self.cbox_clip_end.toggled.connect(self.save)
self.cbox_volume_factor.toggled.connect(self.save)
self.save()
def load(self):
clip_start, clip_end = self.tape["process"]["start"], self.tape["process"]["end"]
start, end = QTime(), QTime()
if clip_start is not None:
start_time = datetime.datetime.strptime(clip_start, "%H:%M:%S.%f")
start.setHMS(start_time.hour, start_time.minute, start_time.second, ms=start_time.microsecond * 1e-3)
self.cbox_clip_start.setChecked(clip_start != "00:00:00.0")
self.clip_start.setTime(start)
if clip_end is not None:
end_time = datetime.datetime.strptime(clip_end, "%H:%M:%S.%f")
end.setHMS(end_time.hour, end_time.minute, end_time.second, ms=end_time.microsecond * 1e-3)
self.cbox_clip_end.setChecked(clip_end != "00:00:00.0")
self.clip_end.setTime(end)
volume_factor = self.tape["process"]["volume"]
if volume_factor is not None:
self.volume_factor.setValue(volume_factor)
self.cbox_volume_factor.setChecked(volume_factor != 0)
def save(self):
start = self.clip_start.time()
start = f"{start.hour():02}:{start.minute():02}:{start.second():02}.{start.msec():03}"
end = self.clip_end.time()
end = f"{end.hour():02}:{end.minute():02}:{end.second():02}.{end.msec():03}"
volume = self.volume_factor.value()
self.tape["process"]["start"] = start if self.cbox_clip_start.isChecked() else None
self.tape["process"]["end"] = end if self.cbox_clip_end.isChecked() else None
self.tape["process"]["volume"] = volume if self.cbox_volume_factor.isChecked() else None
class EconomyEditWidget(QWidget):
def __init__(self, tape: dict):
super().__init__()
self.tape = tape
self.spinbox_price = SpinBoxWithoutWheel()
self.spinbox_price.setRange(10, 100000)
self.spinbox_price.setSuffix(" mk")
self.spinbox_price.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Maximum)
layout = QGridLayout(self)
layout.setContentsMargins(5, 0, 5, 0)
layout.addWidget(self.spinbox_price, 1, 0)
layout.setColumnStretch(4, 1)
for i, text in enumerate(["Price", "Sold in", "Factor", " Local price"]):
layout.addWidget(change_font_size(QLabel(text), 0.8), 0, i)
self.economy = {"outpost": {},
"city": {},
"research": {},
"military": {},
"mine": {}}
for i, (location, editors) in enumerate(self.economy.items()):
editors["cbox"] = QCheckBox(location.capitalize())
editors["float"] = DoubleSpinBoxWithoutWheel()
editors["float"].setSingleStep(0.05)
editors["label"] = QLabel()
editors["label"].setFont("consolas")
layout.addWidget(editors["cbox"], int(i) + 1, 1)
layout.addWidget(editors["float"], int(i) + 1, 2)
layout.addWidget(editors["label"], int(i) + 1, 3)
self.load()
self.update_labels()
self.spinbox_price.valueChanged.connect(self.save)
for editors in self.economy.values():
editors["cbox"].toggled.connect(self.save)
editors["float"].valueChanged.connect(self.save)
def load(self):
self.spinbox_price.setValue(self.tape["price"])
for locale in self.tape["economy"]:
self.economy[locale["location"]]["cbox"].setChecked(locale["sold"])
self.economy[locale["location"]]["float"].setValue(locale["factor"])
def save(self):
self.tape["price"] = self.spinbox_price.value()
self.tape["economy"] = [{"location": location,
"factor": editors["float"].value(),
"sold": editors["cbox"].isChecked()} for location, editors in self.economy.items()]
self.update_labels()
def update_labels(self):
for editors in self.economy.values():
editors["label"].setText(f" {self.tape['price'] * editors['float'].value():4.0f} mk")
class SpawnEditWidget(QWidget):
def __init__(self, tape: dict):
super().__init__()
self.tape = tape
layout = QGridLayout(self)
layout.setContentsMargins(5, 0, 5, 0)
layout.setColumnStretch(2, 1)
for i, text in enumerate(["Place", "Probability"]):
layout.addWidget(change_font_size(QLabel(text), 0.8), 0, i)
self.spawn = {"outpostcrewcabinet": {"label": QLabel("Outpost Crew Cabinet"),
"value": ProbabilitySpinBox()},
"wreckstorage": {"label": QLabel("Wreck Storage"),
"value": ProbabilitySpinBox()},
"abandonedcrewcab": {"label": QLabel("Abandoned Crew Cabinet"),
"value": ProbabilitySpinBox()},
"abandonedstoragecab": {"label": QLabel("Abandoned Storage Cabinet"),
"value": ProbabilitySpinBox()}}
for i, (location, editors) in enumerate(self.spawn.items()):
layout.addWidget(editors["label"], int(i) + 1, 0)
layout.addWidget(editors["value"], int(i) + 1, 1)
self.load()
for i, (location, editors) in enumerate(self.spawn.items()):
editors["value"].valueChanged.connect(self.save)
def load(self):
for locale in self.tape["spawn"]:
self.spawn[locale["location"]]["value"].setValue(locale["probability"])
def save(self):
self.tape["spawn"] = [{"location": location,
"probability": editors["value"].value()} for location, editors in self.spawn.items() if
editors["value"].value() > 0]
# TODO: afflictions with shorter range for walkman songs.
class CraftingEditWidget(QWidget):
def __init__(self, tape: dict):
super().__init__()
self.tape = tape
self.int_uses = SpinBoxWithoutWheel()
self.int_uses.setMinimum(1)
self.int_uses.setMaximum(1000)
self.description = QPlainTextEdit("One use is duration of the tape")
self.description.setEnabled(False)
self.description.setMaximumHeight(QFontMetrics(self.description.font()).height() * 1.7)
layout = QHBoxLayout(self)
layout.addWidget(QLabel("Number of uses:"))
layout.addWidget(self.int_uses)
layout.addWidget(self.description)
layout.addStretch()
layout.setContentsMargins(5, 0, 5, 0)
self.load()
self.int_uses.valueChanged.connect(self.save)
def load(self):
self.int_uses.setValue(self.tape["no_of_uses"])
def save(self):
self.tape["no_of_uses"] = self.int_uses.value()
class AfflictionsEditWidget(QWidget):
def __init__(self, tape: dict):
super().__init__()
self.tape = tape
self.description = QPlainTextEdit("Factor 1.0 means that the affliction will reach the "
"highest level after being exposed to the sound of the "
"tape for its play duration.")
self.description.setEnabled(False)
self.description.setMaximumWidth(212)
layout = QGridLayout(self)
layout.setContentsMargins(5, 0, 5, 0)
layout.addWidget(change_font_size(QLabel("Affliction"), 0.8), 0, 0)
layout.addWidget(change_font_size(QLabel("Factor"), 0.8), 0, 1)
layout.addWidget(self.description, 1, 2, 3, 1)
layout.setColumnStretch(3, 1)
self.afflictions = {"strengthen": {},
"haste": {},
"psychosis": {}}
for i, (affliction, editors) in enumerate(self.afflictions.items()):
editors["cbox"] = QCheckBox(affliction.capitalize())
editors["float"] = DoubleSpinBoxWithoutWheel()
editors["float"].setSingleStep(0.01)
layout.addWidget(editors["cbox"], int(i) + 1, 0)
layout.addWidget(editors["float"], int(i) + 1, 1)
self.load()
for editors in self.afflictions.values():
editors["cbox"].toggled.connect(self.save)
editors["float"].valueChanged.connect(self.save)
def load(self):
if self.tape["afflictions"]:
for affliction in self.tape["afflictions"]:
self.afflictions[affliction["name"]]["cbox"].setChecked(True)
self.afflictions[affliction["name"]]["float"].setValue(affliction["factor"])
def save(self):
self.tape["afflictions"] = [{"name": affliction, "factor": editors["float"].value()} for affliction, editors in
self.afflictions.items() if editors["cbox"].isChecked()]

260
gui/widgets.py 100644
View File

@ -0,0 +1,260 @@
from PySide6.QtCore import QSize, Qt, Slot, QRect, QFileSystemWatcher, Signal, QTime, QObject
from PySide6.QtGui import QIcon, QAction, QShortcut, QKeySequence, QPainter, QColor, QTextFormat, QPixmap, QFontMetrics
from PySide6.QtWidgets import QApplication, QMainWindow, QPushButton, QWidget, QLabel, QVBoxLayout, QHBoxLayout, \
QGroupBox, QLineEdit, QCheckBox, QSpinBox, QPlainTextEdit, QSizePolicy, QGridLayout, QTextEdit, QScrollArea, QFrame, \
QDoubleSpinBox, QFormLayout, QTimeEdit, QFileDialog, QDialog
import os
import webbrowser
import validators
import datetime
from pathlib import Path
class LabelWebLink(QLabel):
def __init__(self, text):
text = f"<a href=\"{text}\">{text.replace('https://', '')}</a>"
super().__init__(text)
self.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
self.setTextInteractionFlags(Qt.TextInteractionFlag.LinksAccessibleByMouse)
self.setOpenExternalLinks(True)
def break_at(self, string):
self.setText(self.text().replace(f'{string}/', f'{string}/<br>'))
class LineNumberArea(QWidget):
def __init__(self, editor):
QWidget.__init__(self, editor)
self._code_editor = editor
def sizeHint(self):
return QSize(self._code_editor.line_number_area_width(), 0)
def paintEvent(self, event):
self._code_editor.line_number_area_paint_event(event)
class CodeEditor(QPlainTextEdit):
def __init__(self):
super().__init__()
self.line_number_area = LineNumberArea(self)
self.blockCountChanged[int].connect(self.update_line_number_area_width)
self.updateRequest[QRect, int].connect(self.update_line_number_area)
self.cursorPositionChanged.connect(self.highlight_current_line)
def line_number_area_width(self):
digits = len(str(self.blockCount()))
space = 3 + self.fontMetrics().horizontalAdvance('9') * digits
return space
def resizeEvent(self, e):
super().resizeEvent(e)
cr = self.contentsRect()
self.line_number_area.setGeometry(QRect(cr.left(),
cr.top(),
self.line_number_area_width(),
cr.height()))
def line_number_area_paint_event(self, event):
with QPainter(self.line_number_area) as painter:
painter.fillRect(event.rect(), Qt.white)
painter.setFont("Consolas")
block = self.firstVisibleBlock()
block_number = block.blockNumber()
top = self.blockBoundingGeometry(block).translated(self.contentOffset()).top()
bottom = top + self.blockBoundingRect(block).height()
while block.isValid() and top <= event.rect().bottom():
if block.isVisible() and bottom >= event.rect().top():
painter.setPen(Qt.gray)
painter.drawText(-2, top,
self.line_number_area.width(),
self.fontMetrics().height(),
Qt.AlignmentFlag.AlignRight,
f"{block_number + 1}")
block = block.next()
top = bottom
bottom = top + self.blockBoundingRect(block).height()
block_number += 1
def update_line_number_area_width(self):
self.setViewportMargins(self.line_number_area_width(), 0, 0, 0)
def update_line_number_area(self, rect, dy):
if dy:
self.line_number_area.scroll(0, dy)
else:
self.line_number_area.update(0, rect.y(), self.line_number_area.width(), rect.height())
if rect.contains(self.viewport().rect()):
self.update_line_number_area_width()
def highlight_current_line(self):
extra_selections = []
if not self.isReadOnly():
selection = QTextEdit.ExtraSelection()
selection.format.setBackground(QColor(Qt.lightGray))
selection.format.setProperty(QTextFormat.FullWidthSelection, True)
selection.cursor = self.textCursor()
selection.cursor.clearSelection()
extra_selections.append(selection)
self.setExtraSelections(extra_selections)
class SpinBoxWithoutWheel(QSpinBox):
def wheelEvent(self, event):
event.ignore()
class DoubleSpinBoxWithoutWheel(QDoubleSpinBox):
def wheelEvent(self, event):
event.ignore()
class ProbabilitySpinBox(DoubleSpinBoxWithoutWheel):
def __init__(self):
super().__init__()
self.setSingleStep(0.01)
self.valueChanged.connect(self.update_color)
self.update_color()
self.setMinimumWidth(75)
def update_color(self):
if self.value() > 0:
self.setStyleSheet("color: black;")
else:
self.setStyleSheet("color: gray;")
class TimeEditWithoutWheel(QTimeEdit):
def wheelEvent(self, event):
event.ignore()
class DirWidget(QWidget):
def __init__(self, default_dir: Path | str | None = None):
super().__init__()
self.directory = default_dir
layout = QHBoxLayout()
layout.setSpacing(2)
layout.setContentsMargins(0, 0, 0, 0)
self.dir_lineedit = QLineEdit()
self.dir_lineedit.setFont("Consolas")
self.dir_lineedit.setText(self.directory)
self.dir_set_btn = QPushButton()
self.dir_set_btn.setIcon(QIcon("./gui/icons/ic_folder_open_48px.svg"))
self.dir_set_btn.setIconSize(QSize(16, 16))
self.dir_set_btn.setToolTip("Set directory with selection dialog")
self.dir_set_btn.clicked.connect(self.set_dir)
self.dir_open_btn = QPushButton()
self.dir_open_btn.setIcon(QIcon("./gui/icons/ic_open_in_new_24px.svg"))
self.dir_open_btn.setIconSize(QSize(16, 16))
self.dir_open_btn.setToolTip("Open directory")
self.dir_open_btn.clicked.connect(self.open_dir)
layout.addWidget(self.dir_set_btn)
layout.addWidget(self.dir_lineedit)
layout.addWidget(self.dir_open_btn)
self.setLayout(layout)
def open_dir(self):
directory = Path(self.directory)
os.startfile(directory)
def set_dir(self):
directory = QFileDialog.getExistingDirectory(self, "Select a directory.", dir=self.directory)
if directory:
directory = Path(directory)
if Path.cwd() in directory.parents:
self.directory = "./" + directory.relative_to(Path.cwd()).as_posix()
elif Path.cwd() == directory:
self.directory = "."
else:
self.directory = directory.as_posix()
self.dir_lineedit.setText(self.directory)
class UrlLineEdit(QLineEdit):
def __init__(self, initial_text: str | None = None):
super().__init__()
self.setFont("Consolas")
self.textChanged.connect(self.color_text)
self.setText(initial_text)
def mousePressEvent(self, event):
modifiers = QApplication.keyboardModifiers()
if modifiers == Qt.ControlModifier:
self.open_url()
def open_url(self):
if validators.url(self.text()):
webbrowser.open(self.text())
def color_text(self):
if validators.url(self.text()):
self.setStyleSheet("color: blue;")
else:
self.setStyleSheet("color: black;")
class UrlWidget(QWidget):
def __init__(self, initial_url: str | None = None):
super().__init__()
layout = QHBoxLayout()
layout.setSpacing(2)
layout.setContentsMargins(0, 0, 0, 0)
self.url_lineedit = UrlLineEdit(initial_url)
self.url_lineedit.textChanged.connect(self.toggle_url_open_btn)
self.url_open_btn = QPushButton()
self.url_open_btn.setIcon(QIcon("./gui/icons/ic_open_in_new_24px.svg"))
self.url_open_btn.setIconSize(QSize(16, 16))
self.url_open_btn.setToolTip("Open in browser")
self.url_open_btn.clicked.connect(self.url_lineedit.open_url)
self.toggle_url_open_btn()
layout.addWidget(self.url_lineedit)
layout.addWidget(self.url_open_btn)
self.setLayout(layout)
def toggle_url_open_btn(self):
url_valid = bool(validators.url(self.url_lineedit.text()))
self.url_open_btn.setEnabled(url_valid)
class TelegraphingCheckbox(QCheckBox):
hover = Signal(str)
def __init__(self, text: str, description_text: str | None = None):
super().__init__(text)
self.description_text = description_text
def enterEvent(self, event):
self.hover.emit(self.description_text)
def leaveEvent(self, event):
self.hover.emit("")

View File

@ -289,7 +289,7 @@ def create_deploy_frame(container, config):
frame_3.pack(side="top", fill="x") frame_3.pack(side="top", fill="x")
does_ffmpeg_exists() #does_ffmpeg_exists()
return frame return frame

16
pyproject.toml 100644
View File

@ -0,0 +1,16 @@
[tool.poetry]
name = "barotrauma-sunken-tapes"
version = "0.1.0"
description = ""
authors = ["Jaka <jaka@kompot.si>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.10"
pyyaml = "^6.0.1"
validators = "^0.22.0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

0
requirements.txt 100644
View File

View File

@ -27,7 +27,7 @@
<InventoryIcon texture="%ModDir%/players_icons.png" sourcerect="0,0,64,42" origin="0.5,0.5" /> <InventoryIcon texture="%ModDir%/players_icons.png" sourcerect="0,0,64,42" origin="0.5,0.5" />
<Sprite texture="%ModDir%/players_sprites.png" sourcerect="0,0,117,76" depth="0.55" origin="0.5,0.5" /> <Sprite texture="%ModDir%/players_sprites.png" sourcerect="0,0,117,76" depth="0.55" origin="0.5,0.5" />
<Body width="117" height="76" /> <Body width="117" height="76" />
<LightComponent LightColor="0.0,0.85,0.0,0.7" range="4" powerconsumption="0" blinkfrequency="0" IsOn="false" canbeselected="false"></LightComponent> <LightComponent LightColor="0.0,0.85,0.0,0.7" range="4" powerconsumption="0" blinkfrequency="0" IsOn="false" canbeselected="false" />
<CustomInterface canbeselected="true" drawhudwhenequipped="true" allowuioverlap="true" msg="Equip to control"> <CustomInterface canbeselected="true" drawhudwhenequipped="true" allowuioverlap="true" msg="Equip to control">
<GuiFrame relativesize="0.10,0.07" anchor="CenterLeft" pivot="BottomLeft" relativeoffset="0.006,-0.05" style="ItemUI" /> <GuiFrame relativesize="0.10,0.07" anchor="CenterLeft" pivot="BottomLeft" relativeoffset="0.006,-0.05" style="ItemUI" />
<TickBox text="Play"> <TickBox text="Play">
@ -66,7 +66,7 @@
<Price locationtype="military" multiplier="1.1" sold="false" /> <Price locationtype="military" multiplier="1.1" sold="false" />
<Price locationtype="mine" multiplier="1.1" sold="false" /> <Price locationtype="mine" multiplier="1.1" sold="false" />
</Price> </Price>
<Deconstruct time="20"></Deconstruct> <Deconstruct time="20"/>
<Fabricate suitablefabricators="fabricator" requiredtime="45"> <Fabricate suitablefabricators="fabricator" requiredtime="45">
<RequiredSkill identifier="mechanical" level="45" /> <RequiredSkill identifier="mechanical" level="45" />
<RequiredSkill identifier="electrical" level="55" /> <RequiredSkill identifier="electrical" level="55" />
@ -79,7 +79,7 @@
<InventoryIcon texture="%ModDir%/players_icons.png" sourcerect="64,0,64,40" origin="0.5,0.5" /> <InventoryIcon texture="%ModDir%/players_icons.png" sourcerect="64,0,64,40" origin="0.5,0.5" />
<Sprite texture="%ModDir%/players_sprites.png" sourcerect="117,0,51,33" depth="0.55" origin="0.5,0.5" /> <Sprite texture="%ModDir%/players_sprites.png" sourcerect="117,0,51,33" depth="0.55" origin="0.5,0.5" />
<Body width="51" height="33" /> <Body width="51" height="33" />
<LightComponent LightColor="0.0,0.0,0.0,0.0" range="1" powerconsumption="0" blinkfrequency="0" IsOn="false" canbeselected="false"></LightComponent> <LightComponent LightColor="0.0,0.0,0.0,0.0" range="1" powerconsumption="0" blinkfrequency="0" IsOn="false" canbeselected="false"/>
<CustomInterface canbeselected="true" drawhudwhenequipped="true" allowuioverlap="true" msg="Equip to control"> <CustomInterface canbeselected="true" drawhudwhenequipped="true" allowuioverlap="true" msg="Equip to control">
<GuiFrame relativesize="0.10,0.07" anchor="CenterLeft" pivot="BottomLeft" relativeoffset="0.006,-0.05" style="ItemUI" /> <GuiFrame relativesize="0.10,0.07" anchor="CenterLeft" pivot="BottomLeft" relativeoffset="0.006,-0.05" style="ItemUI" />
<TickBox text="Play"> <TickBox text="Play">
@ -118,7 +118,7 @@
<Price locationtype="military" multiplier="1.1" sold="false" /> <Price locationtype="military" multiplier="1.1" sold="false" />
<Price locationtype="mine" multiplier="1.1" sold="false" /> <Price locationtype="mine" multiplier="1.1" sold="false" />
</Price> </Price>
<Deconstruct time="20"></Deconstruct> <Deconstruct time="20"/>
<Fabricate suitablefabricators="fabricator" requiredtime="45"> <Fabricate suitablefabricators="fabricator" requiredtime="45">
<RequiredSkill identifier="mechanical" level="45" /> <RequiredSkill identifier="mechanical" level="45" />
<RequiredSkill identifier="electrical" level="55" /> <RequiredSkill identifier="electrical" level="55" />
@ -131,7 +131,7 @@
<InventoryIcon texture="%ModDir%/players_icons.png" sourcerect="128,0,64,40" origin="0.5,0.5" /> <InventoryIcon texture="%ModDir%/players_icons.png" sourcerect="128,0,64,40" origin="0.5,0.5" />
<Sprite texture="%ModDir%/players_sprites.png" sourcerect="168,0,51,33" depth="0.55" origin="0.5,0.5" /> <Sprite texture="%ModDir%/players_sprites.png" sourcerect="168,0,51,33" depth="0.55" origin="0.5,0.5" />
<Body width="51" height="33" /> <Body width="51" height="33" />
<LightComponent LightColor="0.0,0.0,0.0,0.0" range="1" powerconsumption="0" blinkfrequency="0" IsOn="false" canbeselected="false"></LightComponent> <LightComponent LightColor="0.0,0.0,0.0,0.0" range="1" powerconsumption="0" blinkfrequency="0" IsOn="false" canbeselected="false"/>
<CustomInterface canbeselected="true" drawhudwhenequipped="true" allowuioverlap="true" msg="Equip to control"> <CustomInterface canbeselected="true" drawhudwhenequipped="true" allowuioverlap="true" msg="Equip to control">
<GuiFrame relativesize="0.10,0.07" anchor="CenterLeft" pivot="BottomLeft" relativeoffset="0.006,-0.05" style="ItemUI" /> <GuiFrame relativesize="0.10,0.07" anchor="CenterLeft" pivot="BottomLeft" relativeoffset="0.006,-0.05" style="ItemUI" />
<TickBox text="Play"> <TickBox text="Play">
@ -170,7 +170,7 @@
<Price locationtype="military" multiplier="1.1" sold="false" /> <Price locationtype="military" multiplier="1.1" sold="false" />
<Price locationtype="mine" multiplier="1.0" minavailable="1" /> <Price locationtype="mine" multiplier="1.0" minavailable="1" />
</Price> </Price>
<Deconstruct time="20"></Deconstruct> <Deconstruct time="20"/>
<Fabricate suitablefabricators="fabricator" requiredtime="45"> <Fabricate suitablefabricators="fabricator" requiredtime="45">
<RequiredSkill identifier="mechanical" level="45" /> <RequiredSkill identifier="mechanical" level="45" />
<RequiredSkill identifier="electrical" level="55" /> <RequiredSkill identifier="electrical" level="55" />
@ -183,7 +183,7 @@
<InventoryIcon texture="%ModDir%/players_icons.png" sourcerect="192,0,64,40" origin="0.5,0.5" /> <InventoryIcon texture="%ModDir%/players_icons.png" sourcerect="192,0,64,40" origin="0.5,0.5" />
<Sprite texture="%ModDir%/players_sprites.png" sourcerect="219,0,51,33" depth="0.55" origin="0.5,0.5" /> <Sprite texture="%ModDir%/players_sprites.png" sourcerect="219,0,51,33" depth="0.55" origin="0.5,0.5" />
<Body width="51" height="33" /> <Body width="51" height="33" />
<LightComponent LightColor="0.0,0.0,0.0,0.0" range="1" powerconsumption="0" blinkfrequency="0" IsOn="false" canbeselected="false"></LightComponent> <LightComponent LightColor="0.0,0.0,0.0,0.0" range="1" powerconsumption="0" blinkfrequency="0" IsOn="false" canbeselected="false"/>
<CustomInterface canbeselected="true" drawhudwhenequipped="true" allowuioverlap="true" msg="Equip to control"> <CustomInterface canbeselected="true" drawhudwhenequipped="true" allowuioverlap="true" msg="Equip to control">
<GuiFrame relativesize="0.10,0.07" anchor="CenterLeft" pivot="BottomLeft" relativeoffset="0.006,-0.05" style="ItemUI" /> <GuiFrame relativesize="0.10,0.07" anchor="CenterLeft" pivot="BottomLeft" relativeoffset="0.006,-0.05" style="ItemUI" />
<TickBox text="Play"> <TickBox text="Play">
@ -222,7 +222,7 @@
<Price locationtype="military" multiplier="1.0" minavailable="1" /> <Price locationtype="military" multiplier="1.0" minavailable="1" />
<Price locationtype="mine" multiplier="1.1" sold="false" /> <Price locationtype="mine" multiplier="1.1" sold="false" />
</Price> </Price>
<Deconstruct time="20"></Deconstruct> <Deconstruct time="20"/>
<Fabricate suitablefabricators="fabricator" requiredtime="45"> <Fabricate suitablefabricators="fabricator" requiredtime="45">
<RequiredSkill identifier="mechanical" level="45" /> <RequiredSkill identifier="mechanical" level="45" />
<RequiredSkill identifier="electrical" level="55" /> <RequiredSkill identifier="electrical" level="55" />
@ -235,7 +235,7 @@
<InventoryIcon texture="%ModDir%/players_icons.png" sourcerect="256,0,64,40" origin="0.5,0.5" /> <InventoryIcon texture="%ModDir%/players_icons.png" sourcerect="256,0,64,40" origin="0.5,0.5" />
<Sprite texture="%ModDir%/players_sprites.png" sourcerect="270,0,51,33" depth="0.55" origin="0.5,0.5" /> <Sprite texture="%ModDir%/players_sprites.png" sourcerect="270,0,51,33" depth="0.55" origin="0.5,0.5" />
<Body width="51" height="33" /> <Body width="51" height="33" />
<LightComponent LightColor="0.0,0.0,0.0,0.0" range="1" powerconsumption="0" blinkfrequency="0" IsOn="false" canbeselected="false"></LightComponent> <LightComponent LightColor="0.0,0.0,0.0,0.0" range="1" powerconsumption="0" blinkfrequency="0" IsOn="false" canbeselected="false"/>
<CustomInterface canbeselected="true" drawhudwhenequipped="true" allowuioverlap="true" msg="Equip to control"> <CustomInterface canbeselected="true" drawhudwhenequipped="true" allowuioverlap="true" msg="Equip to control">
<GuiFrame relativesize="0.10,0.07" anchor="CenterLeft" pivot="BottomLeft" relativeoffset="0.006,-0.05" style="ItemUI" /> <GuiFrame relativesize="0.10,0.07" anchor="CenterLeft" pivot="BottomLeft" relativeoffset="0.006,-0.05" style="ItemUI" />
<TickBox text="Play"> <TickBox text="Play">
@ -273,7 +273,7 @@
<Price baseprice="{{ tape.price }}" soldeverywhere="false">{% for location in ["outpost", "city", "research", "military", "mine"] %} <Price baseprice="{{ tape.price }}" soldeverywhere="false">{% for location in ["outpost", "city", "research", "military", "mine"] %}
<Price locationtype="{{ location }}" multiplier="{{ tape.multipliers[loop.index0] }}" sold="{{ tape.sold[loop.index0] }}" minavailable="1" />{% endfor %} <Price locationtype="{{ location }}" multiplier="{{ tape.multipliers[loop.index0] }}" sold="{{ tape.sold[loop.index0] }}" minavailable="1" />{% endfor %}
</Price> </Price>
<Deconstruct time="10"></Deconstruct> <Deconstruct time="10"/>
<Fabricate suitablefabricators="fabricator" displayname="recycleitem" requiredtime="{{ song_lengths[loop.index0]*0.5 }}"> <Fabricate suitablefabricators="fabricator" displayname="recycleitem" requiredtime="{{ song_lengths[loop.index0]*0.5 }}">
<RequiredSkill identifier="mechanical" level="25" /> <RequiredSkill identifier="mechanical" level="25" />
<RequiredSkill identifier="electrical" level="45" /> <RequiredSkill identifier="electrical" level="45" />

File diff suppressed because it is too large Load Diff