Merge branch 'master' of git.zavage.net:Zavage-Software/smileyface

This commit is contained in:
Mathew Guest 2024-08-15 05:09:35 -06:00
commit ff06cc59eb
22 changed files with 867 additions and 404 deletions

11
.flake8 Normal file

@ -0,0 +1,11 @@
[flake8]
max-line-length=120
ignore =
E121,
E123,
E126,
E226,
E24,
E704,
W605
exclude = ./tests

1
.gitignore vendored

@ -3,4 +3,5 @@ build/
dist/ dist/
__pycache__ __pycache__
*.egg-info *.egg-info
idea

37
.pre-commit-config.yaml Normal file

@ -0,0 +1,37 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-yaml
- id: check-added-large-files
args: ['--maxkb=10240']
- id: check-merge-conflict
- id: end-of-file-fixer
- id: trailing-whitespace
# isort -- sorts imports
- repo: https://github.com/timothycrosley/isort
rev: 5.13.2
hooks:
- id: isort
args: ["--profile", "black", "--filter-files"]
# Flake8
#- repo: https://github.com/pycqa/flake8
# rev: '7.0.0'
# hooks:
# - id: flake8
# Black
# Using this mirror lets us use mypyc-compiled black, which is about 2x faster
- repo: https://github.com/psf/black-pre-commit-mirror
rev: 24.2.0
hooks:
- id: black
language_version: python3.8
# Poetry
- repo: https://github.com/python-poetry/poetry
rev: 1.8.2
hooks:
- id: poetry-lock

1
.python-version Normal file

@ -0,0 +1 @@
3.8.19

@ -1,6 +1,6 @@
SmileyFace Unreal Tournament 4 Hub Automator SmileyFace Unreal Tournament 4 Hub Automator
Copyright (c) 2020 Mathew Guest Copyright (c) 2024 Mathew Guest
Permission is hereby granted, free of charge, to any person Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation obtaining a copy of this software and associated documentation

440
poetry.lock generated Normal file

@ -0,0 +1,440 @@
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "app-skellington"
version = "0.1.1"
description = "A high-powered command line menu framework."
optional = false
python-versions = ">=3"
files = [
{file = "app_skellington-0.1.1-py3-none-any.whl", hash = "sha256:61853a63b6683a3bccbca87c34d19352c1c5c4ab80fb4ff5fa77ebc0cf3348af"},
{file = "app_skellington-0.1.1.tar.gz", hash = "sha256:d9ea7423f8e9434724065e528f89b35182abe9b338e6997397d46e98e7f87a0a"},
]
[package.dependencies]
appdirs = "*"
colorlog = "*"
configobj = "*"
[[package]]
name = "appdirs"
version = "1.4.4"
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
optional = false
python-versions = "*"
files = [
{file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"},
{file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"},
]
[[package]]
name = "black"
version = "24.4.2"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.8"
files = [
{file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"},
{file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"},
{file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"},
{file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"},
{file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"},
{file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"},
{file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"},
{file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"},
{file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"},
{file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"},
{file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"},
{file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"},
{file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"},
{file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"},
{file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"},
{file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"},
{file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"},
{file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"},
{file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"},
{file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"},
{file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"},
{file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"},
]
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "cfgv"
version = "3.4.0"
description = "Validate configuration and produce human readable error messages."
optional = false
python-versions = ">=3.8"
files = [
{file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
{file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
]
[[package]]
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
files = [
{file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
{file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "colorlog"
version = "6.8.2"
description = "Add colours to the output of Python's logging module."
optional = false
python-versions = ">=3.6"
files = [
{file = "colorlog-6.8.2-py3-none-any.whl", hash = "sha256:4dcbb62368e2800cb3c5abd348da7e53f6c362dda502ec27c560b2e58a66bd33"},
{file = "colorlog-6.8.2.tar.gz", hash = "sha256:3e3e079a41feb5a1b64f978b5ea4f46040a94f11f0e8bbb8261e3dbbeca64d44"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
[package.extras]
development = ["black", "flake8", "mypy", "pytest", "types-colorama"]
[[package]]
name = "configobj"
version = "5.0.8"
description = "Config file reading, writing and validation."
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
files = [
{file = "configobj-5.0.8-py2.py3-none-any.whl", hash = "sha256:a7a8c6ab7daade85c3f329931a807c8aee750a2494363934f8ea84d8a54c87ea"},
{file = "configobj-5.0.8.tar.gz", hash = "sha256:6f704434a07dc4f4dc7c9a745172c1cad449feb548febd9f7fe362629c627a97"},
]
[package.dependencies]
six = "*"
[[package]]
name = "distlib"
version = "0.3.8"
description = "Distribution utilities"
optional = false
python-versions = "*"
files = [
{file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
{file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
]
[[package]]
name = "filelock"
version = "3.15.4"
description = "A platform independent file lock."
optional = false
python-versions = ">=3.8"
files = [
{file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"},
{file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"},
]
[package.extras]
docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"]
typing = ["typing-extensions (>=4.8)"]
[[package]]
name = "flake8"
version = "5.0.4"
description = "the modular source code checker: pep8 pyflakes and co"
optional = false
python-versions = ">=3.6.1"
files = [
{file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"},
{file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"},
]
[package.dependencies]
mccabe = ">=0.7.0,<0.8.0"
pycodestyle = ">=2.9.0,<2.10.0"
pyflakes = ">=2.5.0,<2.6.0"
[[package]]
name = "identify"
version = "2.6.0"
description = "File identification library for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "identify-2.6.0-py2.py3-none-any.whl", hash = "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0"},
{file = "identify-2.6.0.tar.gz", hash = "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf"},
]
[package.extras]
license = ["ukkonen"]
[[package]]
name = "isort"
version = "5.13.2"
description = "A Python utility / library to sort Python imports."
optional = false
python-versions = ">=3.8.0"
files = [
{file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
{file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
]
[package.extras]
colors = ["colorama (>=0.4.6)"]
[[package]]
name = "mccabe"
version = "0.7.0"
description = "McCabe checker, plugin for flake8"
optional = false
python-versions = ">=3.6"
files = [
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.5"
files = [
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]]
name = "nodeenv"
version = "1.9.1"
description = "Node.js virtual environment builder"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
{file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
]
[[package]]
name = "packaging"
version = "24.1"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
files = [
{file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
{file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
]
[[package]]
name = "pathspec"
version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.8"
files = [
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
]
[[package]]
name = "platformdirs"
version = "4.2.2"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.8"
files = [
{file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
{file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
]
[package.extras]
docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
type = ["mypy (>=1.8)"]
[[package]]
name = "pre-commit"
version = "3.5.0"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false
python-versions = ">=3.8"
files = [
{file = "pre_commit-3.5.0-py2.py3-none-any.whl", hash = "sha256:841dc9aef25daba9a0238cd27984041fa0467b4199fc4852e27950664919f660"},
{file = "pre_commit-3.5.0.tar.gz", hash = "sha256:5804465c675b659b0862f07907f96295d490822a450c4c40e747d0b1c6ebcb32"},
]
[package.dependencies]
cfgv = ">=2.0.0"
identify = ">=1.0.0"
nodeenv = ">=0.11.1"
pyyaml = ">=5.1"
virtualenv = ">=20.10.0"
[[package]]
name = "pycodestyle"
version = "2.9.1"
description = "Python style guide checker"
optional = false
python-versions = ">=3.6"
files = [
{file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"},
{file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"},
]
[[package]]
name = "pyflakes"
version = "2.5.0"
description = "passive checker of Python programs"
optional = false
python-versions = ">=3.6"
files = [
{file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"},
{file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"},
]
[[package]]
name = "pyyaml"
version = "6.0.1"
description = "YAML parser and emitter for Python"
optional = false
python-versions = ">=3.6"
files = [
{file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
{file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
{file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
{file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
{file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
{file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
{file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
{file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
{file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
{file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
{file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
{file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
{file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
]
[[package]]
name = "six"
version = "1.16.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
files = [
{file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
{file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
]
[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.7"
files = [
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
[[package]]
name = "typing-extensions"
version = "4.12.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
files = [
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
]
[[package]]
name = "virtualenv"
version = "20.26.3"
description = "Virtual Python Environment builder"
optional = false
python-versions = ">=3.7"
files = [
{file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"},
{file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"},
]
[package.dependencies]
distlib = ">=0.3.7,<1"
filelock = ">=3.12.2,<4"
platformdirs = ">=3.9.1,<5"
[package.extras]
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
[metadata]
lock-version = "2.0"
python-versions = "^3.8"
content-hash = "c2001f64c789fcc98f1fc1702a4a511e788776a0c7d4c438d66193f0a1241c0c"

51
pyproject.toml Normal file

@ -0,0 +1,51 @@
[tool.poetry]
name = "smileyface"
version = "0.1.0"
description = "smileyface UT4 hub automator hosting"
authors = [
"Mathew Guest <mat@zavage.net>",
]
license = "MIT"
readme = "README.md"
homepage = "https://zavage-software.com/portfolio/smileyface"
repository = "https://git-mirror.zavage.net/zavage-software/smileyface"
documentation = "https://git-mirror.zavage.net/zavage-software/smileyface"
keywords = ["cas"]
packages = [{ include = "smileyface" }]
include = [
"README.md",
]
# [tool.poetry.scripts]
[tool.poetry.dependencies]
python = "^3.8"
app_skellington = "*"
configobj = "*"
colorlog = "*"
appdirs = "*"
[tool.poetry.group.dev.dependencies]
black = "*"
pre-commit = "*"
isort = "*"
flake8 = "*"
#Sphinx = "^5.3.0"
#sphinx-rtd-theme = "^1.3.0"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.black]
line-length = 120
target-version = ['py38']
[tool.isort]
multi_line_output = 3
combine_as_imports = true
include_trailing_comma = true
force_grid_wrap = 3
ensure_newline_before_comments = true

@ -1,35 +1,25 @@
#!/usr/bin/env python #!/usr/bin/env python
from setuptools import setup, find_packages from setuptools import find_packages, setup
__project__ = 'SmileyFace UT4 Hub Automator' __project__ = "SmileyFace UT4 Hub Automator"
__version__ = '0.1.0' __version__ = "0.1.0"
app_skellington_requirements = ( app_skellington_requirements = (
'appdirs', "appdirs",
'colorlog', "colorlog",
'configobj', "configobj",
) )
setup( setup(
name = __project__, name=__project__,
version = __version__, version=__version__,
description = 'Unreal Tournament 4 Server Admin and Control Panel', description="Unreal Tournament 4 Server Admin and Control Panel",
author = 'Mathew Guest', author="Mathew Guest",
author_email = 't3h.zavage@gmail.com', author_email="t3h.zavage@gmail.com",
url = 'https://git-mirror.zavage-software.com', url="https://git-mirror.zavage-software.com",
# Third-party dependencies; will be automatically installed # Third-party dependencies; will be automatically installed
install_requires = ( install_requires=("rdiff-backup", "app_skellington", "appdirs", "sqlparse") + app_skellington_requirements,
'rdiff-backup', packages=find_packages(),
'app_skellington', package_dir={"app_skellington": "lib/app_skellington"},
'appdirs',
'sqlparse'
) + app_skellington_requirements,
packages = find_packages(),
package_dir = {
'app_skellington': 'lib/app_skellington'
},
) )

@ -1,4 +1,4 @@
#!/usr/bin/env python #!/usr/bin/env python
import smileyface import smileyface
smileyface.start_app()
smileyface.start_app()

@ -2,18 +2,20 @@ import logging
import sys import sys
# Module parameters and constants # Module parameters and constants
APP_NAME = 'SmileyFace Unreal Tournament 4 Server Panel' APP_NAME = "SmileyFace Unreal Tournament 4 Server Panel"
APP_AUTHOR = 'Mathew Guest' APP_AUTHOR = "Mathew Guest"
APP_VERSION = '0.1.0' APP_VERSION = "0.1.0"
APP_CONFIG_FILENAME = 'config.ini' APP_CONFIG_FILENAME = "config.ini"
# config.spec is relative to the module src directory and is the # config.spec is relative to the module src directory and is the
# config specification (structure, names, and types of config file) # config specification (structure, names, and types of config file)
APP_CONFIGSPEC_FILENAME = 'config.spec' APP_CONFIGSPEC_FILENAME = "config.spec"
# Check and gracefully fail if the user needs to install a 3rd-party dep. # Check and gracefully fail if the user needs to install a 3rd-party dep.
required_lib_names = ['appdirs', 'configobj', 'colorlog'] required_lib_names = ["appdirs", "configobj", "colorlog"]
def check_env_has_dependencies(required_lib_names): def check_env_has_dependencies(required_lib_names):
""" """
Attempts to import each module and gracefully fails if it doesn't Attempts to import each module and gracefully fails if it doesn't
@ -24,15 +26,17 @@ def check_env_has_dependencies(required_lib_names):
try: try:
__import__(libname) __import__(libname)
except ImportError as ex: except ImportError as ex:
print('missing third-part library: ', ex, file=sys.stderr) print("missing third-part library: ", ex, file=sys.stderr)
rc = False rc = False
except Exception as ex: except Exception as ex:
print(ex, type(ex)) print(ex, type(ex))
rc = False rc = False
return rc return rc
if not check_env_has_dependencies(required_lib_names): if not check_env_has_dependencies(required_lib_names):
print('refusing to load program without installed dependencies', file=sys.stderr) print("refusing to load program without installed dependencies", file=sys.stderr)
raise ImportError('python environment needs third-party dependencies installed') raise ImportError("python environment needs third-party dependencies installed")
# Exposed from sub-modules: # Exposed from sub-modules:
from .app import start_app from .app import start_app

@ -1,11 +1,11 @@
import hashlib
import functools import functools
import hashlib
def md5sum_file(filename): def md5sum_file(filename):
with open(filename, mode='rb') as f: with open(filename, mode="rb") as f:
d = hashlib.md5() d = hashlib.md5()
for buf in iter(functools.partial(f.read, 128), b''): for buf in iter(functools.partial(f.read, 128), b""):
d.update(buf) d.update(buf)
h = d.hexdigest() h = d.hexdigest()
return h return h

@ -1,63 +1,49 @@
from . import hub_machine
from . import datalayer
from . import scrape_latest
import app_skellington import app_skellington
from app_skellington import _util from app_skellington import _util
from . import (
datalayer,
hub_machine,
scrape_latest,
)
class SmileyFace(app_skellington.ApplicationContainer): class SmileyFace(app_skellington.ApplicationContainer):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
filename = 'config.spec' filename = "config.spec"
self.configspec_filepath = _util.get_asset(__name__, filename) self.configspec_filepath = _util.get_asset(__name__, filename)
config_filepath = self._get_config_filepath( config_filepath = self._get_config_filepath("smileyface-ut4", "", "hub-config.ini")
'smileyface-ut4',
'',
'hub-config.ini'
)
super().__init__( super().__init__(
configspec_filepath=self.configspec_filepath, configspec_filepath=self.configspec_filepath,
configini_filepath=config_filepath, configini_filepath=config_filepath,
app_name = 'SmileyFace UT4 Server Panel', app_name="SmileyFace UT4 Server Panel",
app_author = 'Mathew Guest', app_author="Mathew Guest",
app_version = '0.1', app_version="0.1",
*args, *args,
**kwargs **kwargs,
) )
def _cli_options(self): def _cli_options(self):
pass pass
def _command_menu(self): def _command_menu(self):
sm_root = self.cli.init_submenu('command') sm_root = self.cli.init_submenu("command")
_util.register_class_as_commands( _util.register_class_as_commands(self, sm_root, hub_machine.UT4ServerMachine)
self, sm_root,
hub_machine.UT4ServerMachine
)
sm_scrape = sm_root.create_submenu('scrape') sm_scrape = sm_root.create_submenu("scrape")
_util.register_class_as_commands( _util.register_class_as_commands(self, sm_scrape, scrape_latest.ScrapeUt4Pugs)
self, sm_scrape,
scrape_latest.ScrapeUt4Pugs
)
_util.register_class_as_commands( _util.register_class_as_commands(self, sm_scrape, scrape_latest.ScrapeUtcc)
self, sm_scrape,
scrape_latest.ScrapeUtcc
)
_util.register_class_as_commands( _util.register_class_as_commands(self, sm_scrape, scrape_latest.LocalFs)
self, sm_scrape,
scrape_latest.LocalFs
)
def _services(self): def _services(self):
self['model'] = lambda: hub_machine.UTServerMachine(self.ctx) self["model"] = lambda: hub_machine.UTServerMachine(self.ctx)
self.dal = datalayer.DataLayer(self.ctx) self.dal = datalayer.DataLayer(self.ctx)
self['dal'] = lambda: self.dal self["dal"] = lambda: self.dal
self['datalayer'] = lambda: datalayer.DbFuncs(self.ctx, self.dal) self["datalayer"] = lambda: datalayer.DbFuncs(self.ctx, self.dal)
# self['localfs'] = lambda: datalayer.LocalFs(self.ctx, datalayer) # self['localfs'] = lambda: datalayer.LocalFs(self.ctx, datalayer)
@ -67,7 +53,7 @@ class SmileyFace(app_skellington.ApplicationContainer):
def invoke_from_cli(self): def invoke_from_cli(self):
rc = self.load_command() rc = self.load_command()
if not rc: if not rc:
print('Invalid command. Try -h for usage') print("Invalid command. Try -h for usage")
return return
# load config # load config
self.invoke_command() self.invoke_command()
@ -142,4 +128,3 @@ Typical Usage:
def start_app(): def start_app():
app = SmileyFace() app = SmileyFace()
app.invoke_from_cli() app.invoke_from_cli()

@ -1,8 +1,9 @@
from smileyface import myutil import os
import sqlite3
import appdirs import appdirs
import sqlite3
import os from smileyface import myutil
class DataLayer: class DataLayer:
@ -17,10 +18,10 @@ class DataLayer:
return self._db_conn return self._db_conn
def _create_db_connection(self): def _create_db_connection(self):
local_db_filename = self.ctx.config['app']['sqlite_filename'] local_db_filename = self.ctx.config["app"]["sqlite_filename"]
appdir = appdirs.user_data_dir('smileyface') appdir = appdirs.user_data_dir("smileyface")
fullpath = os.path.join(appdir, local_db_filename) fullpath = os.path.join(appdir, local_db_filename)
self.ctx.log['ut4'].info('sqlite3 filename: %s', fullpath) self.ctx.log["ut4"].info("sqlite3 filename: %s", fullpath)
myutil.ensure_dir_exists(fullpath) myutil.ensure_dir_exists(fullpath)
@ -28,9 +29,5 @@ class DataLayer:
return db return db
def commit(self): def commit(self):
self.ctx.log['db'].info('commit()') self.ctx.log["db"].info("commit()")
self.db_conn.commit() self.db_conn.commit()

@ -1,12 +1,12 @@
from smileyface import myutil import datetime
from smileyface import structs import os
import app_skellington._util as apputil import app_skellington._util as apputil
import appdirs import appdirs
import datetime
import os
import sqlparse import sqlparse
from smileyface import myutil, structs
class DbFuncs: class DbFuncs:
def __init__(self, ctx, dal): def __init__(self, ctx, dal):
@ -14,7 +14,7 @@ class DbFuncs:
self.dal = dal self.dal = dal
def create_tables(self): def create_tables(self):
sql_filename = apputil.get_asset(__name__, 'create_schema.sql') sql_filename = apputil.get_asset(__name__, "create_schema.sql")
with open(sql_filename) as fp: with open(sql_filename) as fp:
contents_sql = fp.read() contents_sql = fp.read()
stmts = sqlparse.split(contents_sql) stmts = sqlparse.split(contents_sql)
@ -22,7 +22,7 @@ class DbFuncs:
conn = self.dal.db_conn conn = self.dal.db_conn
curs = conn.cursor() curs = conn.cursor()
for stmt in stmts: for stmt in stmts:
print('----') print("----")
print(stmt) print(stmt)
curs.execute(stmt) curs.execute(stmt)
@ -31,9 +31,9 @@ class DbFuncs:
def truncate_tables(self): def truncate_tables(self):
conn = self.dal.db_conn conn = self.dal.db_conn
curs = conn.cursor() curs = conn.cursor()
sql = ''' sql = """
truncate file_paks; truncate file_paks;
''' """
for stmt in sqlparse.split(sql): for stmt in sqlparse.split(sql):
curs.execute(stmt) curs.execute(stmt)
self.dal.commit() self.dal.commit()
@ -43,7 +43,7 @@ truncate file_paks;
# NOTE(MG) datetime parameter logic could be improved and more complete # NOTE(MG) datetime parameter logic could be improved and more complete
conn = self.dal.db_conn conn = self.dal.db_conn
curs = conn.cursor() curs = conn.cursor()
sql = ''' sql = """
insert into file_paks ( insert into file_paks (
file_pak_id, file_pak_id,
fullpath, fullpath,
@ -63,12 +63,12 @@ on conflict(filename) do update set
md5sum = excluded.md5sum, md5sum = excluded.md5sum,
--created at does not update --created at does not update
record_updated_at = datetime('now', 'localtime') record_updated_at = datetime('now', 'localtime')
''' """
args = ( args = (
record.file_pak_id, record.file_pak_id,
record.fullpath, record.fullpath,
record.filename, record.filename,
record.md5sum record.md5sum,
# record.record_created_at, # record.record_created_at,
# record.record_updated_at # record.record_updated_at
) )
@ -80,7 +80,7 @@ on conflict(filename) do update set
# validation data src # validation data src
conn = self.dal.db_conn conn = self.dal.db_conn
curs = conn.cursor() curs = conn.cursor()
sql = ''' sql = """
update file_paks update file_paks
set set
validated_state = ?, validated_state = ?,
@ -90,7 +90,7 @@ set
where where
file_pak_id = ? file_pak_id = ?
''' """
args = (validate_state, src, remote_src_md5, rec_id) args = (validate_state, src, remote_src_md5, rec_id)
curs.execute(sql, args) curs.execute(sql, args)
conn.commit() conn.commit()
@ -98,14 +98,14 @@ where
def query_filepak(self, filename): def query_filepak(self, filename):
conn = self.dal.db_conn conn = self.dal.db_conn
curs = conn.cursor() curs = conn.cursor()
sql = ''' sql = """
select select
file_pak_id, fullpath, filename, file_pak_id, fullpath, filename,
md5sum, record_created_at, record_updated_at md5sum, record_created_at, record_updated_at
from from
file_paks file_paks
where where
lower(filename) = lower(?)''' lower(filename) = lower(?)"""
args = (filename,) args = (filename,)
# print(sql) # print(sql)
curs.execute(sql, args) curs.execute(sql, args)
@ -126,15 +126,14 @@ where
elif len(output) == 1: elif len(output) == 1:
return output[0] return output[0]
elif len(output) > 1: elif len(output) > 1:
input('<breakpoint> unexpected two rows returned from db when expecting to be unique') input("<breakpoint> unexpected two rows returned from db when expecting to be unique")
return output return output
return output return output
def query_invalid_filepaks(self): def query_invalid_filepaks(self):
conn = self.dal.db_conn conn = self.dal.db_conn
curs = conn.cursor() curs = conn.cursor()
sql = ''' sql = """
select select
file_pak_id, fullpath, filename, file_pak_id, fullpath, filename,
md5sum, md5sum,
@ -143,7 +142,7 @@ select
from from
file_paks file_paks
where where
lower(filename) = lower(?)''' lower(filename) = lower(?)"""
args = (filename,) args = (filename,)
# print(sql) # print(sql)
curs.execute(sql, args) curs.execute(sql, args)
@ -160,7 +159,3 @@ where
filepak.record_updated_at = r[8] filepak.record_updated_at = r[8]
output.append(filepak) output.append(filepak)
return output return output

@ -1,6 +1,7 @@
import configparser import configparser
import re import re
class UnrealIniFile: class UnrealIniFile:
def __init__(self, filename=None): def __init__(self, filename=None):
self._config = None self._config = None
@ -14,12 +15,11 @@ class UnrealIniFile:
def filename(self, val): def filename(self, val):
self._filename = val self._filename = val
if val is not None: if val is not None:
self._config = configparser.RawConfigParser( self._config = configparser.RawConfigParser(strict=False)
strict=False self._config.optionxform = str # Trick to preserve case in key names
)
self._config.optionxform = str # Trick to preserve case in key names
self._config.read(self._filename) self._config.read(self._filename)
class GameIniSpecial: class GameIniSpecial:
def __init__(self, filename): def __init__(self, filename):
self._redirect_lines = [] self._redirect_lines = []
@ -37,24 +37,23 @@ class GameIniSpecial:
def clear_redirect_references(self): def clear_redirect_references(self):
self._redirect_lines = [] self._redirect_lines = []
def add_redirect_reference( def add_redirect_reference(self, pkg_basename, redirect_url, redirect_protocol, relative_path, md5sum):
self, pkg_basename, redirect_url, redirect_protocol,
relative_path, md5sum
):
args = { args = {
'pkg_basename': pkg_basename, "pkg_basename": pkg_basename,
'redirect_protocol': redirect_protocol, "redirect_protocol": redirect_protocol,
'redirect_url': redirect_url, "redirect_url": redirect_url,
'relative_path': relative_path, "relative_path": relative_path,
'md5sum': md5sum "md5sum": md5sum,
} }
########### START multi-line awkward indent ########### START multi-line awkward indent
line = '\ line = '\
RedirectReferences=(PackageName="{pkg_basename}",\ RedirectReferences=(PackageName="{pkg_basename}",\
PackageURLProtocol="{redirect_protocol}",\ PackageURLProtocol="{redirect_protocol}",\
PackageURL="{redirect_url}/{relative_path}",\ PackageURL="{redirect_url}/{relative_path}",\
PackageChecksum="{md5sum}")'.format(**args) PackageChecksum="{md5sum}")'.format(
########### END multi-line awkward indent **args
)
########### END multi-line awkward indent
return self.add_redirect_reference_line(line) return self.add_redirect_reference_line(line)
@ -65,18 +64,13 @@ PackageChecksum="{md5sum}")'.format(**args)
def write(self, fp): def write(self, fp):
newcontents = None newcontents = None
with open(self.filename, 'r') as inifile: with open(self.filename, "r") as inifile:
curcontents = inifile.read() curcontents = inifile.read()
lines_str = '\n'.join(self._redirect_lines) lines_str = "\n".join(self._redirect_lines)
newcontents = re.sub( newcontents = re.sub("RedirectReferences = :PARAM:", lines_str, curcontents)
'RedirectReferences = :PARAM:',
lines_str,
curcontents
)
has_data = True has_data = True
if has_data: if has_data:
with open(self.filename, 'w') as fp: with open(self.filename, "w") as fp:
fp.write(newcontents) fp.write(newcontents)

@ -1,10 +1,4 @@
from .gameconfig_edit import UnrealIniFile, GameIniSpecial
from ._util import md5sum_file
from . import myutil
from . import structs
import collections import collections
import configobj
import configparser import configparser
import datetime import datetime
import glob import glob
@ -14,6 +8,12 @@ import subprocess
import sys import sys
import time import time
import configobj
from . import myutil, structs
from ._util import md5sum_file
from .gameconfig_edit import GameIniSpecial, UnrealIniFile
class UT4ServerMachine: class UT4ServerMachine:
def __init__(self, ctx, datalayer): def __init__(self, ctx, datalayer):
@ -23,8 +23,6 @@ class UT4ServerMachine:
if not self._validate_env_vars(): if not self._validate_env_vars():
sys.exit(1) sys.exit(1)
def oneclickdeploy(self): def oneclickdeploy(self):
self.generate_instance() self.generate_instance()
self.upload_redirects() self.upload_redirects()
@ -34,7 +32,7 @@ class UT4ServerMachine:
""" """
Deletes the generated instance on the local machine. Deletes the generated instance on the local machine.
""" """
self.ctx.log['ut4'].info('Clearing .pak folder...') self.ctx.log["ut4"].info("Clearing .pak folder...")
cmd = 'rm -rv "$PROJECT_DIR"/instance/LinuxServer/UnrealTournament/Content/Paks/*' cmd = 'rm -rv "$PROJECT_DIR"/instance/LinuxServer/UnrealTournament/Content/Paks/*'
self._invoke_command(cmd) self._invoke_command(cmd)
@ -42,61 +40,46 @@ class UT4ServerMachine:
""" """
Create required directories which the user installs maps, mutators, and config to. Create required directories which the user installs maps, mutators, and config to.
""" """
dirs = ( dirs = ("base", "files/config", "files/maps", "files/mutators", "files/rulesets", "files/unused")
'base', project_dir = self.ctx.config["app"]["project_dir"]
'files/config',
'files/maps',
'files/mutators',
'files/rulesets',
'files/unused'
)
project_dir = self.ctx.config['app']['project_dir']
if len(project_dir.strip()) == 0: if len(project_dir.strip()) == 0:
project_dir = '.' project_dir = "."
print('project_dir:', project_dir) print("project_dir:", project_dir)
fullpaths = [ fullpaths = ["/".join([project_dir, d]) for d in dirs]
'/'.join([project_dir, d]) for d in dirs
]
for fp in fullpaths: for fp in fullpaths:
cmd = 'mkdir -p {}'.format(fp) cmd = "mkdir -p {}".format(fp)
self._invoke_command(cmd) self._invoke_command(cmd)
def download_linux_server(self, x): def download_linux_server(self, x):
""" """
Download the latest Linux Unreal Tournament 4 Server from Epic Download the latest Linux Unreal Tournament 4 Server from Epic
""" """
self.ctx.log['ut4'].info('Downloading Linux Server Binary from Epic.') self.ctx.log["ut4"].info("Downloading Linux Server Binary from Epic.")
def download_logs(self): def download_logs(self):
""" """
Download the logs from the target hub. Download the logs from the target hub.
""" """
config_dir = self.ctx.config['app']['config_dir'] config_dir = self.ctx.config["app"]["config_dir"]
remote_game_host = self.ctx.config['app']['remote_game_host'] remote_game_host = self.ctx.config["app"]["remote_game_host"]
remote_game_dir = self.ctx.config['app']['remote_game_dir'] remote_game_dir = self.ctx.config["app"]["remote_game_dir"]
self.ctx.log['ut4'].info('Downloading instance logs from target hub.') self.ctx.log["ut4"].info("Downloading instance logs from target hub.")
cmd = ''' cmd = """
rsync -ravzp {remote_game_host}:{remote_game_dir}/LinuxServer/UnrealTournament/Saved/Logs/ {config_dir}/downloaded-logs/ rsync -ravzp {remote_game_host}:{remote_game_dir}/LinuxServer/UnrealTournament/Saved/Logs/ {config_dir}/downloaded-logs/
'''\ """.format(
.format(**{ **{"config_dir": config_dir, "remote_game_host": remote_game_host, "remote_game_dir": remote_game_dir}
'config_dir': config_dir, )
'remote_game_host': remote_game_host,
'remote_game_dir': remote_game_dir
})
self._invoke_command(cmd) self._invoke_command(cmd)
# Delete logs on remote game server if successfully transferred to local: # Delete logs on remote game server if successfully transferred to local:
self.ctx.log['ut4'].info('') self.ctx.log["ut4"].info("")
cmd = ''' cmd = """
ssh {remote_game_host} rm {remote_game_dir}/LinuxServer/UnrealTournament/Saved/Logs/* -r' ssh {remote_game_host} rm {remote_game_dir}/LinuxServer/UnrealTournament/Saved/Logs/* -r'
'''\ """.format(
.format(**{ **{"config_dir": config_dir, "remote_game_host": remote_game_host, "remote_game_dir": remote_game_dir}
'config_dir': config_dir, )
'remote_game_host': remote_game_host,
'remote_game_dir': remote_game_dir
})
# self._invoke_command(cmd) # self._invoke_command(cmd)
def generate_instance(self): def generate_instance(self):
@ -104,34 +87,25 @@ ssh {remote_game_host} rm {remote_game_dir}/LinuxServer/UnrealTournament/Saved/L
Takes the current coniguration and outputs the application files which Takes the current coniguration and outputs the application files which
can be copied to the server. can be copied to the server.
""" """
self.ctx.log['ut4'].info('Generating server instance from custom files...') self.ctx.log["ut4"].info("Generating server instance from custom files...")
project_dir = self.ctx.config['app']['project_dir'] project_dir = self.ctx.config["app"]["project_dir"]
# rsync # rsync
src = '/'.join([project_dir, 'base/LinuxServer']) src = "/".join([project_dir, "base/LinuxServer"])
dst = '/'.join([project_dir, 'instance/']) dst = "/".join([project_dir, "instance/"])
cmd = 'rsync -ravzp {src} {dst}'.format(**{ cmd = "rsync -ravzp {src} {dst}".format(**{"src": src, "dst": dst})
'src': src,
'dst': dst
})
self._invoke_command(cmd) self._invoke_command(cmd)
# cp 1 # cp 1
src = '/'.join([project_dir, 'start-server.sh']) src = "/".join([project_dir, "start-server.sh"])
dst = '/'.join([project_dir, 'instance/']) dst = "/".join([project_dir, "instance/"])
cmd = 'cp {src} {dst}'.format(**{ cmd = "cp {src} {dst}".format(**{"src": src, "dst": dst})
'src': src,
'dst': dst
})
self._invoke_command(cmd) self._invoke_command(cmd)
# cp 2 # cp 2
src = '/'.join([project_dir, 'stop-server.sh']) src = "/".join([project_dir, "stop-server.sh"])
dst = '/'.join([project_dir, 'instance/']) dst = "/".join([project_dir, "instance/"])
cmd = 'cp {src} {dst}'.format(**{ cmd = "cp {src} {dst}".format(**{"src": src, "dst": dst})
'src': src,
'dst': dst
})
self._invoke_command(cmd) self._invoke_command(cmd)
if self._needs_first_run(): if self._needs_first_run():
@ -149,35 +123,35 @@ ssh {remote_game_host} rm {remote_game_dir}/LinuxServer/UnrealTournament/Saved/L
""" """
Flip on the target hub on for Fragging! Flip on the target hub on for Fragging!
""" """
self.ctx.log['ut4'].info('Starting hub...') self.ctx.log["ut4"].info("Starting hub...")
cmd = ''' cmd = """
ssh {remote_game_host} {remote_game_dir}/start-server.sh ssh {remote_game_host} {remote_game_dir}/start-server.sh
''' """
self._invoke_command(cmd) self._invoke_command(cmd)
def stop_server(self): def stop_server(self):
""" """
Stop UT4 Hub processes on the server. Stop UT4 Hub processes on the server.
""" """
self.ctx.log['ut4'].info('Stopping hub.') self.ctx.log["ut4"].info("Stopping hub.")
cmd = ''' cmd = """
ssh {remote_game_host} {remote_game_dir}/stop-server.sh ssh {remote_game_host} {remote_game_dir}/stop-server.sh
''' """
self._invoke_command(cmd) self._invoke_command(cmd)
def upload_redirects(self): def upload_redirects(self):
""" """
Upload paks to redirect server. Upload paks to redirect server.
""" """
self.ctx.log['ut4'].info('Uploading redirects (maps, mutators, etc.) to target hub.') self.ctx.log["ut4"].info("Uploading redirects (maps, mutators, etc.) to target hub.")
project_dir = self.ctx.config['app']['project_dir'] project_dir = self.ctx.config["app"]["project_dir"]
# paks_dir = os.path.join(project_dir, 'instance/LinuxServer/UnrealTournament/Content/Paks/') # paks_dir = os.path.join(project_dir, 'instance/LinuxServer/UnrealTournament/Content/Paks/')
paks_dir = os.path.join(project_dir, 'files/') # trailing slash required paks_dir = os.path.join(project_dir, "files/") # trailing slash required
remote_redirect_host = self.ctx.config['app']['remote_redirect_host'] remote_redirect_host = self.ctx.config["app"]["remote_redirect_host"]
cwd = project_dir cwd = project_dir
cmd = ''' cmd = """
rsync -rivz \ rsync -rivz \
--delete \ --delete \
--exclude "*.md5" \ --exclude "*.md5" \
@ -186,11 +160,12 @@ rsync -rivz \
--exclude Mods.db \ --exclude Mods.db \
{paks_dir} {remote_redirect_host} {paks_dir} {remote_redirect_host}
'''\ """.format(
.format(**{ **{
'paks_dir': paks_dir, "paks_dir": paks_dir,
'remote_redirect_host': remote_redirect_host, "remote_redirect_host": remote_redirect_host,
}) }
)
# subprocess.run(cmd, cwd=cwd) # should be invoke_command? no because gui will need subprocess.run # subprocess.run(cmd, cwd=cwd) # should be invoke_command? no because gui will need subprocess.run
self._invoke_command(cmd) self._invoke_command(cmd)
@ -199,61 +174,66 @@ rsync -rivz \
self._redirect_chown() self._redirect_chown()
def _redirect_hide_passwords(self): def _redirect_hide_passwords(self):
project_dir = self.ctx.config['app']['project_dir'] project_dir = self.ctx.config["app"]["project_dir"]
remote_redirect_host = self.ctx.config['app']['remote_redirect_host'] remote_redirect_host = self.ctx.config["app"]["remote_redirect_host"]
# (on the server): # (on the server):
gameini="/srv/ut4-redirect.zavage.net/config/Game.ini" gameini = "/srv/ut4-redirect.zavage.net/config/Game.ini"
engineini="/srv/ut4-redirect.zavage.net/config/Engine.ini" engineini = "/srv/ut4-redirect.zavage.net/config/Engine.ini"
cmd = ''' cmd = """
ssh mathewguest.com \ ssh mathewguest.com \
sed -i /ServerInstanceID=/c\ServerInstanceID=Hidden {gameini} sed -i /ServerInstanceID=/c\ServerInstanceID=Hidden {gameini}
'''.format(gameini=gameini) """.format(
gameini=gameini
)
self._invoke_command(cmd) self._invoke_command(cmd)
cmd = ''' cmd = """
ssh mathewguest.com \ ssh mathewguest.com \
sed -i /RconPassword=/c\RconPassword=Hidden {engineini} sed -i /RconPassword=/c\RconPassword=Hidden {engineini}
'''.format(engineini=engineini) """.format(
engineini=engineini
)
self._invoke_command(cmd) self._invoke_command(cmd)
def _redirect_upload_script(self): def _redirect_upload_script(self):
project_dir = self.ctx.config['app']['project_dir'] project_dir = self.ctx.config["app"]["project_dir"]
remote_redirect_host = self.ctx.config['app']['remote_redirect_host'] remote_redirect_host = self.ctx.config["app"]["remote_redirect_host"]
cmd = ''' cmd = """
rsync -vz \ rsync -vz \
{project_dir}/ut4-server-ctl.sh \ {project_dir}/ut4-server-ctl.sh \
{remote_redirect_host} {remote_redirect_host}
'''\ """.format(
.format(**{ **{
'project_dir': project_dir, "project_dir": project_dir,
'remote_redirect_host': remote_redirect_host, "remote_redirect_host": remote_redirect_host,
}) }
)
# subprocess.run(cmd, cwd=cwd) # should be invoke_command? no because gui will need subprocess.run # subprocess.run(cmd, cwd=cwd) # should be invoke_command? no because gui will need subprocess.run
self._invoke_command(cmd) self._invoke_command(cmd)
def _redirect_chown(self): def _redirect_chown(self):
project_dir = self.ctx.config['app']['project_dir'] project_dir = self.ctx.config["app"]["project_dir"]
remote_redirect_host = self.ctx.config['app']['remote_redirect_host'] remote_redirect_host = self.ctx.config["app"]["remote_redirect_host"]
cmd = ''' cmd = """
ssh mathewguest.com \ ssh mathewguest.com \
chown http:http /srv/ut4-redirect.zavage.net -R chown http:http /srv/ut4-redirect.zavage.net -R
''' """
self._invoke_command(cmd) self._invoke_command(cmd)
def upload_server(self): def upload_server(self):
""" """
Upload all required game files to the hub server. Upload all required game files to the hub server.
""" """
self.ctx.log['ut4'].info('Uploading customized server') self.ctx.log["ut4"].info("Uploading customized server")
project_dir = self.ctx.config['app']['project_dir'] project_dir = self.ctx.config["app"]["project_dir"]
remote_game_host = self.ctx.config['app']['remote_game_host'] remote_game_host = self.ctx.config["app"]["remote_game_host"]
remote_game_dir = self.ctx.config['app']['remote_game_dir'] remote_game_dir = self.ctx.config["app"]["remote_game_dir"]
cwd = None cwd = None
# transfer #1 # transfer #1
cmd = ''' cmd = """
rsync -raivzp \ rsync -raivzp \
--delete \ --delete \
--exclude ".KEEP" \ --exclude ".KEEP" \
@ -266,88 +246,72 @@ rsync -raivzp \
--exclude "Saved/Logs/*" \ --exclude "Saved/Logs/*" \
{project_dir}/instance/ \ {project_dir}/instance/ \
{remote_game_host}:{remote_game_dir} {remote_game_host}:{remote_game_dir}
'''\ """.format(
.format(**{ **{"project_dir": project_dir, "remote_game_host": remote_game_host, "remote_game_dir": remote_game_dir}
'project_dir': project_dir, )
'remote_game_host': remote_game_host, cmd = cmd.replace(" ", "")
'remote_game_dir': remote_game_dir
})
cmd = cmd.replace(' ', '')
# subprocess.run(cmd, cwd=cwd) # should be invoke_command? no because gui will need subprocess.run # subprocess.run(cmd, cwd=cwd) # should be invoke_command? no because gui will need subprocess.run
self._invoke_command(cmd) self._invoke_command(cmd)
# transfer #2 # transfer #2
cmd = ''' cmd = """
rsync -avzp \ rsync -avzp \
{project_dir}/ut4-server-ctl.sh \ {project_dir}/ut4-server-ctl.sh \
{remote_game_host}:{remote_game_dir} {remote_game_host}:{remote_game_dir}
'''\ """.format(
.format(**{ **{"project_dir": project_dir, "remote_game_host": remote_game_host, "remote_game_dir": remote_game_dir}
'project_dir': project_dir, )
'remote_game_host': remote_game_host,
'remote_game_dir': remote_game_dir
})
# subprocess.run(cmd, cwd=cwd) # subprocess.run(cmd, cwd=cwd)
self._invoke_command(cmd) self._invoke_command(cmd)
# transfer #3 # transfer #3
cmd = ''' cmd = """
scp \ scp \
{project_dir}/instance/ut4-server.service \ {project_dir}/instance/ut4-server.service \
{remote_game_host}:/etc/systemd/system/ {remote_game_host}:/etc/systemd/system/
'''\ """.format(
.format(**{ **{"project_dir": project_dir, "remote_game_host": remote_game_host, "remote_game_dir": remote_game_dir}
'project_dir': project_dir, )
'remote_game_host': remote_game_host,
'remote_game_dir': remote_game_dir
})
# subprocess.run(cmd, cwd=cwd) # subprocess.run(cmd, cwd=cwd)
self._invoke_command(cmd) self._invoke_command(cmd)
# transfer #4 # transfer #4
cmd = ''' cmd = """
ssh {remote_game_host} \ ssh {remote_game_host} \
chown ut4:ut4 {remote_game_dir} -R chown ut4:ut4 {remote_game_dir} -R
'''.format(**{ """.format(
'remote_game_host': remote_game_host, **{"remote_game_host": remote_game_host, "remote_game_dir": remote_game_dir}
'remote_game_dir': remote_game_dir )
})
# subprocess.run(cmd, cwd=cwd) # subprocess.run(cmd, cwd=cwd)
self._invoke_command(cmd) self._invoke_command(cmd)
# Fix +x permissions on bash scripts # Fix +x permissions on bash scripts
cmd = ''' cmd = """
ssh {remote_game_host} \ ssh {remote_game_host} \
chmod +x \ chmod +x \
{remote_game_dir}/start-server.sh \ {remote_game_dir}/start-server.sh \
{remote_game_dir}/stop-server.sh {remote_game_dir}/stop-server.sh
'''.format(**{ """.format(
'remote_game_host': remote_game_host, **{"remote_game_host": remote_game_host, "remote_game_dir": remote_game_dir}
'remote_game_dir': remote_game_dir )
})
# subprocess.run(cmd, cwd=cwd) # subprocess.run(cmd, cwd=cwd)
self._invoke_command(cmd) self._invoke_command(cmd)
def _first_run(self): def _first_run(self):
self.ctx.log['ut4'].info('Starting instance once to get UID.') self.ctx.log["ut4"].info("Starting instance once to get UID.")
self.ctx.log['ut4'].info('Unfortunately, this takes 20 seconds. Just wait.') self.ctx.log["ut4"].info("Unfortunately, this takes 20 seconds. Just wait.")
# Make binary executable: # Make binary executable:
bin_name = 'UE4Server-Linux-Shipping' bin_name = "UE4Server-Linux-Shipping"
project_dir = self.ctx.config['app']['project_dir'] project_dir = self.ctx.config["app"]["project_dir"]
cwd = '{project_dir}/instance/LinuxServer/Engine/Binaries/Linux'\ cwd = "{project_dir}/instance/LinuxServer/Engine/Binaries/Linux".format(project_dir=project_dir)
.format(project_dir=project_dir) target_file = "{cwd}/{bin_name}".format(cwd=cwd, bin_name=bin_name)
target_file = '{cwd}/{bin_name}'.format(cwd=cwd, bin_name=bin_name) cmd = ["chmod", "770", target_file]
cmd = ['chmod', '770', target_file]
p = subprocess.run(cmd) p = subprocess.run(cmd)
cmd = ['./'+bin_name, 'UnrealTournament', 'UT-Entry?Game=Lobby', '-log'] cmd = ["./" + bin_name, "UnrealTournament", "UT-Entry?Game=Lobby", "-log"]
p = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE) p = subprocess.Popen(cmd, cwd=cwd, stdout=subprocess.PIPE)
try: try:
@ -356,68 +320,68 @@ ssh {remote_game_host} \
p.kill() p.kill()
stdout, stderr = p.communicate() stdout, stderr = p.communicate()
self.ctx.log['ut4'].info('sleeping 20 seconds and then we\'ll kill the server we started just now.') self.ctx.log["ut4"].info("sleeping 20 seconds and then we'll kill the server we started just now.")
# # TODO(MG) get uid and export # # TODO(MG) get uid and export
def _install_config(self): def _install_config(self):
files = ( files = ("Game.ini", "Engine.ini")
'Game.ini', project_dir = self.ctx.config["app"]["project_dir"]
'Engine.ini' config_dir = self.ctx.config["app"]["config_dir"]
)
project_dir = self.ctx.config['app']['project_dir']
config_dir = self.ctx.config['app']['config_dir']
for fn in files: for fn in files:
self.ctx.log['ut4'].info('Installing file: %s', fn) self.ctx.log["ut4"].info("Installing file: %s", fn)
src = os.path.join(config_dir, fn) src = os.path.join(config_dir, fn)
dst = os.path.join(project_dir, 'instance/LinuxServer/UnrealTournament/Saved/Config/LinuxServer', fn) dst = os.path.join(project_dir, "instance/LinuxServer/UnrealTournament/Saved/Config/LinuxServer", fn)
cmd = 'cp {src} {dst}'.format(**{ cmd = "cp {src} {dst}".format(**{"src": src, "dst": dst})
'src': src,
'dst': dst
})
self._invoke_command(cmd) self._invoke_command(cmd)
# Monkey-patch Game.ini to ensure it has a place for RedirectReferences # Monkey-patch Game.ini to ensure it has a place for RedirectReferences
if fn == 'Game.ini': if fn == "Game.ini":
ini = UnrealIniFile(dst) ini = UnrealIniFile(dst)
sect_name = '/Script/UnrealTournament.UTBaseGameMode' sect_name = "/Script/UnrealTournament.UTBaseGameMode"
opt_name = 'RedirectReferences' opt_name = "RedirectReferences"
if not ini._config.has_section(sect_name): if not ini._config.has_section(sect_name):
ini._config.add_section(sect_name) ini._config.add_section(sect_name)
if not ini._config.has_option(sect_name, opt_name): if not ini._config.has_option(sect_name, opt_name):
ini._config.set(sect_name, opt_name, ':PARAM:') ini._config.set(sect_name, opt_name, ":PARAM:")
with open(dst, 'w') as fp: with open(dst, "w") as fp:
ini._config.write(fp) ini._config.write(fp)
def _install_paks(self): def _install_paks(self):
project_dir = self.ctx.config['app']['project_dir'] project_dir = self.ctx.config["app"]["project_dir"]
self.ctx.log['ut4'].info('Installing maps...') self.ctx.log["ut4"].info("Installing maps...")
cmd = 'rsync -ravzp {src} {dst}'.format(**{ cmd = "rsync -ravzp {src} {dst}".format(
'src': '/'.join([project_dir, 'files/maps/']), **{
'dst': '/'.join([project_dir, 'instance/LinuxServer/UnrealTournament/Content/Paks/']) "src": "/".join([project_dir, "files/maps/"]),
}) "dst": "/".join([project_dir, "instance/LinuxServer/UnrealTournament/Content/Paks/"]),
}
)
self._invoke_command(cmd) self._invoke_command(cmd)
self.ctx.log['ut4'].info('Installing mutators...') self.ctx.log["ut4"].info("Installing mutators...")
cmd = 'rsync -ravzp {src} {dst}'.format(**{ cmd = "rsync -ravzp {src} {dst}".format(
'src': '/'.join([project_dir, 'files/mutators/']), **{
'dst': '/'.join([project_dir, 'instance/LinuxServer/UnrealTournament/Content/Paks/']) "src": "/".join([project_dir, "files/mutators/"]),
}) "dst": "/".join([project_dir, "instance/LinuxServer/UnrealTournament/Content/Paks/"]),
}
)
self._invoke_command(cmd) self._invoke_command(cmd)
def _install_redirect_lines(self): def _install_redirect_lines(self):
self.ctx.log['ut4'].info('Generating redirect references...') self.ctx.log["ut4"].info("Generating redirect references...")
redirect_protocol = self.ctx.config['app']['redirect_protocol'] redirect_protocol = self.ctx.config["app"]["redirect_protocol"]
redirect_url = self.ctx.config['app']['redirect_url'] redirect_url = self.ctx.config["app"]["redirect_url"]
project_dir = self.ctx.config['app']['project_dir'] project_dir = self.ctx.config["app"]["project_dir"]
mod_dir = '/'.join([project_dir, 'files']) mod_dir = "/".join([project_dir, "files"])
game_ini_filepath = '/'.join([project_dir, 'instance/LinuxServer/UnrealTournament/Saved/Config/LinuxServer/Game.ini']) game_ini_filepath = "/".join(
[project_dir, "instance/LinuxServer/UnrealTournament/Saved/Config/LinuxServer/Game.ini"]
)
game_ini = GameIniSpecial(game_ini_filepath) game_ini = GameIniSpecial(game_ini_filepath)
game_ini.clear_redirect_references() game_ini.clear_redirect_references()
files = glob.glob('{}/**/*.pak'.format(mod_dir)) files = glob.glob("{}/**/*.pak".format(mod_dir))
redirect_lines = [] redirect_lines = []
for idx, filename in enumerate(files): for idx, filename in enumerate(files):
# if idx > 5: # if idx > 5:
@ -435,90 +399,85 @@ ssh {remote_game_host} \
print(ex) print(ex)
continue continue
line = game_ini.add_redirect_reference(**{ line = game_ini.add_redirect_reference(
'pkg_basename': pkg_basename, **{
'redirect_protocol': redirect_protocol, "pkg_basename": pkg_basename,
'redirect_url': redirect_url, "redirect_protocol": redirect_protocol,
'relative_path': relative_path, "redirect_url": redirect_url,
'md5sum': md5sum "relative_path": relative_path,
}) "md5sum": md5sum,
}
)
self.ctx.log['ut4'].debug("redirect line = '%s'", line) self.ctx.log["ut4"].debug("redirect line = '%s'", line)
data = game_ini.write(sys.stdout) data = game_ini.write(sys.stdout)
def _install_rulesets(self): def _install_rulesets(self):
self.ctx.log['ut4'].info('Concatenating rulesets for game modes...') self.ctx.log["ut4"].info("Concatenating rulesets for game modes...")
project_dir = self.ctx.config['app']['project_dir'] project_dir = self.ctx.config["app"]["project_dir"]
src_dir = '/'.join([project_dir, 'files/rulesets']) src_dir = "/".join([project_dir, "files/rulesets"])
out_dir = '/'.join([project_dir, '/instance/LinuxServer/UnrealTournament/Saved/Config/Rulesets']) out_dir = "/".join([project_dir, "/instance/LinuxServer/UnrealTournament/Saved/Config/Rulesets"])
out_filename='/'.join([out_dir, 'ruleset.json']) out_filename = "/".join([out_dir, "ruleset.json"])
cmd = 'mkdir -pv {out_dir}'.format(**{ cmd = "mkdir -pv {out_dir}".format(**{"out_dir": out_dir})
'out_dir': out_dir
})
self._invoke_command(cmd) self._invoke_command(cmd)
self.ctx.log['ut4'].info('out filename=%s', out_filename) self.ctx.log["ut4"].info("out filename=%s", out_filename)
# echo {\"rules\":[ > "$OUT_FILENAME" # echo {\"rules\":[ > "$OUT_FILENAME"
cmd = "echo '{{\"rules\":[' > \"{out_filename}\"".format(out_filename=out_filename) cmd = 'echo \'{{"rules":[\' > "{out_filename}"'.format(out_filename=out_filename)
self._invoke_command(cmd) self._invoke_command(cmd)
cmd = 'for f in "{src_dir}"/*.json ; do cat "$f" >> "{out_filename}" ; done'.format( cmd = 'for f in "{src_dir}"/*.json ; do cat "$f" >> "{out_filename}" ; done'.format(
**{ **{"src_dir": src_dir, "out_filename": out_filename}
'src_dir': src_dir, )
'out_filename': out_filename
})
self._invoke_command(cmd) self._invoke_command(cmd)
cmd = 'echo "]}}" >> "{out_filename}"'.format( cmd = 'echo "]}}" >> "{out_filename}"'.format(**{"out_filename": out_filename})
**{
'out_filename': out_filename
})
self._invoke_command(cmd) self._invoke_command(cmd)
self.ctx.log['ut4'].info('output ruleset is at "%s"', out_filename) self.ctx.log["ut4"].info('output ruleset is at "%s"', out_filename)
def _invoke_command(self, cmd, msg=None): def _invoke_command(self, cmd, msg=None):
assert isinstance(cmd, str), 'cmd input must be string: %s'.format(cmd) assert isinstance(cmd, str), "cmd input must be string: %s".format(cmd)
if msg is None: if msg is None:
msg = cmd msg = cmd
print(msg) print(msg)
self.ctx.log['ut4'].info('running cmd: %s', cmd) self.ctx.log["ut4"].info("running cmd: %s", cmd)
cwd = None # todo(mg) ? cwd = None # todo(mg) ?
os.system(cmd) os.system(cmd)
def _needs_first_run(self): def _needs_first_run(self):
return False # TODO(MG): Hard-coded return False # TODO(MG): Hard-coded
def _validate_env_vars(self): def _validate_env_vars(self):
variable_names = ( variable_names = (
'project_dir', "project_dir",
'download_url', "download_url",
'download_filename', "download_filename",
'download_md5', "download_md5",
'redirect_protocol', "redirect_protocol",
'redirect_url', "redirect_url",
'remote_game_host', "remote_game_host",
'remote_game_dir', "remote_game_dir",
'remote_redirect_host' "remote_redirect_host",
) )
for name in variable_names: for name in variable_names:
value = self.ctx.config['app'][name] value = self.ctx.config["app"][name]
self.ctx.log['ut4'].info('%s: %s', name, value) self.ctx.log["ut4"].info("%s: %s", name, value)
i = input('Continue with above configuration? (y/N):') i = input("Continue with above configuration? (y/N):")
if i.lower() != 'y': if i.lower() != "y":
self.ctx.log['ut4'].info('Doing nothing.') self.ctx.log["ut4"].info("Doing nothing.")
return False return False
self.ctx.log['ut4'].info('Continuing.') self.ctx.log["ut4"].info("Continuing.")
return True return True
class MultiOrderedDict(collections.OrderedDict): class MultiOrderedDict(collections.OrderedDict):
def __setitem__(self, key, value): def __setitem__(self, key, value):
if isinstance(value, list) and key in self: if isinstance(value, list) and key in self:
self[key].extend(value) self[key].extend(value)
else: else:
super().__setitem__(key, value) super().__setitem__(key, value)

@ -5,14 +5,14 @@ import os
def ensure_dir_exists(dirpath): def ensure_dir_exists(dirpath):
if dirpath is None: if dirpath is None:
return return
if dirpath == '': if dirpath == "":
return return
dirpath = os.path.dirname(dirpath) dirpath = os.path.dirname(dirpath)
os.makedirs(dirpath, exist_ok=True) os.makedirs(dirpath, exist_ok=True)
def md5_file(filename): def md5_file(filename):
with open(filename, 'rb') as fp: with open(filename, "rb") as fp:
data = fp.read() data = fp.read()
h = hashlib.md5(data).hexdigest() h = hashlib.md5(data).hexdigest()
return h return h

@ -1,3 +1,3 @@
from .local_fs import * from .local_fs import *
from .scrape_utcc import *
from .scrape_ut4pugs import * from .scrape_ut4pugs import *
from .scrape_utcc import *

@ -1,9 +1,8 @@
from smileyface import myutil
from smileyface import structs
import datetime import datetime
import os import os
from smileyface import myutil, structs
class LocalFs: class LocalFs:
def __init__(self, ctx, datalayer): def __init__(self, ctx, datalayer):
@ -14,12 +13,12 @@ class LocalFs:
self.datalayer.create_tables() self.datalayer.create_tables()
def load_md5s(self): def load_md5s(self):
paks_dir = self.ctx.config['app']['project_dir'] paks_dir = self.ctx.config["app"]["project_dir"]
maps_dir = os.path.join(paks_dir, 'files', 'maps') maps_dir = os.path.join(paks_dir, "files", "maps")
print(maps_dir) print(maps_dir)
self._load_md5_one_dir(maps_dir) self._load_md5_one_dir(maps_dir)
muts_dir = os.path.join(paks_dir, 'files', 'mutators') muts_dir = os.path.join(paks_dir, "files", "mutators")
print(muts_dir) print(muts_dir)
self._load_md5_one_dir(muts_dir) self._load_md5_one_dir(muts_dir)

@ -1,8 +1,8 @@
import selenium import selenium
import selenium.webdriver import selenium.webdriver
URL_MUTATORS = 'https://ut4pugs.us/redirect-mutators' URL_MUTATORS = "https://ut4pugs.us/redirect-mutators"
URL_MAPS = 'https://ut4pugs.us/redirect-mutators' URL_MAPS = "https://ut4pugs.us/redirect-mutators"
class ScrapeUt4Pugs: class ScrapeUt4Pugs:
@ -25,14 +25,14 @@ class ScrapeUt4Pugs:
self._check_pak_md5sums() self._check_pak_md5sums()
def _check_pak_md5sums(self): def _check_pak_md5sums(self):
tbl_of_mutators = self.browser.find_element_by_id('myTable') tbl_of_mutators = self.browser.find_element_by_id("myTable")
print(tbl_of_mutators) print(tbl_of_mutators)
mut_rows = tbl_of_mutators.find_elements_by_xpath('tbody/tr') mut_rows = tbl_of_mutators.find_elements_by_xpath("tbody/tr")
for r in mut_rows: for r in mut_rows:
mut_cols = r.find_elements_by_xpath('td') mut_cols = r.find_elements_by_xpath("td")
if len(mut_cols) != 3: if len(mut_cols) != 3:
input('<breakpoint> at unexpected columns for mutator. received {}'.format(len(mut_cols))) input("<breakpoint> at unexpected columns for mutator. received {}".format(len(mut_cols)))
mut_file = mut_cols[0] mut_file = mut_cols[0]
mut_md5 = mut_cols[1] mut_md5 = mut_cols[1]
mut_ini_line = mut_cols[2] mut_ini_line = mut_cols[2]
@ -44,22 +44,22 @@ class ScrapeUt4Pugs:
local_file = self.datalayer.query_filepak(mut_file.text) local_file = self.datalayer.query_filepak(mut_file.text)
print(local_file) print(local_file)
if not local_file: if not local_file:
self.ctx.log['ut4'].warn('pak not found locally: %s', mut_file.text) self.ctx.log["ut4"].warn("pak not found locally: %s", mut_file.text)
continue continue
local_md5 = local_file.md5sum local_md5 = local_file.md5sum
remote_md5 = mut_md5.text remote_md5 = mut_md5.text
if local_md5 != remote_md5: if local_md5 != remote_md5:
input('<breakpoint> as mismatching md5!') input("<breakpoint> as mismatching md5!")
print('local: ', local_md5) print("local: ", local_md5)
print('remote: ', remote_md5) print("remote: ", remote_md5)
self.datalayer.mark_filepak_validated_state(local_file.file_pak_id, 'mismatch', 'ut4pugs', remote_md5) self.datalayer.mark_filepak_validated_state(local_file.file_pak_id, "mismatch", "ut4pugs", remote_md5)
else: else:
input('<breakpoint> as matching md5! good job') input("<breakpoint> as matching md5! good job")
self.datalayer.mark_filepak_validated_state(local_file.file_pak_id, 'valid', 'ut4pugs', None) self.datalayer.mark_filepak_validated_state(local_file.file_pak_id, "valid", "ut4pugs", None)
# print(r) # print(r)
print('xxx') print("xxx")
pass pass
pass pass

@ -1,7 +1,7 @@
import selenium import selenium
import selenium.webdriver import selenium.webdriver
URL_CARDS_LIST = 'https://utcc.unrealpugs.com/content' URL_CARDS_LIST = "https://utcc.unrealpugs.com/content"
class ScrapeUtcc: class ScrapeUtcc:
@ -21,4 +21,3 @@ class ScrapeUtcc:
def _scrape_list_of_content_cards(self): def _scrape_list_of_content_cards(self):
self.browser.get(URL_CARDS_LIST) self.browser.get(URL_CARDS_LIST)