-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathArmaDediHelper.py
479 lines (412 loc) · 17.8 KB
/
ArmaDediHelper.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
# Arma Dedi Helper, a self-contained tool to easily create -
# dedicated server configuration files for the 'Arma' series of games.
# Copyright (c) 2024 Tuomas Iso-Heiko
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# 1. This software is only to be used with 'Arma' servers that comply with
# the rules defined by 'Bohemia Interactive a.s'.
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""
ArmaDediHelper is a tool to quickly create local dedicated servers for Arma 3.
"""
import sys
import os
import glob
import shutil
import platform
from html.parser import HTMLParser
import urllib.request
# Version number
# format: Major, Minor, Patch
VERSION_NUMBER = "0.2.0"
# To-do list:
# 1. Make a docs page available through GitHub pages that details how to use this program
# Gracefully exit program, informing the user.
def exit_program(reason):
"""
Graceful program exit wrapper
"""
print(f"\nExiting... Reason: {reason}")
sys.exit(0)
# Verify that the execution environment is ok, executable present etc...
def verify_execution_location():
"""
Checks that we are in the correct location (Arma 3 Root).
It verifies this by looking for arma3server_x64
"""
# The first print has a newline to make reading in terminal easier
print("\nLooking for Arma 3 Server...")
found_64bit = False
# Search with wildcard because Linux servers don't have extension
if glob.glob("arma3server_x64*"):
found_64bit = True
print("Found Arma 3 Server (64bit)")
else:
print("Error: Could not find the 64 bit server!"
"\nMake sure your installation is correct.")
return found_64bit
# Search for ServerProfiles directory, return bool depending on found / created or not.
def find_serverprofiles_dir():
"""
Checks if ./ServerProfiles/ exists.
If it doesn't ask the user if they want to make it
"""
# The first print has a newline to make reading in terminal easier
print("\nLooking for ServerProfiles directory...")
# First check - if not found, ask user if we want to create
if not os.path.isdir('ServerProfiles'):
print("Error: Could not find ServerProfiles directory."
"\nDo you wish to create one now?")
create_serverProfiles_prompt = input("Y / n: ") or "Y"
if create_serverProfiles_prompt == "Y":
try:
os.mkdir("ServerProfiles")
except Exception as error:
print("Error while creating ServerProfiles directory: ", error)
return False
# Second check, actual return of the function.
# Will take into account if mkdir fails
if os.path.isdir('ServerProfiles'):
print("ServerProfiles directory found!")
return True
else:
print("Error: Could not find ServerProfiles directory!")
return False
# Search for the 'basic configuration files' we'll use for all profiles later on.
def find_base_configuration():
"""
Verifies that the base configuration (base_basic.cfg and base_server.cfg) exist.
Also informs the user of the checks.
"""
# The first print has a newline to make reading in terminal easier
print("\nLooking for base configs in ServerProfiles...")
verify_default_configs()
found_base_server = False
if os.path.isfile('ServerProfiles\\base_server.cfg'):
print("Found base base_server.cfg!")
found_base_server = True
else:
print("Error: Could not find base_server.cfg!"
"\nPlease create base_server.cfg in the ServerProfiles directory.")
found_base_basic = False
if os.path.isfile('ServerProfiles\\base_basic.cfg'):
print("Found base base_basic.cfg!")
found_base_basic = True
else:
print("Error: Could not find base_basic.cfg!"
"\nPlease create base_basic.cfg in the ServerProfiles directory.")
# Both need to exist
return (found_base_server and found_base_basic)
# Search for mod presets exported from launcher, from the ServerProfiles directory.
def find_modpresets():
"""
Searches ./ServerProfiles/ for A3 Launcher exported mod presets
"""
# The first print has a newline to make reading in terminal easier
print("\nLooking for mod presets in ServerProfiles...")
all_found_presets = []
for file in glob.glob("ServerProfiles/*.html"):
try:
with open(file, "r", -1, "utf-8-sig") as open_file:
for line in open_file:
# This string should appear at least twice in the first 10 lines
if "Arma 3 Launcher" in line:
all_found_presets.append(file)
break
except Exception as error:
print("Error while searching for mod presets: ", error)
return []
if len(all_found_presets) == 0:
print("Could not find any mod presets in ServerProfiles!"
"\nMake sure you have placed the preset in the right directory."
"\nIf you don't have a preset, export one from the Arma 3 Launcher.")
else:
print("Found the following presets:")
for preset in all_found_presets:
print(preset)
return all_found_presets
# Ask user which preset to use
def user_prompt_preset(presets):
"""
Asks user which preset they want to use.
Presets are found from ./ServerProfiles/
"""
# The first print has a newline to make reading in terminal easier
presetname = input("\nPlease type the name of the mod preset to use: ")
found_preset = ""
for preset in presets:
if presetname in preset:
found_preset = preset
break
if found_preset:
print(f"Selected preset: {found_preset}")
else:
print("Error: Query did not match any of the previously found presets!"
"\nMake sure that your capitalization is correct.")
return found_preset
# Finds a substring between two characters
# This code has been written by ChatGPT 3.5
def extract_substring(string, start_char, end_char):
"""
Helper function to extract a substring.
Made by ChatGPT 3.5
"""
start_index = string.find(start_char)
if start_index == -1:
return None
start_index += len(start_char)
end_index = string.find(end_char, start_index)
if end_index == -1:
return None
return string[start_index:end_index]
# Check if the files for this preset are present, if not, prompt to create them now.
def check_preset_files(preset):
"""
Prompts the user to create the preset's directory, or what to do if it already exists
"""
# The first print has a newline to make reading in terminal easier
print("\nLooking for preset configuration...")
preset_name = extract_substring(preset, "\\", ".")
preset_path = f"ServerProfiles\\{preset_name}\\"
if not os.path.isdir(preset_path):
print(f"Failed to find the directory for this preset ({preset_path}).")
create_preset_prompt = input("Would you like to create them now? (Y/n): ") or "Y"
if create_preset_prompt.upper() == "Y":
if not create_preset_files(preset, preset_name):
return False # Return false if fails
else:
try:
recreate_preset_prompt = input("Found the preset's directory!"
" Enter the number of the action you wish to take:"
"\n(Default - 1): Nothing, don't touch the files!"
"\n(2): Regenerate the mod parameter"
"\n(3): Regenerate everything,"
" this will remove any modifications you've made!\n"
) or 1
recreate_preset_prompt = int(recreate_preset_prompt)
except Exception as error:
print("Error: Failed to parse your input,"
f" make sure you've entered an integer!\nError: {error}")
return False
match recreate_preset_prompt:
case 2:
print("Rewriting params.txt")
if not write_params_file(preset, preset_name):
return False # return false if fails
case 3:
print("Rewriting everything")
if not create_preset_files(preset, preset_name):
return False # Return false if fails
case _:
print("Doing nothing ...")
return True
def verify_default_configs() -> None:
"""
Checks that base_basic.cfg and base_server.cfg exist.
If they do not, they will be downloaded from the GitHub repository.
"""
# Verify basic configuration
if not os.path.isfile("ServerProfiles\\base_basic.cfg"):
# Download the default one from GitHub
with urllib.request.urlopen("https://raw.githubusercontent.com/rekterakathom/"
"ArmaDediHelper/main/configs/base_basic.cfg",
timeout=10) as response:
file_path = "ServerProfiles\\base_basic.cfg"
content = response.read()
if response.getcode() == 200:
with open(file_path, 'wb') as file:
file.write(content)
print('basic.cfg downloaded successfully')
else:
print('Failed to download basic.cfg')
# Verify server configuration
if not os.path.isfile("ServerProfiles\\base_server.cfg"):
# Download the default one from GitHub
with urllib.request.urlopen("https://raw.githubusercontent.com/rekterakathom/"
"ArmaDediHelper/main/configs/base_server.cfg",
timeout=10) as response:
file_path = "ServerProfiles\\base_server.cfg"
content = response.read()
if response.getcode() == 200:
with open(file_path, 'wb') as file:
file.write(content)
print('server.cfg downloaded successfully')
else:
print('Failed to download server.cfg')
# Create the preset folders and all the files in it
def create_preset_files(preset, preset_name):
"""
Creates the files for the preset
"""
# The first print has a newline to make reading in terminal easier
print("\nCreating preset files...")
# Make the directory
if not os.path.isdir(f"ServerProfiles\\{preset_name}"):
try:
os.mkdir(f"ServerProfiles\\{preset_name}")
except Exception as error:
print("Error while creating directory for preset: ", error)
return False
verify_default_configs()
# Copy over the basic configuration
try:
shutil.copy("ServerProfiles\\base_server.cfg", f"ServerProfiles\\{preset_name}\\server.cfg")
shutil.copy("ServerProfiles\\base_basic.cfg", f"ServerProfiles\\{preset_name}\\basic.cfg")
except Exception as error:
print("Error while copying config files: ", error)
return False
# Create the start script
print("\nCreating the start script...")
if platform.system() == "Windows":
print("Detected platform: Windows - creating batch script 'start.bat'")
try:
with open(f"ServerProfiles\\{preset_name}\\start.bat", "w") as startscript:
startscript.write('start "" "..\\..\\arma3server_x64.exe"'
' -cfg="%~dp0basic.cfg"'
' -config="%~dp0server.cfg"'
' -profiles="%~dp0Profiles"'
' -port=2302'
' -par="%~dp0params.txt"')
except Exception as error:
print("Error while creating the batch script: ", error)
return False
if not write_params_file(preset, preset_name):
return False # Return False if fails
return True
# Fetch all the mods from the HTML document
def get_mods_from_preset(preset, preset_name):
"""
Parses a modlist from an A3 launcher exported preset list
"""
class PresetParser(HTMLParser):
def __init__(self):
super().__init__()
self.found_mods = {}
self.current_name = ""
self.current_link = ""
self.current_attribute = ""
def handle_starttag(self, tag, attrs):
for attr, value in attrs:
if attr == "data-type":
if value == "DisplayName":
self.current_attribute = "DisplayName"
break
if value == "Link":
self.current_attribute = "Link"
break
else:
self.current_attribute = ""
def handle_data(self, data):
if self.current_attribute == "DisplayName":
self.current_name = data.strip()
if self.current_attribute == "Link":
self.current_link = data.strip()
def handle_endtag(self, tag):
if hasattr(self, 'current_name') and hasattr(self, 'current_link'):
self.found_mods[self.current_name] = self.current_link
del self.current_name
del self.current_link
self.current_attribute = ""
def get_found_mods(self):
return self.found_mods
parser = PresetParser()
try:
with open(preset, "r", -1, "utf-8-sig") as presetfile:
parser.feed(presetfile.read())
except Exception as error:
print("Error while reading the mod preset: ", error)
return []
mod_id_list = []
print(f"\nFound the following mods from the preset {preset_name}:")
for name, url in parser.get_found_mods().items():
print(f"{name} - {url}")
id_index = url.find("=")
if id_index != -1:
mod_id_list.append(url[id_index + 1:])
return mod_id_list
def write_params_file(preset, preset_name):
"""
Writes the params file.
We use it only to load the mods,
as modlists will otherwise be too long to handle without params file
"""
print("\nBuilding mod parameter...")
# Get modlist
modlist = get_mods_from_preset(preset, preset_name)
modparam = ""
for mod in modlist:
try:
path = os.path.abspath(f"..\\..\\workshop\\content\\107410\\{mod}")
modparam += (path + ";")
except Exception as error:
print("Error while building mod parameter: ", error)
return False
print("\nCreating the parameter file...")
try:
with open(f"ServerProfiles\\{preset_name}\\params.txt", "w", -1, "UTF-8") as paramsfile:
paramsfile.write(f'-servermod=""\n-mod="{modparam}"')
except Exception as error:
print("Error while writing parameter.txt file: ", error)
return False
return True
def print_server_instructions():
"""
Tell the user how to use their new server
"""
print("\nSetup finished!")
print("You now have the minimum required server configuration in"
" '\\ServerProfiles\\<name-of-preset>\\'")
print("\nIt is highly recommended that you now manually"
" tweak the configs and the startup script to your needs.")
print("You can find the server logs (.rpt files) to assist you in this endeavour in"
" '\\ServerProfiles\\<name-of-preset>\\profile\\'")
print("\nTo start the server, run the shell script located in the specified directory.")
# Entry point
def main():
"""
Sequentially executes all necessary functions to create a dedicated server instance
"""
print(f"\nWelcome to Arma Dedi Helper!\nVersion: {VERSION_NUMBER}")
# Verify that this script is being executed in the server root
if not verify_execution_location():
exit_program("Could not find server files.")
# Check that ServerProfiles exists, we'll need it later
if not find_serverprofiles_dir():
exit_program("Could not find ServerProfiles directory.")
# Look for the base configuration files
if not find_base_configuration():
exit_program("Could not find base configuration files in ServerProfiles.")
# Check that we have modpresets to use
presets = find_modpresets()
if len(presets) == 0:
exit_program("Could not find any presets to use.")
# Prompt the user for the preset they would like to use
selected_preset = user_prompt_preset(presets)
if not selected_preset:
exit_program("Preset selection failed.")
# If preset doesn't have files set up, create them
# Always regenerate modlist if argument is passed
if not check_preset_files(selected_preset):
exit_program("Failed to create server files.")
# Inform the user how to run their new server installation and exit :)
print_server_instructions()
exit_program("Script complete.")
# Don't run as a module
if __name__ == "__main__":
main()