diff --git a/.gitignore b/.gitignore index 801cb43..7655200 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,6 @@ *.pak +build/ +dist/ +__pycache__ +*.egg-info + diff --git a/edit-config.sh b/edit-config.sh old mode 100644 new mode 100755 diff --git a/files/rulesets/3-Duel-Beginner.json b/files/rulesets/3-Duel-Beginner.json new file mode 100644 index 0000000..a0c1c20 --- /dev/null +++ b/files/rulesets/3-Duel-Beginner.json @@ -0,0 +1,51 @@ +{ +"uniqueTag": "UTPlusDUELBeginner", +"categories": [ + "Duel" +], +"title": "Duel (Beginners)", +"toolTip": "Ultimate 1v1 test of deathmatch skill", +"description": "1v1 Duel with shield timers\n\nMutators: UT+, TeamSkins, WeaponSkins, HitSounds, CustomHUD\nOptions: MaxPlayers=2, TimeLimit=10, WeaponStay=OFF, Amp=OFF, PickupTimers=ON", +"mapPrefixes": [], +"maxMapsInList": 0, +"epicMaps": "/Game/RestrictedAssets/Maps/DM-Chill,/Game/RestrictedAssets/Maps/WIP/DM-ASDF,/Game/EpicInternal/Lea/DM-Lea,/Game/RestrictedAssets/Maps/WIP/DM-Solo,/Game/RestrictedAssets/Maps/WIP/DM-Decktest", +"defaultMap": "/Game/RestrictedAssets/Maps/WIP/DM-ASDF", +"customMapList": [ + "/Game/DM-BoneBone_Textured", + "/Game/DM-Campgrounds-G1E/DM-Campgrounds-G1E", + "/Game/DM-Coma/Maps/DM-Coma_A015", + "/Game/Maps/DM-1/DM-Pure_vrc_3", + "/Game/Maps/DM-Deep-03g", + "/Game/Maps/DM-Erase/DM-EraseV2_a04", + "/Game/Maps/DM-Fervor/DM-Fervor_V8", + "/Game/Rankin/DM-Rankin-LE", + "/Game/RestrictedAssets/Maps/DM-1on1-Roughinery", + "/Game/RestrictedAssets/Maps/WIP/DM-BoneCrusher_MC2", + "/Game/RestrictedAssets/Maps/WIP/DM-Echo_b7", + "/Game/RestrictedAssets/Maps/WIP/DM-Focus-LE", + "/Game/RestrictedAssets/Maps/WIP/DM-HyperBlast-UT99/DM-HyperBlast-UT99_metal-1", + "/Game/RestrictedAssets/Maps/WIP/DM-Idoma-UT3/DM-Idoma", + "/Game/_Ransom/Maps/DM-ChillShell-Abs-A/DM-ChillShell-Abs-A", + "/Game/dc/Ironic/DM-Alanis" +], +"maxPlayers": 2, +"maxTeamCount": 2, +"maxTeamSize": 1, +"maxPartySize": 1, +"displayTexture": "Texture2D'/Game/RestrictedAssets/UI/GameModeBadges/GB_Duel.GB_Duel'", +"gameMode" : "/Script/UnrealTournament.UTDuelGame", +"gameOptions" : "?MaxPlayers=2?TimeLimit=10?ignoreidle=1?GoalScore=0?ForceNoBots=1?ForceRespawn=1?mutator=UTPlus,UTPlusMovement,MutHitsounds,MutTeamskins,WeaponSkins,CustomHUD,WeaponReplacement?WTR=/Game/UT+/UTPlus/UT+GrenadeLauncher.UT+GrenadeLauncher_C:/Game/UT+/UTPlus/UT+BioRifle.UT+BioRifle_C?", +"requiredPackages" : [ + "CustomHUD", + "MutHitSounds", + "MutTeamSkins", + "WeaponSkins", + "UTPlus", + "UTPlusMovement" +], +"bTeamGame": true, +"bCompetitiveMatch": false, +"optionFlags": 65535, +"bHideFromUI": false, +"epicForceUIVisibility": 0 +}, diff --git a/files/rulesets/3-Duel-ProMode-noGL.json b/files/rulesets/3-Duel-ProMode-noGL.json new file mode 100644 index 0000000..2033e10 --- /dev/null +++ b/files/rulesets/3-Duel-ProMode-noGL.json @@ -0,0 +1,52 @@ +{ +"uniqueTag": "MYDUELnogl", +"categories": [ + "Duel" +], +"title": "ProMode Duel noGL", +"toolTip": "Ultimate 1v1 test of deathmatch skill", +"description": "1v1 Duel (with ProMode Movement and Weapon Stats)\n\nMutators: ProMode, TeamSkins, WeaponSkins, HitSounds, CustomHUD\nOptions: MaxPlayers=2, TimeLimit=10, WeaponStay=OFF, Amp=OFF*, PickupTimers=OFF", +"mapPrefixes": [], +"maxMapsInList": 0, +"epicMaps": "/Game/RestrictedAssets/Maps/DM-Chill,/Game/RestrictedAssets/Maps/WIP/DM-ASDF,/Game/EpicInternal/Lea/DM-Lea,/Game/RestrictedAssets/Maps/WIP/DM-Solo,/Game/RestrictedAssets/Maps/WIP/DM-Decktest", +"defaultMap": "/Game/RestrictedAssets/Maps/WIP/DM-ASDF", +"customMapList": [ + "/Game/DM-BoneBone_Textured", + "/Game/DM-Campgrounds-G1E/DM-Campgrounds-G1E", + "/Game/DM-Coma/Maps/DM-Coma_A015", + "/Game/Maps/DM-1/DM-Pure_vrc_3", + "/Game/Maps/DM-Deep-03g", + "/Game/Maps/DM-Erase/DM-EraseV2_a04", + "/Game/Maps/DM-Fervor/DM-Fervor_V8", + "/Game/Rankin/DM-Rankin-LE", + "/Game/RestrictedAssets/Maps/DM-1on1-Roughinery", + "/Game/RestrictedAssets/Maps/WIP/DM-BoneCrusher_MC2", + "/Game/RestrictedAssets/Maps/WIP/DM-Echo_b7", + "/Game/RestrictedAssets/Maps/WIP/DM-Focus-LE", + "/Game/RestrictedAssets/Maps/WIP/DM-HyperBlast-UT99/DM-HyperBlast-UT99_metal-1", + "/Game/RestrictedAssets/Maps/WIP/DM-Idoma-UT3/DM-Idoma", + "/Game/_Ransom/Maps/DM-ChillShell-Abs-A/DM-ChillShell-Abs-A", + "/Game/dc/Ironic/DM-Alanis" +], +"maxPlayers": 2, +"maxTeamCount": 2, +"maxTeamSize": 1, +"maxPartySize": 1, +"displayTexture": "Texture2D'/Game/RestrictedAssets/UI/GameModeBadges/GB_Duel.GB_Duel'", +"gameMode" : "/Script/UnrealTournament.UTDuelGame", +"gameOptions" : "?MaxPlayers=2?TimeLimit=10?ignoreidle=1?GoalScore=0?ForceNoBots=1?ForceRespawn=1?WTR=/Game/ProMode/Weapons/Pro_GrenadeLauncher.Pro_GrenadeLauncher_C:/Game/Proctf/Weapons/Proctf/Weapons/bp2_Orig_BioRifle.bp2_Orig_BioRifle_C?mutator=ProMode,MutHitsounds,MutTeamSkins,WeaponSkins,CustomHUD,NoPickupTimerMutator,WeaponReplacement", +"requiredPackages" : [ + "CustomHUD", + "MutHitSounds", + "MutTeamSkins", + "ProMovement", + "ProWeapons", + "WeaponSkins", + "NoPickupTimerMutator" +], +"bTeamGame": true, +"bCompetitiveMatch": false, +"optionFlags": 65535, +"bHideFromUI": false, +"epicForceUIVisibility": 0 +}, diff --git a/files/rulesets/3-Duel-ProMode.json b/files/rulesets/3-Duel-ProMode.json new file mode 100644 index 0000000..c667465 --- /dev/null +++ b/files/rulesets/3-Duel-ProMode.json @@ -0,0 +1,52 @@ +{ +"uniqueTag": "MYDUEL", +"categories": [ + "Duel" +], +"title": "ProMode Duel", +"toolTip": "Ultimate 1v1 test of deathmatch skill", +"description": "1v1 Duel (with ProMode Movement and Weapon Stats)\n\nMutators: ProMode, TeamSkins, WeaponSkins, HitSounds, CustomHUD\nOptions: MaxPlayers=2, TimeLimit=10, WeaponStay=OFF, Amp=OFF*, PickupTimers=OFF", +"mapPrefixes": [], +"maxMapsInList": 0, +"epicMaps": "/Game/RestrictedAssets/Maps/DM-Chill,/Game/RestrictedAssets/Maps/WIP/DM-ASDF,/Game/EpicInternal/Lea/DM-Lea,/Game/RestrictedAssets/Maps/WIP/DM-Solo,/Game/RestrictedAssets/Maps/WIP/DM-Decktest", +"defaultMap": "/Game/RestrictedAssets/Maps/WIP/DM-ASDF", +"customMapList": [ + "/Game/DM-BoneBone_Textured", + "/Game/DM-Campgrounds-G1E/DM-Campgrounds-G1E", + "/Game/DM-Coma/Maps/DM-Coma_A015", + "/Game/Maps/DM-1/DM-Pure_vrc_3", + "/Game/Maps/DM-Deep-03g", + "/Game/Maps/DM-Erase/DM-EraseV2_a04", + "/Game/Maps/DM-Fervor/DM-Fervor_V8", + "/Game/Rankin/DM-Rankin-LE", + "/Game/RestrictedAssets/Maps/DM-1on1-Roughinery", + "/Game/RestrictedAssets/Maps/WIP/DM-BoneCrusher_MC2", + "/Game/RestrictedAssets/Maps/WIP/DM-Echo_b7", + "/Game/RestrictedAssets/Maps/WIP/DM-Focus-LE", + "/Game/RestrictedAssets/Maps/WIP/DM-HyperBlast-UT99/DM-HyperBlast-UT99_metal-1", + "/Game/RestrictedAssets/Maps/WIP/DM-Idoma-UT3/DM-Idoma", + "/Game/_Ransom/Maps/DM-ChillShell-Abs-A/DM-ChillShell-Abs-A", + "/Game/dc/Ironic/DM-Alanis" +], +"maxPlayers": 2, +"maxTeamCount": 2, +"maxTeamSize": 1, +"maxPartySize": 1, +"displayTexture": "Texture2D'/Game/RestrictedAssets/UI/GameModeBadges/GB_Duel.GB_Duel'", +"gameMode" : "/Script/UnrealTournament.UTDuelGame", +"gameOptions" : "?MaxPlayers=2?TimeLimit=10?ignoreidle=1?GoalScore=0?ForceNoBots=1?ForceRespawn=1?mutator=ProMovement,ProWeapons,MutHitsounds,MutTeamSkins,WeaponSkins,CustomHUD,NoPickupTimerMutator", +"requiredPackages" : [ + "CustomHUD", + "MutHitSounds", + "MutTeamSkins", + "ProMovement", + "ProWeapons", + "WeaponSkins", + "NoPickupTimerMutator" +], +"bTeamGame": true, +"bCompetitiveMatch": false, +"optionFlags": 65535, +"bHideFromUI": false, +"epicForceUIVisibility": 0 +}, diff --git a/files/rulesets/3-Duel-UTplus-noGL.json b/files/rulesets/3-Duel-UTplus-noGL.json new file mode 100644 index 0000000..fc7d24b --- /dev/null +++ b/files/rulesets/3-Duel-UTplus-noGL.json @@ -0,0 +1,53 @@ +{ +"uniqueTag": "UTPlusDUELnogl", +"categories": [ + "Duel" +], +"title": "UT+ Duel noGL", +"toolTip": "Ultimate 1v1 test of deathmatch skill", +"description": "1v1 Duel (with UT+ Movement and Weapon Stats)\n\nMutators: UT+, TeamSkins, WeaponSkins, HitSounds, CustomHUD\nOptions: MaxPlayers=2, TimeLimit=10, WeaponStay=OFF, Amp=OFF, PickupTimers=OFF", +"mapPrefixes": [], +"maxMapsInList": 0, +"epicMaps": "/Game/RestrictedAssets/Maps/DM-Chill,/Game/RestrictedAssets/Maps/WIP/DM-ASDF,/Game/EpicInternal/Lea/DM-Lea,/Game/RestrictedAssets/Maps/WIP/DM-Solo,/Game/RestrictedAssets/Maps/WIP/DM-Decktest", +"defaultMap": "/Game/RestrictedAssets/Maps/WIP/DM-ASDF", +"customMapList": [ + "/Game/DM-BoneBone_Textured", + "/Game/DM-Campgrounds-G1E/DM-Campgrounds-G1E", + "/Game/DM-Coma/Maps/DM-Coma_A015", + "/Game/Maps/DM-1/DM-Pure_vrc_3", + "/Game/Maps/DM-Deep-03g", + "/Game/Maps/DM-Erase/DM-EraseV2_a04", + "/Game/Maps/DM-Fervor/DM-Fervor_V8", + "/Game/Rankin/DM-Rankin-LE", + "/Game/RestrictedAssets/Maps/DM-1on1-Roughinery", + "/Game/RestrictedAssets/Maps/WIP/DM-BoneCrusher_MC2", + "/Game/RestrictedAssets/Maps/WIP/DM-Echo_b7", + "/Game/RestrictedAssets/Maps/WIP/DM-Focus-LE", + "/Game/RestrictedAssets/Maps/WIP/DM-HyperBlast-UT99/DM-HyperBlast-UT99_metal-1", + "/Game/RestrictedAssets/Maps/WIP/DM-Idoma-UT3/DM-Idoma", + "/Game/_Ransom/Maps/DM-ChillShell-Abs-A/DM-ChillShell-Abs-A", + "/Game/dc/Ironic/DM-Alanis" +], +"maxPlayers": 2, +"maxTeamCount": 2, +"maxTeamSize": 1, +"maxPartySize": 1, +"displayTexture": "Texture2D'/Game/RestrictedAssets/UI/GameModeBadges/GB_Duel.GB_Duel'", +"gameMode" : "/Script/UnrealTournament.UTDuelGame", +"gameOptions" : "?MaxPlayers=2?TimeLimit=10?ignoreidle=1?GoalScore=0?MaxSpecators=10?ForceNoBots=1?ForceRespawn=1?mutator=UTPlus,UTPlusMovement,MutHitsounds,MutTeamskins,WeaponSkins,CustomHUD,NoPickupTimerMutator,WeaponReplacement?WTR=/Game/UT+/UTPlus/UT+GrenadeLauncher.UT+GrenadeLauncher_C:/Game/UT+/UTPlus/UT+BioRifle.UT+BioRifle_C?", +"requiredPackages" : [ + "CustomHUD", + "MutHitSounds", + "MutTeamskins", + "WeaponSkins", + "UTPlus", + "UTPlusMovement", + "NoPickupTimerMutator" +], +"bTeamGame": true, +"bCompetitiveMatch": false, +"optionFlags": 65535, +"bHideFromUI": false, +"minPlayersToStart": 2, +"epicForceUIVisibility": 0 +}, diff --git a/files/rulesets/3-Duel-UTplus.json b/files/rulesets/3-Duel-UTplus.json new file mode 100644 index 0000000..a02588d --- /dev/null +++ b/files/rulesets/3-Duel-UTplus.json @@ -0,0 +1,52 @@ +{ +"uniqueTag": "UTPlusDUEL", +"categories": [ + "Duel" +], +"title": "UT+ Duel", +"toolTip": "Ultimate 1v1 test of deathmatch skill", +"description": "1v1 Duel (with UT+ Movement and Weapon Stats)\n\nMutators: UT+, TeamSkins, WeaponSkins, HitSounds, CustomHUD\nOptions: MaxPlayers=2, TimeLimit=10, WeaponStay=OFF, Amp=OFF, PickupTimers=OFF", +"mapPrefixes": [], +"maxMapsInList": 0, +"epicMaps": "/Game/RestrictedAssets/Maps/DM-Chill,/Game/RestrictedAssets/Maps/WIP/DM-ASDF,/Game/EpicInternal/Lea/DM-Lea,/Game/RestrictedAssets/Maps/WIP/DM-Solo,/Game/RestrictedAssets/Maps/WIP/DM-Decktest", +"defaultMap": "/Game/RestrictedAssets/Maps/WIP/DM-ASDF", +"customMapList": [ + "/Game/DM-BoneBone_Textured", + "/Game/DM-Campgrounds-G1E/DM-Campgrounds-G1E", + "/Game/DM-Coma/Maps/DM-Coma_A015", + "/Game/Maps/DM-1/DM-Pure_vrc_3", + "/Game/Maps/DM-Deep-03g", + "/Game/Maps/DM-Erase/DM-EraseV2_a04", + "/Game/Maps/DM-Fervor/DM-Fervor_V8", + "/Game/Rankin/DM-Rankin-LE", + "/Game/RestrictedAssets/Maps/DM-1on1-Roughinery", + "/Game/RestrictedAssets/Maps/WIP/DM-BoneCrusher_MC2", + "/Game/RestrictedAssets/Maps/WIP/DM-Echo_b7", + "/Game/RestrictedAssets/Maps/WIP/DM-Focus-LE", + "/Game/RestrictedAssets/Maps/WIP/DM-HyperBlast-UT99/DM-HyperBlast-UT99_metal-1", + "/Game/RestrictedAssets/Maps/WIP/DM-Idoma-UT3/DM-Idoma", + "/Game/_Ransom/Maps/DM-ChillShell-Abs-A/DM-ChillShell-Abs-A", + "/Game/dc/Ironic/DM-Alanis" +], +"maxPlayers": 2, +"maxTeamCount": 2, +"maxTeamSize": 1, +"maxPartySize": 1, +"displayTexture": "Texture2D'/Game/RestrictedAssets/UI/GameModeBadges/GB_Duel.GB_Duel'", +"gameMode" : "/Script/UnrealTournament.UTDuelGame", +"gameOptions" : "?MaxPlayers=2?TimeLimit=10?ignoreidle=1?GoalScore=0?ForceNoBots=1?ForceRespawn=1?mutator=UTPlus,UTPlusMovement,MutHitsounds,MutTeamskins,WeaponSkins,CustomHUD,NoPickupTimerMutator", +"requiredPackages" : [ + "CustomHUD", + "MutHitSounds", + "MutTeamSkins", + "WeaponSkins", + "UTPlus", + "UTPlusMovement", + "NoPickupTimerMutator" +], +"bTeamGame": true, +"bCompetitiveMatch": false, +"optionFlags": 65535, +"bHideFromUI": false, +"epicForceUIVisibility": 0 +}, diff --git a/setup.py b/setup.py index 12ff834..3efe71d 100755 --- a/setup.py +++ b/setup.py @@ -1,25 +1,16 @@ #!/usr/bin/env python -# -# Usage: -# -# First, enable the python environment you want to install to, or if installing -# system-wide then ensure you're logged in with sufficient permissions -# (admin or root to install to system directories) -# -# installation: -# -# $ ./setup.py install -# -# de-installation: -# -# $ pip uninstall +from setuptools import setup, find_packages -from setuptools import setup - -__project__ = 'SmileyFace UT4 Server Panel' +__project__ = 'SmileyFace UT4 Hub Automator' __version__ = '0.1.0' +app_skellington_requirements = ( + 'appdirs', + 'colorlog', + 'configobj', +) + setup( name = __project__, version = __version__, @@ -30,13 +21,12 @@ setup( # Third-party dependencies; will be automatically installed install_requires = ( - 'app_skellington' - ), - - # Local packages to be installed (our packages) - packages = ( - 'smileyface', - ), + 'rdiff-backup', + ) + app_skellington_requirements, + packages = find_packages(), + package_dir = { + 'app_skellington': 'lib/app_skellington' + }, ) diff --git a/smileyface/_util.py b/smileyface/_util.py new file mode 100644 index 0000000..540053c --- /dev/null +++ b/smileyface/_util.py @@ -0,0 +1,10 @@ +import hashlib + +def md5sum_file(filename): + with open(filename, mode='rb') as f: + d = hashlib.md5() + for buf in iter(partial(f.read, 128), b''): + d.update(buf) + h = d.hexdigest() + return h + diff --git a/smileyface/app.py b/smileyface/app.py index 9fe67ec..d6e161e 100644 --- a/smileyface/app.py +++ b/smileyface/app.py @@ -1,56 +1,41 @@ +from . import hub_machine + import app_skellington from app_skellington import _util -from . import model - class SmileyFace(app_skellington.ApplicationContainer): def __init__(self, *args, **kwargs): filename = 'config.spec' self.configspec_filepath = _util.get_asset(__name__, filename) + config_filepath = self._get_config_filepath( + 'smileyface-ut4', + 'app_author', + 'hub-config.ini' + ) + super().__init__( configspec_filepath=self.configspec_filepath, + config_filepath=config_filepath, app_name = 'SmileyFace UT4 Server Panel', app_author = 'Mathew Guest', app_version = '0.1', *args, **kwargs ) - # super().__init__( - # app_name = 'SmileyFace UT4 Server Panel', - # app_author = 'Mathew Guest', - # app_version = '0.1' - # ) - self._load_config() - - def _load_config(self, config_file=None): - """ - Parse the config file, (todo) environment variables, and command-line - arguments to determine runtime configuration. - """ - if config_file is None: - config_file = self._get_config_filepath( - 'smileyface-ut4', - 'app_author', - 'hub-config.ini' - ) - - rc = self.ctx.config.load_config_from_file(config_file) def _cli_options(self): pass def _command_menu(self): sm_root = self.cli.init_submenu('command') - # sm_root.register_command(model.foo) - # sm_root.register_command(model.bar) _util.register_class_as_commands( self, sm_root, - model.UT4ServerMachine + hub_machine.UT4ServerMachine ) def _services(self): - self['model'] = lambda: model.UTServerMachine(self.ctx) + self['model'] = lambda: hub_machine.UTServerMachine(self.ctx) def interactive_shell(self): pass diff --git a/smileyface/cmd_menu.py b/smileyface/cmd_menu.py deleted file mode 100644 index e69de29..0000000 diff --git a/smileyface/config_editor.py b/smileyface/config_editor.py new file mode 100644 index 0000000..91f20e0 --- /dev/null +++ b/smileyface/config_editor.py @@ -0,0 +1,82 @@ +import configparser +import re + +class UnrealIniFile: + def __init__(self, filename=None): + self._config = None + self.filename = filename + + @property + def filename(self): + return self._filename + + @filename.setter + def filename(self, val): + self._filename = val + if val is not None: + self._config = configparser.RawConfigParser( + strict=False + ) + self._config.optionxform = str # Trick to preserve case in key names + self._config.read(self._filename) + +class GameIniSpecial: + def __init__(self, filename): + self._redirect_lines = [] + self._filename = None + self.filename = filename + + @property + def filename(self): + return self._filename + + @filename.setter + def filename(self, val): + self._filename = val + + def clear_redirect_references(self): + self._redirect_lines = [] + + def add_redirect_reference( + self, pkg_basename, redirect_url, redirect_protocol, + relative_path, md5sum + ): + args = { + 'pkg_basename': pkg_basename, + 'redirect_protocol': redirect_protocol, + 'redirect_url': redirect_url, + 'relative_path': relative_path, + 'md5sum': md5sum + } +########### START multi-line awkward indent + line = '\ +RedirectReferences=(PackageName="{pkg_basename}",\ +PackageURLProtocol="{redirect_protocol}",\ +PackageURL="{redirect_url}/{relative_path}",\ +PackageChecksum="{md5sum}")'.format(**args) +########### END multi-line awkward indent + + return self.add_redirect_reference_line(line) + + def add_redirect_reference_line(self, line): + self._redirect_lines.append(line) + return line + + def write(self, fp): + newcontents = None + + with open(self.filename, 'r') as inifile: + curcontents = inifile.read() + lines_str = '\n'.join(self._redirect_lines) + + newcontents = re.sub( + 'RedirectReferences = :PARAM:', + lines_str, + curcontents + ) + has_data = True + + if has_data: + with open(self.filename, 'w') as fp: + fp.write(newcontents) + diff --git a/smileyface/model.py b/smileyface/hub_machine.py similarity index 84% rename from smileyface/model.py rename to smileyface/hub_machine.py index cc9067c..f181459 100644 --- a/smileyface/model.py +++ b/smileyface/hub_machine.py @@ -1,3 +1,7 @@ +from .config_editor import UnrealIniFile, GameIniSpecial +from . import config_editor +from . import _util + from app_skellington import _util from functools import partial import collections @@ -80,7 +84,6 @@ rsync -ravzp {remote_game_host}:{remote_game_dir}/LinuxServer/UnrealTournament/S }) self._invoke_command(cmd) - # Delete logs on remote game server if successfully transferred to local: self.ctx.log['ut4'].info('') cmd = ''' @@ -252,14 +255,26 @@ ssh {remote_game_host} chown ut4.ut4 {remote_game_dir} -R config_dir = self.ctx.config['app']['config_dir'] for fn in files: self.ctx.log['ut4'].info('Installing file: %s', fn) - # src = '/'.join([config_dir, fn]) - # dst = '/'.join([project_dir, 'instance']) # needs corrected + src = os.path.join(config_dir, fn) + dst = os.path.join(project_dir, 'instance/LinuxServer/UnrealTournament/Saved/Config/LinuxServer', fn) cmd = 'cp {src} {dst}'.format(**{ - 'src': '/'.join([config_dir, fn]), - 'dst': '/'.join([project_dir, 'instance']) # needs corrected + 'src': src, + 'dst': dst }) self._invoke_command(cmd) + # Monkey-patch Game.ini to ensure it has a place for RedirectReferences + if fn == 'Game.ini': + ini = UnrealIniFile(dst) + sect_name = '/Script/UnrealTournament.UTBaseGameMode' + opt_name = 'RedirectReferences' + if not ini._config.has_section(sect_name): + ini._config.add_section(sect_name) + if not ini._config.has_option(sect_name, opt_name): + ini._config.set(sect_name, opt_name, ':PARAM:') + with open(dst, 'w') as fp: + ini._config.write(fp) + def _install_paks(self): project_dir = self.ctx.config['app']['project_dir'] @@ -278,64 +293,39 @@ ssh {remote_game_host} chown ut4.ut4 {remote_game_dir} -R self._invoke_command(cmd) def _install_redirect_lines(self): - return self.install_redirect_lines() - - def install_redirect_lines(self): self.ctx.log['ut4'].info('Generating redirect references...') + redirect_protocol = self.ctx.config['app']['redirect_protocol'] redirect_url = self.ctx.config['app']['redirect_url'] project_dir = self.ctx.config['app']['project_dir'] mod_dir = '/'.join([project_dir, 'files']) - files = glob.iglob('{}/**/*.pak'.format(mod_dir)) + + game_ini_filepath = '/'.join([project_dir, 'instance/LinuxServer/UnrealTournament/Saved/Config/LinuxServer/Game.ini']) + game_ini = GameIniSpecial(game_ini_filepath) + game_ini.clear_redirect_references() + + files = glob.glob('{}/**/*.pak'.format(mod_dir)) redirect_lines = [] for idx, filename in enumerate(files): - if idx > 5: - break - md5sum = self._md5sum_file(filename) + # if idx > 5: + # break + md5sum = _util.md5sum_file(filename) p = pathlib.Path(filename) relative_path = p.relative_to(mod_dir) - basename = p.name + pkg_basename = p.name + + line = game_ini.add_redirect_reference(**{ + 'pkg_basename': pkg_basename, + 'redirect_protocol': redirect_protocol, + 'redirect_url': redirect_url, + 'relative_path': relative_path, + 'md5sum': md5sum + }) - line = '\ -RedirectReferences=(PackageName="{basename}",\ -PackageURLProtocol="{redirect_protocol}",\ -PackageURL="{redirect_url}/{relative_path}",\ -PackageChecksum="{md5sum}")'\ -.format(**{ - 'basename': basename, - 'redirect_protocol': redirect_protocol, - 'redirect_url': redirect_url, - 'relative_path': relative_path, - 'md5sum': md5sum -}) self.ctx.log['ut4'].debug("redirect line = '%s'", line) - redirect_lines.append(line) - # end loop - for filename in files: - # START HERE --trying to dynamically add redirect references and - # gonna need to be able to work with the ini file, including support - # for duplicate keys. Alternatively, maybe do the redirect lines through - # a patch + data = game_ini.write(sys.stdout) - # I found out the hard way, configobj doesn't seem the best suited for - # duplicate keys... - - # install into Game.ini - game_ini_filepath = '/'.join([project_dir, 'files/config/Game.ini']) - # game_ini_data = configobj.ConfigObj(game_ini_filepath) - config = configparser.ConfigParser( - # game_ini_filepath, - # dict_type=MultiOrderedDict, - strict=False - ) - s = config.sections() - print(s) - # config.set('/Script/UnrealTournament.UTBaseGameMode', 'RedirectReferences', line) - config.read(game_ini_filepath) - l = config.write(sys.stdout) - print('l', l) - - def _install_rulesets(self): self.ctx.log['ut4'].info('Concatenating rulesets for game modes...') project_dir = self.ctx.config['app']['project_dir'] @@ -378,14 +368,6 @@ PackageChecksum="{md5sum}")'\ cwd = None # todo(mg) ? os.system(cmd) - def _md5sum_file(self, filename): - with open(filename, mode='rb') as f: - d = hashlib.md5() - for buf in iter(partial(f.read, 128), b''): - d.update(buf) - h = d.hexdigest() - return h - def _needs_first_run(self): return False # TODO(MG): Hard-coded