diff --git a/README.md b/README.md index 6a2ed6a..4ffaf67 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/etc/t2server/config.yaml b/etc/t2server/config.yaml index c6d38c6..95fed6f 100644 --- a/etc/t2server/config.yaml +++ b/etc/t2server/config.yaml @@ -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"] diff --git a/etc/t2server/release b/etc/t2server/release new file mode 100644 index 0000000..8adc70f --- /dev/null +++ b/etc/t2server/release @@ -0,0 +1 @@ +0.8.0 \ No newline at end of file diff --git a/setup b/setup index 72d17a4..17dbb6a 100755 --- a/setup +++ b/setup @@ -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") \ No newline at end of file diff --git a/usr/local/bin/t2fixer b/usr/local/bin/t2fixer index 85fcfdc..d9d94c0 100755 --- a/usr/local/bin/t2fixer +++ b/usr/local/bin/t2fixer @@ -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") \ No newline at end of file + setperm(f"{bin_dir}/t2support.py") + setperm(f"{etc_dir}/release") \ No newline at end of file diff --git a/usr/local/bin/t2help b/usr/local/bin/t2help index b5dcee2..b1f4fa0 100755 --- a/usr/local/bin/t2help +++ b/usr/local/bin/t2help @@ -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 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 diff --git a/usr/local/bin/t2server b/usr/local/bin/t2server index 2cd3450..ecc72a4 100755 --- a/usr/local/bin/t2server +++ b/usr/local/bin/t2server @@ -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) diff --git a/usr/local/bin/t2support.py b/usr/local/bin/t2support.py index 6afd43e..f133e66 100644 --- a/usr/local/bin/t2support.py +++ b/usr/local/bin/t2support.py @@ -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()) diff --git a/winbin/install_wrapper.au3 b/winbin/install_wrapper.au3 index 68cc715..25b5361 100644 --- a/winbin/install_wrapper.au3 +++ b/winbin/install_wrapper.au3 @@ -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 >"