warlock-manager 2.2.3__tar.gz → 2.2.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/PKG-INFO +1 -1
  2. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/pyproject.toml +1 -1
  3. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/tests/test_base_service.py +1 -1
  4. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/tests/test_unreal_config.py +81 -15
  5. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/apps/base_app.py +8 -8
  6. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/apps/steam_app.py +2 -2
  7. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/config/unreal_config.py +73 -57
  8. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/formatters/cli_formatter.py +42 -0
  9. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/libs/cache.py +3 -3
  10. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/libs/firewall.py +36 -15
  11. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/libs/utils.py +13 -0
  12. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/mods/base_mod.py +7 -3
  13. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/nexus/nexus.py +18 -4
  14. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/services/base_service.py +14 -4
  15. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager.egg-info/PKG-INFO +1 -1
  16. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/LICENSE +0 -0
  17. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/README.md +0 -0
  18. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/setup.cfg +0 -0
  19. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/tests/test_app.py +0 -0
  20. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/tests/test_base_config.py +0 -0
  21. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/tests/test_cli_config.py +0 -0
  22. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/tests/test_cli_formatter.py +0 -0
  23. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/tests/test_cmd.py +0 -0
  24. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/tests/test_config_key.py +0 -0
  25. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/tests/test_ini_config.py +0 -0
  26. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/tests/test_sensitive_data_filter.py +0 -0
  27. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/tests/test_socket_service.py +0 -0
  28. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/tests/test_unreal_config_ark_spawn_entities.py +0 -0
  29. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/tests/test_unreal_save.py +0 -0
  30. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/tests/test_version.py +0 -0
  31. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/__init__.py +0 -0
  32. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/apps/__init__.py +0 -0
  33. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/apps/manual_app.py +0 -0
  34. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/config/__init__.py +0 -0
  35. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/config/base_config.py +0 -0
  36. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/config/cli_config.py +0 -0
  37. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/config/config_key.py +0 -0
  38. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/config/ini_config.py +0 -0
  39. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/config/json_config.py +0 -0
  40. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/config/properties_config.py +0 -0
  41. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/formatters/__init__.py +0 -0
  42. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/libs/__init__.py +0 -0
  43. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/libs/app.py +0 -0
  44. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/libs/app_runner.py +0 -0
  45. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/libs/cmd.py +0 -0
  46. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/libs/download.py +0 -0
  47. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/libs/get_wan_ip.py +0 -0
  48. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/libs/java.py +0 -0
  49. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/libs/meta.py +0 -0
  50. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/libs/ports.py +0 -0
  51. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/libs/sensitive_data_filter.py +0 -0
  52. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/libs/tui.py +0 -0
  53. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/libs/version.py +0 -0
  54. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/mods/__init__.py +0 -0
  55. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/mods/warlock_nexus_mod.py +0 -0
  56. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/services/__init__.py +0 -0
  57. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/services/http_service.py +0 -0
  58. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/services/rcon_service.py +0 -0
  59. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager/services/socket_service.py +0 -0
  60. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager.egg-info/SOURCES.txt +0 -0
  61. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager.egg-info/dependency_links.txt +0 -0
  62. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager.egg-info/requires.txt +0 -0
  63. {warlock_manager-2.2.3 → warlock_manager-2.2.5}/warlock_manager.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: warlock-manager
3
- Version: 2.2.3
3
+ Version: 2.2.5
4
4
  Summary: Dependency library for game-server management applications.
5
5
  Author-email: Charlie Powell <cdp1337@bitsnbytes.dev>
6
6
  Maintainer-email: Charlie Powell <cdp1337@bitsnbytes.dev>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "warlock-manager"
7
- version = "2.2.3"
7
+ version = "2.2.5"
8
8
  description = "Dependency library for game-server management applications."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -61,4 +61,4 @@ class TestBaseService(unittest.TestCase):
61
61
  self.assertIn('Type=simple', data_new)
62
62
  self.assertIn('ExecStart=%s' % svc.get_executable(), data_new)
63
63
  self.assertIn('WorkingDirectory=%s' % svc.get_app_directory(), data_new)
64
- self.assertIn('EnvironmentFile=%s/Environments/%s.env' % (utils.get_app_directory(), svc.service), data_new)
64
+ self.assertIn('EnvironmentFile=%s/Environments/%s.env' % (utils.get_base_directory(), svc.service), data_new)
@@ -23,32 +23,32 @@ class TestUnrealConfig(unittest.TestCase):
23
23
  cfg = UnrealConfig('test', os.path.join(here, 'data', 'unreal_simple.ini'))
24
24
  # Configs are grouped by named parameters, so let's add some options
25
25
  cfg.add_option({
26
- 'name': 'Key1',
26
+ 'name': 'Key 1',
27
27
  'section': 'SomeSection',
28
28
  'key': 'Key1',
29
29
  })
30
30
  cfg.add_option({
31
- 'name': 'Key2',
31
+ 'name': 'Key 2',
32
32
  'section': 'SomeSection',
33
33
  'key': 'Key2',
34
34
  'type': 'int'
35
35
  })
36
36
  cfg.add_option({
37
- 'name': 'Key3',
37
+ 'name': 'Key 3',
38
38
  'section': 'SomeSection',
39
39
  'key': 'Key3',
40
40
  'type': 'bool'
41
41
  })
42
42
  cfg.load()
43
43
 
44
- self.assertEqual(cfg.get_value('Key1'), 'Value1')
45
- self.assertEqual(cfg.get_value('Key2'), 42)
46
- self.assertEqual(cfg.get_value('Key3'), True)
44
+ self.assertEqual(cfg.get_value('Key 1'), 'Value1')
45
+ self.assertEqual(cfg.get_value('Key 2'), 42)
46
+ self.assertEqual(cfg.get_value('Key 3'), True)
47
47
 
48
48
  # These values should exist
49
- self.assertTrue(cfg.has_value('Key1'))
50
- self.assertTrue(cfg.has_value('Key2'))
51
- self.assertTrue(cfg.has_value('Key3'))
49
+ self.assertTrue(cfg.has_value('Key 1'))
50
+ self.assertTrue(cfg.has_value('Key 2'))
51
+ self.assertTrue(cfg.has_value('Key 3'))
52
52
 
53
53
  # This value should not
54
54
  self.assertFalse(cfg.has_value('NonExistentKey'))
@@ -58,14 +58,14 @@ class TestUnrealConfig(unittest.TestCase):
58
58
  expected = f.read()
59
59
  self.assertEqual(expected, cfg.fetch())
60
60
 
61
- cfg.set_value('Key1', 'NewValue')
62
- self.assertEqual(cfg.get_value('Key1'), 'NewValue')
61
+ cfg.set_value('Key 1', 'NewValue')
62
+ self.assertEqual(cfg.get_value('Key 1'), 'NewValue')
63
63
 
64
- cfg.set_value('Key2', 100)
65
- self.assertEqual(cfg.get_value('Key2'), 100)
64
+ cfg.set_value('Key 2', 100)
65
+ self.assertEqual(cfg.get_value('Key 2'), 100)
66
66
 
67
- cfg.set_value('Key3', False)
68
- self.assertEqual(cfg.get_value('Key3'), False)
67
+ cfg.set_value('Key 3', False)
68
+ self.assertEqual(cfg.get_value('Key 3'), False)
69
69
 
70
70
  # Ensure the generated data matches expectations
71
71
  expected = '''; This is a simple config file for testing purposes.
@@ -80,6 +80,61 @@ Key3=False
80
80
  '''
81
81
  self.assertEqual(expected, cfg.fetch())
82
82
 
83
+ def test_always_escape_strings(self):
84
+ cfg = UnrealConfig('test', os.path.join(here, 'data', 'unreal_simple.ini'))
85
+ cfg.always_escape_strings = True
86
+ # Configs are grouped by named parameters, so let's add some options
87
+ cfg.add_option({
88
+ 'name': 'Key 1',
89
+ 'section': 'SomeSection',
90
+ 'key': 'Group/Key1',
91
+ })
92
+ cfg.add_option({
93
+ 'name': 'Key 2',
94
+ 'section': 'SomeSection',
95
+ 'key': 'Group/Key2',
96
+ 'type': 'int'
97
+ })
98
+ cfg.add_option({
99
+ 'name': 'Key 3',
100
+ 'section': 'SomeSection',
101
+ 'key': 'Group/Key3',
102
+ 'type': 'bool'
103
+ })
104
+
105
+ cfg.set_value('Key 1', 'Value1')
106
+ cfg.set_value('Key 2', 42)
107
+ cfg.set_value('Key 3', True)
108
+
109
+ self.assertEqual(cfg.get_value('Key 1'), 'Value1')
110
+ self.assertEqual(cfg.get_value('Key 2'), 42)
111
+ self.assertEqual(cfg.get_value('Key 3'), True)
112
+
113
+ # These values should exist
114
+ self.assertTrue(cfg.has_value('Key 1'))
115
+ self.assertTrue(cfg.has_value('Key 2'))
116
+ self.assertTrue(cfg.has_value('Key 3'))
117
+
118
+ # This value should not
119
+ self.assertFalse(cfg.has_value('NonExistentKey'))
120
+
121
+ # Ensure the generated data matches expectations
122
+ expected = '[SomeSection]\nGroup=(Key1="Value1",Key2=42,Key3=True)\n'
123
+ self.assertEqual(expected, cfg.fetch())
124
+
125
+ cfg.set_value('Key 1', 'NewValue')
126
+ self.assertEqual(cfg.get_value('Key 1'), 'NewValue')
127
+
128
+ cfg.set_value('Key 2', 100)
129
+ self.assertEqual(cfg.get_value('Key 2'), 100)
130
+
131
+ cfg.set_value('Key 3', False)
132
+ self.assertEqual(cfg.get_value('Key 3'), False)
133
+
134
+ # Ensure the generated data matches expectations
135
+ expected = '[SomeSection]\nGroup=(Key1="NewValue",Key2=100,Key3=False)\n'
136
+ self.assertEqual(expected, cfg.fetch())
137
+
83
138
  def test_simple_create(self):
84
139
  cfg = UnrealConfig('test', os.path.join(here, 'data', 'unreal_simple.ini'))
85
140
  # Configs are grouped by named parameters, so let's add some options
@@ -247,6 +302,9 @@ PlayedMaps=NewMap2_WP
247
302
  })
248
303
  cfg.load()
249
304
 
305
+ self.assertTrue(cfg.has_value('Difficulty'))
306
+ self.assertFalse(cfg.has_value('I do not exist'))
307
+
250
308
  self.assertEqual(cfg.get_value('Difficulty'), 'None')
251
309
  self.assertEqual(cfg.get_value('Randomizer Seed'), '')
252
310
  self.assertEqual(cfg.get_value('Randomizer Pal Level Random'), False)
@@ -256,6 +314,14 @@ PlayedMaps=NewMap2_WP
256
314
  expected = f.read()
257
315
  self.assertEqual(expected, cfg.fetch())
258
316
 
317
+ cfg.set_value('Difficulty', 'Super Duper Hard')
318
+ cfg.set_value('Randomizer Seed', 'Random Seed')
319
+ cfg.set_value('Randomizer Pal Level Random', True)
320
+
321
+ self.assertEqual(cfg.get_value('Difficulty'), 'Super Duper Hard')
322
+ self.assertEqual(cfg.get_value('Randomizer Seed'), 'Random Seed')
323
+ self.assertEqual(cfg.get_value('Randomizer Pal Level Random'), True)
324
+
259
325
  def test_palworld_empty(self):
260
326
  """
261
327
  Test that the Palworld format works even when the ini is empty.
@@ -576,7 +576,7 @@ class BaseApp(ABC):
576
576
  except urllib_error.HTTPError as e:
577
577
  print('Could not notify Discord: %s' % e)
578
578
 
579
- @deprecated("Please use get_app_directory() from utils instead.")
579
+ @deprecated("Please use get_base_directory() from utils instead.")
580
580
  def get_app_directory(self) -> str:
581
581
  """
582
582
  Get the base directory for this game installation.
@@ -585,7 +585,7 @@ class BaseApp(ABC):
585
585
 
586
586
  :return:
587
587
  """
588
- return utils.get_app_directory()
588
+ return utils.get_base_directory()
589
589
 
590
590
  @deprecated("Please use get_home_directory() from utils instead")
591
591
  def get_home_directory(self) -> str:
@@ -663,8 +663,8 @@ class BaseApp(ABC):
663
663
  logging.info('Removed config file for %s at %s' % (self.name, config.path))
664
664
 
665
665
  # Cleanup directory structure
666
- shutil.rmtree(os.path.join(utils.get_app_directory(), 'AppFiles'))
667
- shutil.rmtree(os.path.join(utils.get_app_directory(), 'Environments'))
666
+ shutil.rmtree(os.path.join(utils.get_base_directory(), 'AppFiles'))
667
+ shutil.rmtree(os.path.join(utils.get_base_directory(), 'Environments'))
668
668
 
669
669
  def remove_service(self, service_name: str):
670
670
  """
@@ -689,7 +689,7 @@ class BaseApp(ABC):
689
689
  Try to detect available services for this game.
690
690
  :return:
691
691
  """
692
- envs = os.path.join(utils.get_app_directory(), 'Environments')
692
+ envs = os.path.join(utils.get_base_directory(), 'Environments')
693
693
  if os.path.exists(envs):
694
694
  # Each service should have a file here, named as {service}.env
695
695
  services = []
@@ -724,9 +724,9 @@ class BaseApp(ABC):
724
724
  """
725
725
 
726
726
  # Ensure some baseline directories exist with the correct ownership and permissions
727
- utils.makedirs(os.path.join(utils.get_app_directory(), 'Backups'))
728
- utils.makedirs(os.path.join(utils.get_app_directory(), 'AppFiles'))
729
- utils.makedirs(os.path.join(utils.get_app_directory(), 'Environments'))
727
+ utils.makedirs(os.path.join(utils.get_base_directory(), 'Backups'))
728
+ utils.makedirs(os.path.join(utils.get_base_directory(), 'AppFiles'))
729
+ utils.makedirs(os.path.join(utils.get_base_directory(), 'Environments'))
730
730
 
731
731
  return True
732
732
 
@@ -317,7 +317,7 @@ class SteamApp(BaseApp, ABC):
317
317
 
318
318
  :return:
319
319
  """
320
- app_manifest = os.path.join(utils.get_app_directory(), 'AppFiles', 'steamapps', 'appmanifest_%s.acf' % self.steam_id)
320
+ app_manifest = os.path.join(utils.get_base_directory(), 'AppFiles', 'steamapps', 'appmanifest_%s.acf' % self.steam_id)
321
321
 
322
322
  if not os.path.exists(app_manifest):
323
323
  print(f"App manifest file {app_manifest} does not exist.", file=sys.stderr)
@@ -395,7 +395,7 @@ class SteamApp(BaseApp, ABC):
395
395
  cmd = Cmd([
396
396
  guess_steamcmd_path(),
397
397
  '+force_install_dir',
398
- os.path.join(utils.get_app_directory(), 'AppFiles'),
398
+ os.path.join(utils.get_base_directory(), 'AppFiles'),
399
399
  '+login',
400
400
  'anonymous',
401
401
  '+app_update',
@@ -12,9 +12,64 @@ class UnrealConfig(BaseConfig):
12
12
  super().__init__(group_name)
13
13
  self.path = path
14
14
  self._data = []
15
- self._values = {}
16
15
  self._use_array_operators = False
17
16
  self._is_changed = False
17
+ self.always_escape_strings = False
18
+ """
19
+ Set to True to always escape strings in Configuration files. Some games require this.
20
+ """
21
+
22
+ def _get_raw_value(self, name: str):
23
+ """
24
+ Get the raw value, possibly in a nested dictionary.
25
+
26
+ useful for get_value and has_value.
27
+
28
+ :param name: Name of the option
29
+ :return:
30
+ """
31
+ if name not in self.options:
32
+ return None
33
+ opt = self.options[name]
34
+
35
+ # Check if this section exists, also serves to find the section.
36
+ section = None
37
+ for sec in self._data:
38
+ if sec[0]['type'] == 'section' and sec[0]['value'] == opt.section:
39
+ section = sec[1:]
40
+ break
41
+ if section is None:
42
+ return None
43
+
44
+ if '/' in opt.key:
45
+ # Look for a key inside structured data (aka a dict)
46
+ group = opt.key.split('/')[0]
47
+ for sec in section:
48
+ if sec['key'] == group:
49
+ current = sec['value']
50
+ for part in opt.key.split('/')[1:]:
51
+ if part in current:
52
+ current = current[part]
53
+ else:
54
+ # Subkey does not exist in the value of this section config; doesn't exist.
55
+ return None
56
+ return current
57
+ else:
58
+ # Simple key.
59
+ ret = None
60
+ for sec in section:
61
+ if sec['type'] == 'keyvalue' and sec['key'] == opt.key:
62
+ # Unreal supports duplicate keys; these should be exposed as a list.
63
+ if ret is None:
64
+ ret = sec['value']
65
+ elif isinstance(ret, list):
66
+ ret.append(sec['value'])
67
+ else:
68
+ ret = [ret]
69
+ ret.append(sec['value'])
70
+ return ret
71
+
72
+ return None
18
73
 
19
74
  def get_value(self, name: str) -> Union[str, int, bool, list]:
20
75
  """
@@ -28,27 +83,11 @@ class UnrealConfig(BaseConfig):
28
83
  return ''
29
84
 
30
85
  opt = self.options[name]
31
-
32
- if opt.section not in self._values:
33
- val = opt.default
86
+ raw_value = self._get_raw_value(name)
87
+ if raw_value is None:
88
+ return opt.default
34
89
  else:
35
- if '/' in opt.key:
36
- # Struct key
37
- parts = opt.key.split('/')
38
- current = self._values[opt.section]
39
- for part in parts:
40
- if part in current:
41
- current = current[part]
42
- else:
43
- current = opt.default
44
- break
45
- val = current
46
- elif opt.key not in self._values[opt.section]:
47
- val = opt.default
48
- else:
49
- val = self._values[opt.section][opt.key]
50
-
51
- return opt.to_system_type(val)
90
+ return opt.to_system_type(raw_value)
52
91
 
53
92
  def _find_or_create_value(self, section: list, key: str, str_value: Union[str, list]) -> list:
54
93
  """
@@ -136,9 +175,14 @@ class UnrealConfig(BaseConfig):
136
175
  opt = self.options[name]
137
176
  str_value = self.from_system_type(name, value)
138
177
 
139
- if opt.section not in self._values:
178
+ # Create the section if necessary
179
+ exists = False
180
+ for sec in self._data:
181
+ if sec[0]['type'] == 'section' and sec[0]['value'] == opt.section:
182
+ exists = True
183
+ break
184
+ if not exists:
140
185
  # Create the section
141
- self._values[opt.section] = {}
142
186
  self._data.append([{'type': 'section', 'value': opt.section}])
143
187
 
144
188
  # Ensure the updated value is in the data structure
@@ -157,7 +201,6 @@ class UnrealConfig(BaseConfig):
157
201
  new_data.append(sec)
158
202
 
159
203
  self._data = new_data
160
- self._values[opt.section][opt.key] = str_value
161
204
  self._is_changed = True
162
205
 
163
206
  def has_value(self, name: str) -> bool:
@@ -167,17 +210,8 @@ class UnrealConfig(BaseConfig):
167
210
  :param name: Name of the option
168
211
  :return:
169
212
  """
170
- if name not in self.options:
171
- return False
172
- opt = self.options[name]
173
-
174
- if opt.section not in self._values:
175
- return False
176
- else:
177
- if opt.key not in self._values[opt.section]:
178
- return False
179
- else:
180
- return self._values[opt.section][opt.key] != ''
213
+ raw_value = self._get_raw_value(name)
214
+ return raw_value is not None
181
215
 
182
216
  def exists(self) -> bool:
183
217
  """
@@ -194,7 +228,6 @@ class UnrealConfig(BaseConfig):
194
228
  if os.path.exists(self.path):
195
229
  with open(self.path, 'r', encoding='utf-8') as f:
196
230
  section = []
197
- last_section = ''
198
231
  for line in f.readlines():
199
232
  data = None
200
233
  stripped = line.strip()
@@ -232,27 +265,6 @@ class UnrealConfig(BaseConfig):
232
265
  self._data.append(section)
233
266
  section = []
234
267
  section.append(data)
235
- last_section = data['value']
236
- elif data['type'] == 'keystruct':
237
- section.append(data)
238
- if last_section not in self._values:
239
- self._values[last_section] = {}
240
- self._values[last_section][data['key']] = data['value']
241
- elif data['type'] == 'keyvalue':
242
- section.append(data)
243
- if last_section not in self._values:
244
- self._values[last_section] = {}
245
- # Auto-handle duplicate keys by converting them to a list.
246
- # UE is weird.
247
- if data['key'] in self._values[last_section]:
248
- # Existing key, convert to list
249
- existing_value = self._values[last_section][data['key']]
250
- if not isinstance(existing_value, list):
251
- existing_value = [existing_value]
252
- existing_value.append(data['value'])
253
- self._values[last_section][data['key']] = existing_value
254
- else:
255
- self._values[last_section][data['key']] = data['value']
256
268
  else:
257
269
  section.append(data)
258
270
  if len(section) > 0:
@@ -289,6 +301,10 @@ class UnrealConfig(BaseConfig):
289
301
  # Boolean strings do not require quoting
290
302
  return False
291
303
 
304
+ if self.always_escape_strings:
305
+ # Game requested to always escape strings
306
+ return True
307
+
292
308
  if re.match(r'^[A-Za-z0-9]+$', s) is not None:
293
309
  # Simple strings do not require quoting
294
310
  return False
@@ -10,6 +10,48 @@ def cli_formatter(
10
10
  true_value: str | bool = 'True',
11
11
  false_value: str | bool = 'False',
12
12
  ) -> str:
13
+ """
14
+ Format a given Configuration object as CLI arguments.
15
+
16
+ ## True/False Formatting
17
+
18
+ The most complicated part of this is handling true/false boolean values.
19
+
20
+ The default is to render bool TRUE values as -key_name=True and bool FALSE values as -key_name=False
21
+
22
+ Render bool TRUE values as -key_name and bool FALSE values are omitted completely
23
+
24
+ ```python
25
+ cli_formatter(..., prefix='-', true_value=True, false_value=False)
26
+ ```
27
+
28
+ The inverse is possible too, to omit TRUE values and only render FALSE values.
29
+
30
+ ```python
31
+ cli_formatter(..., prefix='-', true_value=False, false_value=True)
32
+ ```
33
+
34
+ Render bool TRUE values as -key_name=true and bool FALSE values as -key_name=false
35
+
36
+ ```python
37
+ cli_formatter(..., prefix='-', true_value='true', false_value='false')
38
+ ```
39
+
40
+ Render bool TRUE values as ?key_name:YUP and bool FALSE values as ?key_name:LULZNOPE
41
+
42
+ ```python
43
+ cli_formatter(..., prefix='?', sep=':', true_value='YUP', false_value='LULZNOPE')
44
+ ```
45
+
46
+ :param data:
47
+ :param section:
48
+ :param prefix:
49
+ :param sep:
50
+ :param joiner:
51
+ :param true_value:
52
+ :param false_value:
53
+ :return:
54
+ """
13
55
  values = []
14
56
  for opt in data.options.values():
15
57
  if opt.section != section:
@@ -8,7 +8,7 @@ from warlock_manager.libs import utils
8
8
  def get_cache(some_string: str, expires: int = 3600) -> str | None:
9
9
  # Check cache prior to running the command.
10
10
  cmd_hash = hashlib.sha256(some_string.encode()).hexdigest()
11
- cache_path = os.path.join(utils.get_app_directory(), '.cache', cmd_hash)
11
+ cache_path = os.path.join(utils.get_base_directory(), '.cache', cmd_hash)
12
12
  if os.path.exists(cache_path) and os.path.getmtime(cache_path) > time.time() - expires:
13
13
  with open(cache_path, "r") as f:
14
14
  return f.read()
@@ -17,13 +17,13 @@ def get_cache(some_string: str, expires: int = 3600) -> str | None:
17
17
 
18
18
 
19
19
  def save_cache(some_string: str, content: str):
20
- cache_path = os.path.join(utils.get_app_directory(), '.cache')
20
+ cache_path = os.path.join(utils.get_base_directory(), '.cache')
21
21
  if not os.path.exists(cache_path):
22
22
  os.makedirs(cache_path)
23
23
  utils.ensure_file_ownership(cache_path)
24
24
 
25
25
  cmd_hash = hashlib.sha256(some_string.encode()).hexdigest()
26
- cache_path = os.path.join(utils.get_app_directory(), '.cache', cmd_hash)
26
+ cache_path = os.path.join(utils.get_base_directory(), '.cache', cmd_hash)
27
27
  with open(cache_path, "w") as f:
28
28
  f.write(content)
29
29
  utils.ensure_file_ownership(cache_path)
@@ -1,4 +1,5 @@
1
1
  from warlock_manager.libs.cmd import Cmd
2
+ import logging
2
3
 
3
4
 
4
5
  class Firewall:
@@ -79,7 +80,7 @@ class Firewall:
79
80
  return None
80
81
 
81
82
  @classmethod
82
- def allow(cls, port: int, protocol: str = 'tcp', comment: str = None) -> None:
83
+ def allow(cls, port: int, protocol: str = 'tcp', comment: str = None) -> bool:
83
84
  """
84
85
  Allows a specific port through the system's firewall.
85
86
  Supports UFW, Firewalld, and iptables.
@@ -90,30 +91,41 @@ class Firewall:
90
91
  comment (str, optional): An optional comment for the rule. Defaults to None.
91
92
  """
92
93
 
94
+ if port <= 0 or port >= 65536:
95
+ logging.error(f"Invalid port number: {port}")
96
+ return False
97
+
93
98
  firewall = cls.get_available()
94
99
 
95
100
  if firewall == 'ufw':
101
+ logging.info(f"Allowing {port}/{protocol} via UFW")
96
102
  cmd = Cmd(['ufw', 'allow', f'{port}/{protocol}'])
97
103
  if comment:
98
104
  cmd.extend(['comment', comment])
99
- cmd.run()
105
+ return cmd.success
100
106
 
101
107
  elif firewall == 'firewalld':
102
- Cmd(['firewall-cmd', '--permanent', '--add-port', f'{port}/{protocol}']).run()
103
- Cmd(['firewall-cmd', '--reload']).run()
108
+ logging.info(f"Allowing {port}/{protocol} via Firewalld")
109
+ if Cmd(['firewall-cmd', '--permanent', '--add-port', f'{port}/{protocol}']).success:
110
+ Cmd(['firewall-cmd', '--reload']).run()
111
+ return True
104
112
 
105
113
  elif firewall == 'iptables':
114
+ logging.info(f"Allowing {port}/{protocol} via iptables")
106
115
  cmd = Cmd(['iptables', '-A', 'INPUT', '-p', protocol, '--dport', str(port), '-j', 'ACCEPT'])
107
116
  if comment:
108
117
  cmd.extend(['-m', 'comment', '--comment', comment])
109
- cmd.run()
110
- Cmd(['service', 'iptables', 'save']).run()
118
+ if cmd.success:
119
+ Cmd(['service', 'iptables', 'save']).run()
120
+ return True
111
121
 
112
122
  else:
113
- raise OSError("No supported firewall found on the system.")
123
+ logging.error('No supported firewall found on the system.')
124
+
125
+ return False
114
126
 
115
127
  @classmethod
116
- def remove(cls, port: int, protocol: str = 'tcp') -> None:
128
+ def remove(cls, port: int, protocol: str = 'tcp') -> bool:
117
129
  """
118
130
  Removes a specific port from the system's firewall.
119
131
  Supports UFW, Firewalld, and iptables.
@@ -123,21 +135,29 @@ class Firewall:
123
135
  protocol (str, optional): The protocol to use ('tcp' or 'udp'). Defaults to 'tcp'.
124
136
  """
125
137
 
138
+ if port <= 0 or port >= 65536:
139
+ logging.error(f"Invalid port number: {port}")
140
+ return False
141
+
126
142
  firewall = cls.get_available()
127
143
 
128
144
  if firewall == 'ufw':
129
- Cmd(['ufw', 'delete', 'allow', f'{port}/{protocol}']).run()
145
+ return Cmd(['ufw', 'delete', 'allow', f'{port}/{protocol}']).success
130
146
 
131
147
  elif firewall == 'firewalld':
132
- Cmd(['firewall-cmd', '--permanent', '--remove-port', f'{port}/{protocol}']).run()
133
- Cmd(['firewall-cmd', '--reload']).run()
148
+ if Cmd(['firewall-cmd', '--permanent', '--remove-port', f'{port}/{protocol}']).success:
149
+ Cmd(['firewall-cmd', '--reload']).run()
150
+ return True
134
151
 
135
152
  elif firewall == 'iptables':
136
- Cmd(['iptables', '-D', 'INPUT', '-p', protocol, '--dport', str(port), '-j', 'ACCEPT']).run()
137
- Cmd(['service', 'iptables', 'save']).run()
153
+ if Cmd(['iptables', '-D', 'INPUT', '-p', protocol, '--dport', str(port), '-j', 'ACCEPT']).success:
154
+ Cmd(['service', 'iptables', 'save']).run()
155
+ return True
138
156
 
139
157
  else:
140
- raise OSError("No supported firewall found on the system.")
158
+ logging.error('No supported firewall found on the system.')
159
+
160
+ return False
141
161
 
142
162
  @classmethod
143
163
  def is_global_open(cls, port: int, protocol: str = 'tcp') -> bool:
@@ -188,4 +208,5 @@ class Firewall:
188
208
  return False
189
209
 
190
210
  else:
191
- raise OSError("No supported firewall found on the system.")
211
+ logging.error('No supported firewall found on the system.')
212
+ return True # No firewall means it's probably enabled by default, so we return true.
@@ -2,8 +2,10 @@ import logging
2
2
  import os
3
3
  import pwd
4
4
  import sys
5
+ from typing_extensions import deprecated
5
6
 
6
7
 
8
+ @deprecated('Please use utils.get_base_directory instead to avoid confusion')
7
9
  def get_app_directory() -> str:
8
10
  """
9
11
  Get the base directory for this game installation.
@@ -15,6 +17,17 @@ def get_app_directory() -> str:
15
17
  return os.path.dirname(os.path.realpath(sys.argv[0]))
16
18
 
17
19
 
20
+ def get_base_directory() -> str:
21
+ """
22
+ Get the base directory for this game installation.
23
+
24
+ This directory usually will contain manage.py, AppFiles, Backups, and other related files.
25
+
26
+ :return:
27
+ """
28
+ return os.path.dirname(os.path.realpath(sys.argv[0]))
29
+
30
+
18
31
  def get_home_directory() -> str:
19
32
  """
20
33
  Get the home directory of the user running this application
@@ -195,7 +195,7 @@ class BaseMod:
195
195
  logging.error('Mod install package not found!')
196
196
  return False
197
197
 
198
- target_archive = os.path.join(utils.get_app_directory(), 'Packages', self.package)
198
+ target_archive = os.path.join(utils.get_base_directory(), 'Packages', self.package)
199
199
  if not os.path.exists(target_archive):
200
200
  download_file(self.source, target_archive)
201
201
  else:
@@ -242,7 +242,7 @@ class BaseMod:
242
242
  Get all registered mods, eg all mods which are present in the registration file
243
243
  :return:
244
244
  """
245
- mods_path = os.path.join(utils.get_app_directory(), 'Packages', 'mods.json')
245
+ mods_path = os.path.join(utils.get_base_directory(), 'Packages', 'mods.json')
246
246
  if not os.path.exists(mods_path):
247
247
  # No mods installed; mods cache is empty.
248
248
  return []
@@ -264,7 +264,11 @@ class BaseMod:
264
264
  :param mods:
265
265
  :return:
266
266
  """
267
- mods_path = os.path.join(utils.get_app_directory(), 'Packages', 'mods.json')
267
+ mods_directory = os.path.join(utils.get_base_directory(), 'Packages')
268
+ if not os.path.exists(mods_directory):
269
+ utils.makedirs(mods_directory)
270
+
271
+ mods_path = os.path.join(utils.get_base_directory(), 'Packages', 'mods.json')
268
272
 
269
273
  flat_mods = []
270
274
  for mod in mods:
@@ -1,3 +1,4 @@
1
+ import json
1
2
  import logging
2
3
  import os
3
4
  import time
@@ -33,10 +34,11 @@ class Nexus:
33
34
  with open(self.email_file, 'r') as f:
34
35
  self.email = f.read().strip()
35
36
 
36
- guid_path = os.path.join(get_app_directory(), '.warlock.guid')
37
- if os.path.exists(guid_path):
38
- with open(guid_path, 'r') as f:
39
- self.game = f.read().strip()
37
+ manager_meta_path = os.path.join(get_app_directory(), '.manage.json')
38
+ if os.path.exists(manager_meta_path):
39
+ with open(manager_meta_path, 'r') as f:
40
+ meta = json.load(f)
41
+ self.game = meta['game']
40
42
 
41
43
  def set_email(self, email: str):
42
44
  """
@@ -273,6 +275,12 @@ class Nexus:
273
275
  'X-Host-Token': self.host_auth,
274
276
  }
275
277
 
278
+ if self.game is None:
279
+ return {
280
+ 'success': False,
281
+ 'message': 'Game not set',
282
+ }
283
+
276
284
  try:
277
285
  url = self.base_url + '/mod/search/' + self.game
278
286
  params = {
@@ -304,6 +312,12 @@ class Nexus:
304
312
  'X-Host-Token': self.host_auth,
305
313
  }
306
314
 
315
+ if self.game is None:
316
+ return {
317
+ 'success': False,
318
+ 'message': 'Game not set',
319
+ }
320
+
307
321
  try:
308
322
  url = self.base_url + '/mod/get/' + self.game + '/' + str(provider) + '/' + str(mod_id)
309
323
  params = {}
@@ -49,7 +49,7 @@ class BaseService(ABC):
49
49
  used for checking existence and loading configuration options from the file
50
50
  """
51
51
 
52
- self._env_file = os.path.join(utils.get_app_directory(), 'Environments', '%s.env' % service)
52
+ self._env_file = os.path.join(utils.get_base_directory(), 'Environments', '%s.env' % service)
53
53
  """
54
54
  :type str:
55
55
  Fully resolved path on the filesystem for the environmental variable for this service
@@ -1274,13 +1274,23 @@ class BaseService(ABC):
1274
1274
  continue
1275
1275
 
1276
1276
  port = self.get_option_value(port_config[0])
1277
+ port_default = self.get_option_default(port_config[0])
1277
1278
  if port == 0:
1278
1279
  # This port does not have a default value, probably not enabled by default.
1279
1280
  continue
1280
1281
  new_port = self.game.get_next_available_port(self, port, port_config[1])
1281
1282
 
1283
+ if port_default == new_port:
1284
+ # New installations where the default port may not trigger the 'change' logic
1285
+ # as the port _technically_ didn't change, therefore the firewall rules won't be added.
1286
+ logging.info('Setting %s to 0 to force firewall change' % port_config[0])
1287
+ self.set_option(port_config[0], 0)
1288
+
1282
1289
  self.set_option(port_config[0], new_port)
1283
- logging.info('Set %s to %s to try to avoid conflicts' % (port_config[0], new_port))
1290
+ if new_port != port:
1291
+ logging.info('Set %s to %s to try to avoid conflicts' % (port_config[0], new_port))
1292
+ else:
1293
+ logging.info('Set %s to %s' % (port_config[0], new_port))
1284
1294
 
1285
1295
  # Reload systemd to pick up the new service
1286
1296
  self.reload()
@@ -1715,14 +1725,14 @@ class BaseService(ABC):
1715
1725
  utils.makedirs(target_file)
1716
1726
  elif source == '@':
1717
1727
  # Source is the package itself; this just copies the entire mod into the destination.
1718
- source_file = os.path.join(utils.get_app_directory(), 'Packages', mod.package)
1728
+ source_file = os.path.join(utils.get_base_directory(), 'Packages', mod.package)
1719
1729
  logging.info('Copying %s -> %s' % (source_file, target_file))
1720
1730
  shutil.copy(source_file, target_file)
1721
1731
  utils.ensure_file_ownership(target_file)
1722
1732
  elif source.startswith('@:'):
1723
1733
  # Source is a file within the package, (usually a ZIP archive)
1724
1734
  source_file = source[2:]
1725
- source_archive = os.path.join(utils.get_app_directory(), 'Packages', mod.package)
1735
+ source_archive = os.path.join(utils.get_base_directory(), 'Packages', mod.package)
1726
1736
  if source_archive.endswith('.zip'):
1727
1737
  logging.info('Extracting %s -> %s' % (source_file, target_file))
1728
1738
  with ZipFile(source_archive, 'r') as zip_ref:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: warlock-manager
3
- Version: 2.2.3
3
+ Version: 2.2.5
4
4
  Summary: Dependency library for game-server management applications.
5
5
  Author-email: Charlie Powell <cdp1337@bitsnbytes.dev>
6
6
  Maintainer-email: Charlie Powell <cdp1337@bitsnbytes.dev>
File without changes