Compare commits

...

9 commits

Author SHA1 Message Date
greenseeker 6592b1b4f1 add 'apt upgrade' to install instructions 2024-02-18 19:01:48 -05:00
greenseeker e2ab01674f add support to upgrade pre-existing installs 2024-02-18 18:58:06 -05:00
greenseeker 88cdae6de0 include configuration defaults in case they are not set in /etc/t2config/config.yaml 2024-02-18 18:54:55 -05:00
greenseeker aca1b4056b get version number from /etc/t2server/version and include in output 2024-02-18 18:53:10 -05:00
greenseeker 0ca42d26f9 improvments to winecmd, dso_cleanup, formatting, and comments 2024-02-18 18:51:32 -05:00
greenseeker a67dc7bb48 add release file 2024-02-18 18:38:17 -05:00
greenseeker d9017910ec include release file with version number 2024-02-18 18:37:41 -05:00
greenseeker 95374b3d23 Increase timeouts 2024-02-18 18:36:49 -05:00
greenseeker c410fcb645 reformatted comments 2024-02-07 20:51:48 -05:00
9 changed files with 377 additions and 281 deletions

View file

@ -11,7 +11,8 @@ t2server has a handful of dependencies which are not automatically handled at th
```
$ sudo dpkg --add-architecture i386
$ sudo apt update
$ sudo apt install unzip xvfb python3-minimal python3-pip wine32 cpulimit
$ sudo apt upgrade
$ sudo apt install unzip xvfb python3-minimal python3-pip wine32 cpulimit less
$ sudo pip3 install tqdm requests pyyaml
```
(more to be tested and added)
@ -47,6 +48,6 @@ RHEL/CentOS 7 and 8 no longer include wine32 in their repos, so installing on th
t2server depends on systemd, so it definitely won't work on any distro that's not using it.
When wine is run in the background, it spawns 2 `wineconsole --use-event=*n*` processes that will consume all available CPU. I have been unable to find a proper solution to this, so for now t2server will run cpulimit against these processes at startup in order to contain them. I have not seen this result in any performance issues.
When wine is run in the background, it spawns 2 "wineconsole --use-event=*nn*" processes that will consume all available CPU. I have been unable to find a proper solution to this, so for now t2server will run cpulimit against these processes at startup in order to contain them. I have not seen this result in any performance issues.
console.log includes ANSI escape sequences to set colors and terminal size, so your shell will get a little funky after tailing this log. Just run `reset` to reinitialize your session and you'll be back to normal.

View file

@ -1,21 +1,20 @@
---
## ServerPrefs indicates the server config file in /etc/t2server/serverprefs
## to use. This is case-sensitive and must match the filename exactly.
## ServerPrefs indicates the server config file in /etc/t2server/serverprefs to
## use. This is case-sensitive and must match the filename exactly.
ServerPrefs: Classic_CTF.cs
## Tribes 2 servers tend to get unstable after a couple weeks of being online.
## Here you can specify a day and hour to automatically bounce the server.
## Set RestartTime to the hour of the day, 0-23, to cycle the server
## (eg. 4=4:00am, 16=4:00pm) or False to disable. Set RestartDay to the
## three-letter abbreviation of the day on which the server should be restarted
## (Sun, Mon, Tue, Wed, Thu, Fri, or Sat). This will be ignored if
## RestartTime = False.
## Here you can specify a day and hour to automatically bounce the server. Set
## RestartTime to the hour of the day, 0-23, to cycle the server (eg. 4=4:00am,
## 16=4:00pm) or False to disable. Set RestartDay to the three-letter
## abbreviation of the day on which the server should be restarted (Sun, Mon,
## Tue, Wed, Thu, Fri, or Sat). This will be ignored if RestartTime = False.
RestartTime: False
RestartDay: Mon
## Set Mod to the directory name of the mod to be loaded, or 'base' for
## vanilla (but why?). This is case-sensitive and must match the subdirectory
## name exactly.
## Set Mod to the directory name of the mod to be loaded, or 'base' for vanilla
## (but why?). This is case-sensitive and must match the subdirectory name
## exactly.
Mod: Classic
## Set Public to False to host a LAN-only game, or to True to host a public
@ -29,14 +28,26 @@ Public: False
## detection. This setting has no effect if Public = False.
OverrideMITM: True
## The built-in master server heartbeat sometimes works inconsistently. If you
## have this problem, you can enable t2server's heartbeat. This has no effect
## if Public = False.
Heartbeat: False
## At startup, Tribes 2 compiles cs scripts into a dso binary format. If those
## scripts are later updated, the changes won't take effect because the game
## will continue to use the compiled dso. If DSOCleanup is enabled, t2server
## will delete all dso files that are older than their associated cs file at
## startup. Note that dso files associated with a cs file that only exists
## within a vl2 package won't be touched and may occasionally need manual
## cleanup.
DSOCleanup: True
## Configure a custom map rotation list. The standard Mission Types are
## "Bounty", "CnH" (Capture and Hold), "CTF" (Capture the Flag), "DM"
## (Deathmatch), "DnD" (Defend and Destroy), "Hunters", "Rabbit", "Siege",
## "TeamHunters", and "TeamRabbit". Your server will always launch with the
## MissionType and Map specified in your serverprefs file ($Host::MissionType
## and $Host::Map), so make sure MissionType matches $Host::MissionType and
## the first map in MapList matches $Host::Map. If you're running a mod that
## handles map rotation, set these to 'False'.
## (Deathmatch), "DnD" (Defend and Destroy), "Hunters", "Rabbit", "Siege", map
## "TeamHunters", and "TeamRabbit". If you're running a mod that handles
## rotation, set these to 'False'.
##
## Example:
## MissionType: CTF
## MapList: ["Katabatic", "Minotaur", "Tombstone"]

1
etc/t2server/release Normal file
View file

@ -0,0 +1 @@
0.8.0

431
setup
View file

@ -62,10 +62,59 @@ def download_file(url, filename):
pbar.close()
return filename
if __name__ == "__main__":
def version_compare(installed_version, package_version):
if installed_version == package_version: return 0
installed = installed_version.split(".")
packaged = package_version.split(".")
if installed[0] < packaged[0]:
return 1
elif installed[0] > packaged[0]:
return -1
if installed[1] < packaged[1]:
return 1
elif installed[1] > packaged[1]:
return -1
if installed[2] < packaged[2]:
return 1
elif installed[2] > packaged[2]:
return -1
action=menu(["~~[C]ontinue","[Q]uit"],header="This script will install Tribes 2 for use as a dedicated server.")
if action == "Q": bail()
if __name__ == "__main__":
# Check for an existing install. Read version from the release file if it exists, otherwise assume 0.7.3
if isfile(release_file):
with open(release_file, 'r') as rf:
installed_version = rf.read().rstrip()
elif isfile(f"{install_dir}/GameData/Tribes2.exe"):
installed_version = "0.7.3"
else:
installed_version = None
# Get the version in this install package
if isfile(f"{pwd}/etc/t2server/release"):
with open(f"{pwd}/etc/t2server/release", 'r') as rf:
package_version = rf.read().rstrip()
else:
package_version = None
# Compare versions to see if the install package is newer than the installed version and quit if not
if installed_version and package_version: upgrade = version_compare(installed_version, package_version)
else: upgrade = None
# Exit if trying to update to an older version or the same version, otherwise display the initial menu and determine the setup mode
if upgrade == -1:
bail(f"This would install t2server {package_version} which is older than what's already installed, version {installed_version}.")
elif upgrade == 0:
bail(f"The existing t2server install is the same version as this install package, {package_version}.")
elif upgrade == 1:
setup_mode=menu([f"[U]pgrade to {package_version}","[Q]uit"],header=f"An existing t2server {installed_version} install was detected. 'Upgrade' will only update the scripts and files that come with t2server and won't reinstall Tribes 2.")
if setup_mode == "Q": bail()
else:
action=menu(["~~[C]ontinue","[Q]uit"],header="This script will install Tribes 2 for use as a dedicated server.")
if action == "Q": bail()
setup_mode="I"
if setup_mode == "R":
system(f"{pwd}/usr/local/bin/t2remove -Y")
# Check if user exists
try:
@ -73,226 +122,232 @@ if __name__ == "__main__":
except KeyError:
user_info = False
# Create or repurpose user
if user_info:
if user_info.pw_dir == install_dir:
pwarn(f"User '{user}' exists and will be reused.")
if setup_mode == "I" or setup_mode == "R":
# Create or repurpose user if installing or reinstalling
if user_info:
if user_info.pw_dir == install_dir:
pwarn(f"User '{user}' exists and will be reused.")
else:
bail(f"ERROR: User '{user}' already exists and may belong to another person or process.")
else:
bail(f"ERROR: User '{user}' already exists and may belong to another person or process.")
else:
pinfo(f"Creating {user} user and {install_dir}.")
system(f"useradd -md {install_dir} {user}")
print(f"Creating {user} user and {install_dir}.")
system(f"useradd -md {install_dir} {user}")
if not user_info: user_info = getuser(user)
if not user_info: user_info = getuser(user)
# Create log_dir
pinfo(f"Creating {log_dir}.")
makedirs(log_dir, mode=0o777, exist_ok=True)
chmod(log_dir, 0o777)
# Create log_dir
print(f"Creating {log_dir}.")
makedirs(log_dir, mode=0o777, exist_ok=True)
chmod(log_dir, 0o777)
# Create .wine dir
pinfo(f"Creating {install_dir}/.wine defaults.")
system(f"su - {user} -c'wineboot -i > /dev/null 2>&1'")
# Create .wine dir
print(f"Creating {install_dir}/.wine defaults.")
system(f"su - {user} -c'wineboot -i > /dev/null 2>&1'")
# Map wine I: drive to pwd T: drive to install_dir and L: to log_dir
pinfo(f"Mapping I: in wine for {user}.")
try:
symlink(f"{pwd}/winbin", f"{install_dir}/.wine/dosdevices/i:")
except FileExistsError:
pass
pinfo(f"Mapping L: in wine for {user}.")
try:
symlink(log_dir, f"{install_dir}/.wine/dosdevices/l:")
except FileExistsError:
pass
pinfo(f"Mapping T: in wine for {user}.")
try:
symlink(install_parent, f"{install_dir}/.wine/dosdevices/t:")
except FileExistsError:
pass
# Check for needed exe/zip/dll files in winbin dir
needed_files=[]
if isfile(f"{pwd}/winbin/tribes2gsi.exe"):
pinfo("tribes2gsi.exe found.")
rename(f"{pwd}/winbin/tribes2gsi.exe",f"{pwd}/winbin/tribes2_gsi.exe")
installer_exe = f"{pwd}/winbin/tribes2_gsi.exe"
elif isfile(f"{pwd}/winbin/tribes2_gsi.exe"):
pinfo("tribes2_gsi.exe found.")
installer_exe = f"{pwd}/winbin/tribes2_gsi.exe"
else:
pwarn("Tribes 2 installer not found.")
needed_files.append("tribes2_gsi.exe")
installer_exe = False
if isfile(f"{pwd}/winbin/TribesNext_rc2a.exe"):
pinfo("TribesNext_rc2a.exe found.")
tnpatch_exe = f"{pwd}/winbin/TribesNext_rc2a.exe"
else:
pwarn("Tribes Next patch not found.")
needed_files.append("TribesNext_rc2a.exe")
tnpatch_exe = False
ruby_dll = f"{pwd}/winbin/msvcrt-ruby191.dll"
instwrap_exe = f"{pwd}/winbin/install_wrapper.exe"
# Download files if needed
if not installer_exe or not tnpatch_exe:
action=menu(["~~[D]ownload automatically","[Q]uit"],header="One or more needed files were not found. Download automatically or quit so they can be manually placed in the 'winbin' subdirectory?")
if needed_files == 2:
needed_files=f"{needed_files[0]} and {needed_files[1]}"
if action=="Q": bail(f"Manually place {needed_files} in the 'winbin' subdirectory then rerun setup.")
if not installer_exe:
for url in installer_mirror_list:
try:
pinfo(f"\nDownloading from {url.split('/')[2]}...")
installer_exe = download_file(url, f"{pwd}/winbin/tribes2_gsi.exe")
if md5sum(installer_exe) == installer_checksum:
pinfo("Checksum validation passed.")
break
else:
perror("Checksum validation failed. Trying next mirror.")
except KeyError:
perror("Download error. Trying next mirror.")
continue
if not installer_exe:
bail("ERROR: Tribes 2 installer could not be downloaded.")
if not tnpatch_exe:
for url in tnpatch_mirror_list:
try:
pinfo(f"\nDownloading from {url.split('/')[2]}...")
tnpatch_exe = download_file(url, f"{pwd}/winbin/TribesNext_rc2a.exe")
if md5sum(tnpatch_exe) == tnpatch_checksum:
pinfo("Checksum validation passed.")
break
else:
perror("Checksum validation failed. Trying next mirror." )
except KeyError:
perror("Download error. Trying next mirror.")
continue
if not tnpatch_exe:
bail("ERROR: Tribes Next patch could not be downloaded.")
# Present SLAs before beginning install
sla = None
while not sla:
sla=menu(["[V]iew Tribes 2 and TribesNext License Agreements", "[A]ccept License Agreements", "[Q]uit"], header="Please take a moment to review and accept the Tribes 2 and TribeNext License Agreements before beginning automated install.")
if sla == "V":
print(color.DY)
system(f"/usr/bin/less {pwd}/sla/tribes2.txt")
print(color.DP)
system(f"/usr/bin/less {pwd}/sla/tribesnext.txt")
sla = None
elif sla == "A":
break
elif sla == "Q":
bail("You must accept the License Agreements to install.")
# Ensure sufficient permissions on winbin and its contents
chmod(f"{pwd}/winbin", 0o777)
chmod(installer_exe, 0o777)
chmod(tnpatch_exe, 0o777)
chmod(instwrap_exe, 0o777)
chmod(ruby_dll, 0o777)
# Execute install wrapper
pinfo(f"\nInstalling Tribes 2 and the TribesNext patch in wine. Please wait...")
chowner(install_dir, user)
system(f"su - {user} -c'xvfb-run -as " + '"-fbdir /var/tmp"' + " wine I:/install_wrapper.exe > /dev/null 2>&1'")
# Rudamentary check to see if T2 install succeeded
if not isfile(f"{install_dir}/Tribes 2 Solo & LAN.lnk"): bail(f"ERROR: Tribes 2 installation appears to have failed. Check {log_dir}/install_wrapper.log")
# Rudamentary check to see if TN install succeeded
if not isfile(f"{install_dir}/GameData/TN_Uninstall.exe"): bail(f"ERROR: Tribes Next installation appears to have failed. Check {log_dir}/install_wrapper.log")
# Replace msvcrt-ruby190.dll with msvcrt-ruby191.dll
pinfo("Updating msvcrt-ruby190.dll to msvcrt-ruby191.dll.\n")
copyfile(ruby_dll,f"{install_dir}/GameData/msvcrt-ruby191.dll")
unlink(f"{install_dir}/GameData/msvcrt-ruby190.dll")
symlink(f"{install_dir}/GameData/msvcrt-ruby191.dll", f"{install_dir}/GameData/msvcrt-ruby190.dll")
# Install addons
for addon in iglob(f"{pwd}/addons/*"):
if addon.endswith(".zip"):
pinfo(f"Unpacking {addon} into {install_dir}/GameData.")
system(f"unzip -qqd {install_dir}/GameData {addon}")
elif addon.endswith((".tar",".tgz",".tar.gz",".txz",".tar.xz",".tbz",".tar.bz")):
pinfo(f"Unpacking {addon} into {install_dir}/GameData.")
system(f"tar -C {install_dir}/GameData -xf {addon}")
elif addon.endswith(".vl2"):
pinfo(f"Copying {addon} to {install_dir}/GameData/base.")
copyfile(addon,f"{install_dir}/GameData/base/{addon.split('/')[-1]}")
elif addon.endswith("readme.txt"):
# Map wine I: drive to pwd T: drive to install_dir and L: to log_dir
print(f"Mapping I: in wine for {user}.")
try:
symlink(f"{pwd}/winbin", f"{install_dir}/.wine/dosdevices/i:")
except FileExistsError:
pass
print(f"Mapping L: in wine for {user}.")
try:
symlink(log_dir, f"{install_dir}/.wine/dosdevices/l:")
except FileExistsError:
pass
print(f"Mapping T: in wine for {user}.")
try:
symlink(install_parent, f"{install_dir}/.wine/dosdevices/t:")
except FileExistsError:
pass
# Check for needed exe/zip/dll files in winbin dir
needed_files=[]
if isfile(f"{pwd}/winbin/tribes2gsi.exe"):
pinfo("tribes2gsi.exe found.")
rename(f"{pwd}/winbin/tribes2gsi.exe",f"{pwd}/winbin/tribes2_gsi.exe")
installer_exe = f"{pwd}/winbin/tribes2_gsi.exe"
elif isfile(f"{pwd}/winbin/tribes2_gsi.exe"):
pinfo("tribes2_gsi.exe found.")
installer_exe = f"{pwd}/winbin/tribes2_gsi.exe"
else:
pwarn(f"Ignoring {addon}.")
pwarn("Tribes 2 installer not found.")
needed_files.append("tribes2_gsi.exe")
installer_exe = False
if isfile(f"{pwd}/winbin/TribesNext_rc2a.exe"):
pinfo("TribesNext_rc2a.exe found.")
tnpatch_exe = f"{pwd}/winbin/TribesNext_rc2a.exe"
else:
pwarn("Tribes Next patch not found.")
needed_files.append("TribesNext_rc2a.exe")
tnpatch_exe = False
ruby_dll = f"{pwd}/winbin/msvcrt-ruby191.dll"
instwrap_exe = f"{pwd}/winbin/install_wrapper.exe"
# Download files if needed
if not installer_exe or not tnpatch_exe:
action=menu(["~~[D]ownload automatically","[Q]uit"],header="One or more needed files were not found. Download automatically or quit so they can be manually placed in the 'winbin' subdirectory?")
if needed_files == 2:
needed_files=f"{needed_files[0]} and {needed_files[1]}"
if action=="Q": bail(f"Manually place {needed_files} in the 'winbin' subdirectory then rerun setup.")
if not installer_exe:
for url in installer_mirror_list:
try:
pinfo(f"\nDownloading from {url.split('/')[2]}...")
installer_exe = download_file(url, f"{pwd}/winbin/tribes2_gsi.exe")
if md5sum(installer_exe) == installer_checksum:
print("Checksum validation passed.")
break
else:
perror("Checksum validation failed. Trying next mirror.")
except KeyError:
perror("Download error. Trying next mirror.")
continue
if not installer_exe:
bail("ERROR: Tribes 2 installer could not be downloaded.")
if not tnpatch_exe:
for url in tnpatch_mirror_list:
try:
pinfo(f"\nDownloading from {url.split('/')[2]}...")
tnpatch_exe = download_file(url, f"{pwd}/winbin/TribesNext_rc2a.exe")
if md5sum(tnpatch_exe) == tnpatch_checksum:
print("Checksum validation passed.")
break
else:
perror("Checksum validation failed. Trying next mirror." )
except KeyError:
perror("Download error. Trying next mirror.")
continue
if not tnpatch_exe:
bail("ERROR: Tribes Next patch could not be downloaded.")
# Present SLAs before beginning install
sla = None
while not sla:
sla=menu(["[V]iew Tribes 2 and TribesNext License Agreements", "[A]ccept License Agreements", "[Q]uit"], header="Please take a moment to review and accept the Tribes 2 and TribeNext License Agreements before beginning automated install.")
if sla == "V":
print(color.DY)
system(f"/usr/bin/less {pwd}/sla/tribes2.txt")
print(color.DP)
system(f"/usr/bin/less {pwd}/sla/tribesnext.txt")
sla = None
elif sla == "A":
break
elif sla == "Q":
bail("You must accept the License Agreements to install.")
# Ensure sufficient permissions on winbin and its contents
chmod(f"{pwd}/winbin", 0o777)
chmod(installer_exe, 0o777)
chmod(tnpatch_exe, 0o777)
chmod(instwrap_exe, 0o777)
chmod(ruby_dll, 0o777)
# Execute install wrapper
pinfo(f"\nInstalling Tribes 2 and the TribesNext patch in wine. Please wait...")
chowner(install_dir, user)
system(f"su - {user} -c'xvfb-run -as " + '"-fbdir /var/tmp"' + " wine I:/install_wrapper.exe > /dev/null 2>&1'")
# Rudamentary check to see if T2 install succeeded
if not isfile(f"{install_dir}/Tribes 2 Solo & LAN.lnk"): bail(f"ERROR: Tribes 2 installation appears to have failed. Check {log_dir}/install_wrapper.log")
# Rudamentary check to see if TN install succeeded
if not isfile(f"{install_dir}/GameData/TN_Uninstall.exe"): bail(f"ERROR: Tribes Next installation appears to have failed. Check {log_dir}/install_wrapper.log")
# Replace msvcrt-ruby190.dll with msvcrt-ruby191.dll
print("Updating msvcrt-ruby190.dll to msvcrt-ruby191.dll.\n")
copyfile(ruby_dll,f"{install_dir}/GameData/msvcrt-ruby191.dll")
unlink(f"{install_dir}/GameData/msvcrt-ruby190.dll")
symlink(f"{install_dir}/GameData/msvcrt-ruby191.dll", f"{install_dir}/GameData/msvcrt-ruby190.dll")
# Install addons
for addon in iglob(f"{pwd}/addons/*"):
if addon.endswith(".zip"):
pinfo(f"Unpacking {addon} into {install_dir}/GameData.")
system(f"unzip -qqd {install_dir}/GameData {addon}")
elif addon.endswith((".tar",".tgz",".tar.gz",".txz",".tar.xz",".tbz",".tar.bz")):
pinfo(f"Unpacking {addon} into {install_dir}/GameData.")
system(f"tar -C {install_dir}/GameData -xf {addon}")
elif addon.endswith(".vl2"):
pinfo(f"Copying {addon} to {install_dir}/GameData/base.")
copyfile(addon,f"{install_dir}/GameData/base/{addon.split('/')[-1]}")
elif addon.endswith("readme.txt"):
pass
else:
pwarn(f"Ignoring {addon}.")
# Copy t2server and t2bouncer to /usr/local/bin/
pinfo("Installing t2server script.")
if setup_mode == "U": print("Updating t2server script.")
else: print("Installing t2server script.")
copyfile(f"{pwd}/usr/local/bin/t2server",f"{bin_dir}/t2server")
pinfo("Installing t2bouncer script.")
if setup_mode == "U": print("Updating t2bouncer script.")
else: print("Installing t2bouncer script.")
copyfile(f"{pwd}/usr/local/bin/t2bouncer",f"{bin_dir}/t2bouncer")
# Set owner/group on install_dir
chowner(install_dir, user)
# Clean up temp dir and some unneeded files
pinfo("A little housekeeping...")
if isfile(f"{install_dir}/Tribes 2 Online.lnk"): unlink(f"{install_dir}/Tribes 2 Online.lnk")
if isfile(f"{install_dir}/Tribes 2 Solo & LAN.lnk"): unlink(f"{install_dir}/Tribes 2 Solo & LAN.lnk")
if isfile(f"{install_dir}/UNWISE.EXE"): unlink(f"{install_dir}/UNWISE.EXE")
if isfile(f"{install_dir}/Readme.txt"): unlink(f"{install_dir}/Readme.txt")
if isfile(f"{install_dir}/GameData/Classic_LAN.bat"): unlink(f"{install_dir}/GameData/Classic_LAN.bat")
if isfile(f"{install_dir}/GameData/Classic_dedicated_server.bat"): unlink(f"{install_dir}/GameData/Classic_dedicated_server.bat")
if isfile(f"{install_dir}/GameData/Classic_online.bat"): unlink(f"{install_dir}/GameData/Classic_online.bat")
if isfile(f"{install_dir}/GameData/base/EULA.txt"): unlink(f"{install_dir}/GameData/base/EULA.txt")
if isfile(f"{install_dir}/GameData/base/UKEULA.txt"): unlink(f"{install_dir}/GameData/base/UKEULA.txt")
if isdir(f"{install_dir}/Manual"): rmtree(f"{install_dir}/Manual")
if isdir(f"{install_dir}/.wine/drive_c/users/t2server/Temp"): rmtree(f"{install_dir}/.wine/drive_c/users/t2server/Temp")
if isfile(f"{install_dir}/t2csri_eula.txt"): unlink(f"{install_dir}/t2csri_eula.txt")
if isfile(f"{install_dir}/Inside\ Team\ Rabbit\ 2.txt"): unlink(f"{install_dir}/Inside\ Team\ Rabbit\ 2.txt")
if isfile(f"{install_dir}/UpdatePatch.txt"): unlink(f"{install_dir}/UpdatePatch.txt")
if isfile(f"{install_dir}/Classic/Classic_readme.txt"): unlink(f"{install_dir}/Classic/Classic_readme.txt")
if isfile(f"{install_dir}/Classic_technical.txt"): unlink(f"{install_dir}/Classic_technical.txt")
if setup_mode == "I" or setup_mode == "R":
# Clean up temp dir and some unneeded files
print("A little housekeeping...")
if isfile(f"{install_dir}/Tribes 2 Online.lnk"): unlink(f"{install_dir}/Tribes 2 Online.lnk")
if isfile(f"{install_dir}/Tribes 2 Solo & LAN.lnk"): unlink(f"{install_dir}/Tribes 2 Solo & LAN.lnk")
if isfile(f"{install_dir}/UNWISE.EXE"): unlink(f"{install_dir}/UNWISE.EXE")
if isfile(f"{install_dir}/Readme.txt"): unlink(f"{install_dir}/Readme.txt")
if isfile(f"{install_dir}/GameData/Classic_LAN.bat"): unlink(f"{install_dir}/GameData/Classic_LAN.bat")
if isfile(f"{install_dir}/GameData/Classic_dedicated_server.bat"): unlink(f"{install_dir}/GameData/Classic_dedicated_server.bat")
if isfile(f"{install_dir}/GameData/Classic_online.bat"): unlink(f"{install_dir}/GameData/Classic_online.bat")
if isfile(f"{install_dir}/GameData/base/EULA.txt"): unlink(f"{install_dir}/GameData/base/EULA.txt")
if isfile(f"{install_dir}/GameData/base/UKEULA.txt"): unlink(f"{install_dir}/GameData/base/UKEULA.txt")
if isdir(f"{install_dir}/Manual"): rmtree(f"{install_dir}/Manual")
if isdir(f"{install_dir}/.wine/drive_c/users/t2server/Temp"): rmtree(f"{install_dir}/.wine/drive_c/users/t2server/Temp")
if isfile(f"{install_dir}/t2csri_eula.txt"): unlink(f"{install_dir}/t2csri_eula.txt")
if isfile(f"{install_dir}/Inside\ Team\ Rabbit\ 2.txt"): unlink(f"{install_dir}/Inside\ Team\ Rabbit\ 2.txt")
if isfile(f"{install_dir}/UpdatePatch.txt"): unlink(f"{install_dir}/UpdatePatch.txt")
if isfile(f"{install_dir}/Classic/Classic_readme.txt"): unlink(f"{install_dir}/Classic/Classic_readme.txt")
if isfile(f"{install_dir}/Classic_technical.txt"): unlink(f"{install_dir}/Classic_technical.txt")
# Create config directory and files
pinfo(f"\nCreating {etc_dir}, default config, and installing prefs files.")
print(f"\nCreating {etc_dir}, default config, and installing prefs files.")
makedirs(f"{etc_dir}/serverprefs", mode=0o775, exist_ok=True)
if isfile(f"{etc_dir}/config.yaml"):
timestamp = int(time())
rename(f"{etc_dir}/config.yaml",f"{etc_dir}/config.yaml.{timestamp}")
pwarn(f"Existing {etc_dir}/config.yaml renamed to {etc_dir}/config.yaml.{timestamp}. Be sure to compare with and update the new config.yaml file.")
pinfo(f"Writing default {etc_dir}/config.yaml.")
print(f"Writing default {etc_dir}/config.yaml.")
copyfile(f"{pwd}/etc/t2server/config.yaml", f"{etc_dir}/config.yaml")
print(f"Writing {etc_dir}/release")
copyfile(f"{pwd}/etc/t2server/release", f"{etc_dir}/release")
for pfile in iglob(f"{pwd}/etc/t2server/serverprefs/*"):
pinfo(f"Copying {pfile} to {etc_dir}/serverprefs.")
copyfile(pfile,f"{etc_dir}/serverprefs/{pfile.split('/')[-1]}")
# Create systemd units
pinfo("\nCreating systemd units:")
pinfo("- t2server service")
print("\nCreating systemd units:")
print("- t2server service")
copyfile(f"{pwd}/etc/systemd/system/t2server.service",f"{unit_dir}/t2server.service")
pinfo("- t2bouncer service")
print("- t2bouncer service")
copyfile(f"{pwd}/etc/systemd/system/t2bouncer.service",f"{unit_dir}/t2bouncer.service")
pinfo("- t2bouncer timer")
print("- t2bouncer timer")
copyfile(f"{pwd}/etc/systemd/system/t2bouncer.timer",f"{unit_dir}/t2bouncer.timer")
system("systemctl daemon-reload")
# Install utility scripts
pinfo("\nInstalling utilities:")
pinfo("- t2bouncer")
print("\nInstalling utilities:")
print("- t2bouncer")
copyfile(f"{pwd}/usr/local/bin/t2fixer",f"{bin_dir}/t2fixer")
pinfo("- t2remove")
print("- t2remove")
copyfile(f"{pwd}/usr/local/bin/t2remove",f"{bin_dir}/t2remove")
pinfo("- t2help")
print("- t2help")
copyfile(f"{pwd}/usr/local/bin/t2help",f"{bin_dir}/t2help")
# Install python module
@ -302,6 +357,6 @@ if __name__ == "__main__":
# Show help
system(f"{bin_dir}/t2help")
menu(['~~[E]xit'],header="You can run 't2help' at any time to view the info above again.")
pinfo("You can run 't2help' at any time to view the info above again.")
print(f"{color.X}\n")

View file

@ -47,6 +47,15 @@ def setperm(file):
pwarn(f"Failed to set permissions on {file}")
log.write(f"... FAILED!\n")
pass
elif file == f"{etc_dir}/release":
log.write(f"Setting mode 444 on {file}")
try:
chmod(file,0o444)
log.write(f"\n")
except:
pwarn(f"Failed to set permissions on {file}")
log.write(f"... FAILED!\n")
pass
else:
log.write(f"Setting mode 664 on file {file}")
try:
@ -100,4 +109,5 @@ with open(f'{log_dir}/t2fixer.log', 'w') as log:
setperm(f"{bin_dir}/t2remove")
setperm(f"{bin_dir}/t2fixer")
setperm(f"{bin_dir}/t2help")
setperm(f"{bin_dir}/t2support.py")
setperm(f"{bin_dir}/t2support.py")
setperm(f"{etc_dir}/release")

View file

@ -1,10 +1,20 @@
#!/usr/bin/env -S python3 -B
from t2support import color
from t2support import color, release_file
from os.path import isfile
# Get the version from the release file
if isfile(release_file):
with open(release_file, 'r') as rf:
version = rf.read().rstrip()
else:
version = None
if version: print(f"{color.DC}t2server {version}")
print(f"\n{color.DC}The follow commands can be used to manage your Tribes 2 server:")
print(f"""\n{color.BW}systemctl <action> t2server{color.DC}: The t2server service can be managed with the
standard Linux systemctl command.""")
standard Linux systemctl command. See 'man systemctl' for more info.""")
print(f"""\n{color.BW}t2fixer{color.DC}: This command resets the owner and permissions on all files associated
with t2server. It's recommended to run this command any time you make changes

View file

@ -10,32 +10,26 @@ from requests import get
from threading import Thread
from t2support import *
winecmd=["/usr/bin/wineconsole", "--backend=curses"]
argbase=["Tribes2.exe","-dedicated"]
basecmd=winecmd+argbase
parent=getppid()
winecmd = ["/usr/bin/wine"]
argbase = ["Tribes2.exe","-dedicated"]
basecmd = winecmd + argbase
parent = getppid()
def dso_cleanup():
"""
This function finds all .dso files (compiled .cs scripts) and deletes them
if they are older than their associated .cs script so that Tribes 2 will
recompile them at startup.
This function finds all .dso files (compiled .cs scripts) and deletes them
so that Tribes 2 will recompile them at startup.
"""
for dso_file in iglob(f"{install_dir}/**/*.dso", recursive=True):
cs_file=dso_file[:-4]
if isfile(cs_file):
cs_mtime=getmtime(cs_file)
dso_mtime=getmtime(dso_file)
if cs_mtime > dso_mtime:
print(f"Deleting {dso_file} so it can be rebuilt.")
unlink(dso_file)
for dso_file in iglob(f"{install_dir}/**/*.dso", recursive = True):
print(f"Deleting {dso_file} so it can be rebuilt.")
unlink(dso_file)
def build_args(basecmd,config):
"""
This function assembles the command line to launch the server based on the
config.yaml file.
"""
server_command=basecmd
server_command = basecmd
if config['Public']:
server_command.append("-online")
else:
@ -63,12 +57,12 @@ def server_files(config):
print(f"Deleting {install_dir}/GameData/{config['Mod']}/prefs/ServerPrefs.cs")
unlink(f"{install_dir}/GameData/{config['Mod']}/prefs/ServerPrefs.cs")
print(f"Linking {install_dir}/GameData/{config['Mod']}/prefs/ServerPrefs.cs -> {etc_dir}/serverprefs/{config['ServerPrefs']}")
makedirs(f"{install_dir}/GameData/{config['Mod']}/prefs", mode=0o755, exist_ok=True)
makedirs(f"{install_dir}/GameData/{config['Mod']}/prefs", mode = 0o755, exist_ok = True)
symlink(f"{etc_dir}/serverprefs/{config['ServerPrefs']}", f"{install_dir}/GameData/{config['Mod']}/prefs/ServerPrefs.cs")
if config["MapList"] and config["MissionType"]:
print(f"Writing {install_dir}/GameData/base/prefs/missions.txt")
makedirs(f"{install_dir}/GameData/base/prefs", mode=0o755, exist_ok=True)
makedirs(f"{install_dir}/GameData/base/prefs", mode = 0o755, exist_ok = True)
with open(f"{install_dir}/GameData/base/prefs/missions.txt", 'w') as mlist:
for mission in config["MapList"]:
mlist.write(f"{config['MissionType']} {mission}\n")
@ -78,7 +72,6 @@ def server_files(config):
with open(f"{install_dir}/GameData/base/prefs/missions.txt", 'w') as mlist:
mlist.write("")
def runaway_control(runaway_proc_cmd):
"""
When run in the background, wine will spawn two 'wineconsole'
@ -88,9 +81,9 @@ def runaway_control(runaway_proc_cmd):
for x in range(20):
sleep(15)
print(f"Checking for runaway '{runaway_proc_cmd}' process...")
runaway_pid=run(["/usr/bin/pgrep","-f", runaway_proc_cmd],stdout=PIPE).stdout
runaway_pid = run(["/usr/bin/pgrep","-f", runaway_proc_cmd],stdout = PIPE).stdout
if runaway_pid:
runaway_pid=str(int(runaway_pid))
runaway_pid = str(int(runaway_pid))
print(f"Limiting runaway wineconsole process: {runaway_pid}")
run(["/usr/bin/cpulimit","-bp", runaway_pid,"-l2"])
break
@ -99,7 +92,7 @@ def master_heartbeat():
"""
A public Tribes 2 server should send a regular heartbeat to the TribexNext
master server so that it appears in the list, however this seems
inconsistent, so this function takes over that responsibility.
inconsistent, so this function takes over that responsibility if enabled.
"""
print("Starting TribesNext heartbeat thread...")
while True:
@ -110,43 +103,24 @@ def is_valid_ip(ip):
"""Check if an ip looks like a valid IP address."""
return bool(match(r"^(\d{1,3}\.){3}\d{1,3}$", ip))
def override_mitm():
"""
Tribes 2 servers try to detect descrepencies between their own IP and the IP
that the client believes it's connecting to as possible man-in-the-middle
attacks, however this often interfers with connections when the server is
NATed or multi-homed. This function gets the public-facing IP of the host
and writes an autoexec script to effectively disable this detection.
"""
for ip_service in ["http://api.ipify.org","http://ifconfig.me","http://ipinfo.io/ip"]:
r=get(ip_service)
if r.status_code == 200 and is_valid_ip(r.text):
print(f"Got public IP address {r.text}")
break
if r.status_code != 200: bail("Could not get this server's public IP address.")
print(f"Overriding Man-in-the-Middle attack detection.")
with open(f'{install_dir}/GameData/base/scripts/autoexec/noMITM.cs', 'w') as nomitm_script:
nomitm_script.write(f'$IPv4::InetAddress = "{r.text}";\n')
if __name__ == "__main__":
# Get the version from the release file and print it.
if isfile(release_file):
with open(release_file, 'r') as rf:
version = rf.read().rstrip()
else:
version = None
if version: print(f"t2server version {version}")
# If run interactively, warn user that they probably don't want to do this unless troubleshooting.
if parent == 1:
print(f"Started by init")
else:
interactive_run=menu(['[Y]es, run t2server interactively.',"~~[N]o, abort."],header="Running t2server directly can be helpful for troubleshooting but generally it's best to manage your server with 'systemctl'. Do you still want to run t2server?",footer="Choose [N] and run 't2help' if you're unsure of what to do.")
interactive_run = menu([f'[Y]es, run t2server interactively.',"~~[N]o, abort."], header = "Running t2server directly can be helpful for troubleshooting but generally it's best to manage your server with 'systemctl'. Do you still want to run t2server?", footer = "Choose [N] and run 't2help' if you're unsure of what to do.")
if interactive_run == 'N': bail()
chdir(f"{install_dir}/GameData")
# Set default configuration
config_defaults = {
'ServerPrefs' : 'Classic_CTF.cs',
'Mod' : 'Classic',
'Public' : False,
'OverrideMITM': True,
'MissionType' : 'CTF',
'MapList' : False
}
# Read configuration from config.yaml
with open(f'{etc_dir}/config.yaml', 'r') as f:
loaded_config = yaml.full_load(f)
@ -161,33 +135,55 @@ if __name__ == "__main__":
if not isfile(f"{etc_dir}/serverprefs/{config['ServerPrefs']}"):
bail(f"Invalid ServerPrefs file: {config['ServerPrefs']}")
# Delete any pre-existing noMITM script/dso. It will be recreated below, if needed.
if isfile(f"{install_dir}/GameData/base/scripts/autoexec/noMITM.cs"): unlink(f"{install_dir}/GameData/base/scripts/autoexec/noMITM.cs")
if isfile(f"{install_dir}/GameData/base/scripts/autoexec/noMITM.cs.dso"): unlink(f"{install_dir}/GameData/base/scripts/autoexec/noMITM.cs.dso")
# Delete any pre-existing 00_t2server_opts script. It will be recreated below, if needed.
if isfile(f"{install_dir}/GameData/base/scripts/autoexec/00_t2server_opts.cs"): unlink(f"{install_dir}/GameData/base/scripts/autoexec/00_t2server_opts.cs")
# Create serverprefs symlink and missions.txt (if appropriate), clean out stale dso files, then assemble the command line arguments to launch the server
# Create serverprefs symlink and missions.txt (if appropriate), clean out dso files, then assemble the command line arguments to launch the server
server_files(config)
dso_cleanup()
server_command=build_args(basecmd,config)
if config['DSOCleanup']: dso_cleanup()
server_command = build_args(basecmd,config)
# If this is a public server, start a hearbeat thread. Also write the MITM override file if configured.
# Start the opts script with a comment at the top indicating this is auto-generated and shouldn't be edited, and also disable PureServer.
opts_script_content = "// This file is updated automatically by t2server based on /etc/t2server/config.yaml.\n// Do not edit this file directly.\n$Host::PureServer = 0;\n"
# If this is a public server...
if config['Public']:
print("Starting heartbeat...")
if config['OverrideMITM']: override_mitm()
heartbeat=Thread(target=master_heartbeat)
heartbeat.daemon=True
heartbeat.start()
# Start a heartbeat thread, if configured.
if config['Heartbeat']:
heartbeat = Thread(target = master_heartbeat)
heartbeat.daemon = True
heartbeat.start()
# Capture the public IP for MITM override, if configured.
if config['OverrideMITM']:
for ip_service in ["http://api.ipify.org","http://ifconfig.me","http://ipinfo.io/ip"]:
r = get(ip_service)
if r.status_code == 200 and is_valid_ip(r.text):
print(f"Got public IP address {r.text}")
break
if r.status_code != 200: bail("Could not get this server's public IP address.")
opts_script_content += f'$IPv4::InetAddress = "{r.text}";\n'
print(f"Overriding Man-in-the-Middle attack detection.")
# If MissionType and MapList are defined, write the first map into 00_t2server_opts.cs
if config['MissionType'] and config['MapList']:
opts_script_content += f'$Host::MissionType = "' + config['MissionType'] + '";\n$Host::Map = "' + config['MapList'][0] + '";\n'
if config['MissionType'] and config['MissionType'].lower() != 'teamrabbit':
opts_script_content += "$Host::LoadTR2Gametype = 0;\n"
# Write all options to the 00_t2server_opts file
with open(f'{install_dir}/GameData/base/scripts/autoexec/00_t2server_opts.cs', 'w') as opts_script:
opts_script.write(opts_script_content)
# Cap the CPU of the runaway wineconsole processes
wcpid1_limit=Thread(target=runaway_control, args=("wineconsole --use-event=52",))
wcpid1_limit = Thread(target = runaway_control, args = ("wineconsole --use-event=52",))
wcpid1_limit.start()
wcpid2_limit=Thread(target=runaway_control, args=("wineconsole --use-event=188",))
wcpid2_limit = Thread(target = runaway_control, args = ("wineconsole --use-event=188",))
wcpid2_limit.start()
# Open the console log file if running as service and start the Tribes 2 server
print(f"Starting Tribes 2 server: " + " ".join(server_command))
if parent == 1:
with open(f"{log_dir}/console.log", 'w') as consolelog:
run(server_command,stdout=consolelog)
run(server_command,stdout = consolelog)
else:
run(server_command)
run(server_command)

View file

@ -14,6 +14,18 @@ etc_dir = "/etc/t2server"
log_dir = "/var/log/t2server"
unit_dir = "/etc/systemd/system"
bin_dir = "/usr/local/bin"
release_file = f"{etc_dir}/release"
config_defaults = {
'ServerPrefs' : 'Classic_CTF.cs',
'Mod' : 'Classic',
'Public' : False,
'OverrideMITM': False,
'Heartbeat' : False,
'DSOCleanup' : True,
'MissionType' : 'CTF',
'MapList' : False
}
class color:
X = '\033[m' # Reset
@ -78,7 +90,7 @@ def menu(option_list,header="",footer=""):
try:
key=search(r'\[([0-9a-zA-Z])\]', option).group(1)
except AttributeError:
pass
bail("Error while processing menu option list.")
if option.startswith("~~"):
default = str(key)
keys.append(key.upper())

View file

@ -21,7 +21,7 @@ WriteLog("Wait for Tribes Vengeance Preorder window: " & WinWait("Tribes: Vengea
WriteLog("Click [Next]: " & ControlClick("Tribes: Vengeance", "Click here to Pre-order Tribes: Vengeance Now!", 5)) ; click "Next >"
; Welcome 2
WriteLog("Wait for Welcome: " & WinWait("Welcome", "Welcome to the Tribes 2 Setup program", 60))
WriteLog("Wait for Welcome: " & WinWait("Welcome", "Welcome to the Tribes 2 Setup program", 180))
WriteLog("Click [Next]: " & ControlClick("Welcome", "Welcome to the Tribes 2 Setup program", 3)) ; click "Next >"
; Credits
@ -46,7 +46,7 @@ WriteLog("Wait for Start Installation window: " & WinWait("Start Installation",
WriteLog("Click [Next]: " & ControlClick("Start Installation", "You are now ready to install Tribes 2.", 3)) ; click "Next >"
; Register
WriteLog("Wait for Register window: " & WinWait("Register", "You can register Tribes 2 on the World Wide Web.", 60))
WriteLog("Wait for Register window: " & WinWait("Register", "You can register Tribes 2 on the World Wide Web.", 180))
WriteLog("Uncheck 'Register Tribes 2 Now': " & ControlClick("Register", "You can register Tribes 2 on the World Wide Web.", 9)) ; uncheck "Register Tribes 2 Now"
WriteLog("Click [Next]: " & ControlClick("Register", "You can register Tribes 2 on the World Wide Web.", 3)) ; click "Next >"