Browse Source

Initial commit

nosoop 2 years ago
commit
29918642cf
7 changed files with 698 additions and 0 deletions
  1. 14 0
      .gitignore
  2. 94 0
      BUILD.md
  3. 15 0
      README.md
  4. 117 0
      configure.py
  5. 183 0
      misc/ninja_syntax.py
  6. 16 0
      misc/spcomp_util.py
  7. 259 0
      scripting/auto_steam_update.sp

+ 14 - 0
.gitignore

@@ -0,0 +1,14 @@
+# ---> SourceMod
+# Ignore compiled SourceMod plugins
+
+*.smx
+
+# Ignore ninja build stuff
+# (These should be handled with configure.py)
+**/__pycache__
+build/
+build.ninja
+
+# Ignore personal configuration files copied from contrib
+/modd.conf
+/uploader.ini

+ 94 - 0
BUILD.md

@@ -0,0 +1,94 @@
+# SourceMod Ninja Project Build Instructions
+
+This project uses the [Ninja Project Template for SourceMod Plugins][].
+
+The following is documentation of build steps required for the repository at the time this
+project was generated; the source template may be different in the future.
+
+[Ninja Project Template for SourceMod Plugins]: https://github.com/nosoop/NinjaBuild-SMPlugin
+
+## Prerequisites
+
+A few things are needed for developing with this environment:
+
+- A familiarity with command line tooling.
+    - An understanding of calling programs and changing directories will do.
+- The ninja build system.
+    - It's small, fast, cross-platform, and isn't tied to any particular set of tools.
+- Python 3.6 or newer.
+    - Used to detect our compiler and write out the build script for ninja.
+- A clean copy of the [SourceMod][] compiler.  It should not contain any third-party includes.
+    - Which version you'll need depends on the project, but assume latest stable if not
+    specified.
+    - An untouched compiler directory ensures build consistency by not polluting it with custom
+    files — all non built-in dependencies should be encapsulated in the project
+    repository (by adding the dependency as a submodule or copying the include files directly).
+    - You only need the `addons/sourcemod/scripting/` directory from the SourceMod package.
+    - The scripting directory doesn't need to be in `%PATH%` / `$PATH`; the script provides
+    `--spcomp-dir`.  This also allows you to quickly switch between compiler / SourceMod
+    versions.
+    - Do not add / commit the compiler and core includes into your project; the clean compiler
+    can be shared between projects and updated independently from your project.  However, *do*
+    add third-party includes into your project.
+
+You only need to install these once, but make sure to skim over the `README.md` in case other
+projects using this project template require additional software dependencies.
+
+<details>
+<summary>Expand to see instructions for installing dependencies</summary>
+
+1. Install `ninja`.
+    - You can download the latest version for Windows / Mac / Linux from the [ninja releases][]
+    page and install it into your path.
+    - With [Scoop][] on Windows, use `scoop install ninja`.
+    - With Debian and Debian-based distributions like Ubuntu, `apt install ninja-build` will get
+    you the distro's version, which may be a few versions behind current.  That should be fine
+    enough in most cases.
+2. Install Python 3.
+    - You can download and install it manually from [the official site][Python].
+    - With [Scoop][], `scoop install python`.
+    - With Debian-based distributions, `apt install python3`.
+3. Download the [SourceMod][] compiler.
+    - On Linux, both 32- and 64-bit versions of `spcomp` are supported by the build script; you
+    do not need to install 32-bit compatibility libraries on your build machine.
+
+</details>
+
+[ninja releases]: https://github.com/ninja-build/ninja/releases
+[Python]: https://www.python.org/
+[Scoop]: https://scoop.sh/
+[SourceMod]: https://www.sourcemod.net/
+
+## Building
+
+The tl;dr is that you should be able to build any git-based project in this format with the
+following commands:
+
+    git clone --recurse-submodules ${repo}
+    # cd into repo
+    python3 configure.py --spcomp-dir ${dir}
+    ninja
+
+Detailed explanation:
+
+1. Clone the repository and any git repositories it depends on, then navigate to it.
+2. Run `python3 configure.py --spcomp-dir ${dir}` within the project root, where `${dir}` is a
+directory containing the SourcePawn compiler (`spcomp`) and SourceMod's base include files.
+This will create the `build.ninja` script.
+    - You may need to use `python3.8` or some other variant of the executable name, depending on
+    your environment.
+    - If `--spcomp-dir` isn't specified, the script will try to detect the compiler based on an
+    existing `spcomp` executable in your path.
+    - Do not add `build.ninja` to version control; it should always be generated from
+    `configure.py` as it contains paths specific to your filesystem.  (It's ignored by default,
+    but mentioned here for emphasis.)
+3. Run `ninja`; this will read the `build.ninja` script and build things as necessary.  Files
+will be generated and copied to `build/`, creating any intermediate folders if they don't exist.
+Re-run `ninja` whenever you make changes to rebuild any files as necessary.
+    - You do not have to re-run `python3 configure.py` yourself; running `ninja` will do this
+    for you if `configure.py` itself is modified, and it will pass in the same parameters you
+    originally used.  You may want to re-run it yourself if you change the options.
+    - Any files removed from `configure.py` will remain in `build/`; run `ninja -t cleandead`
+    to remove any lingering outputs.
+    - In case you need to wipe the build outputs, delete `build.ninja` and `build/`, then start
+    from step 2.

+ 15 - 0
README.md

@@ -0,0 +1,15 @@
+# Auto Steam Update
+
+Modified version of [Doctor McKay's Automatic Steam Update plugin][autosteamupdate].
+
+[autosteamupdate]: https://github.com/DoctorMcKay/sourcemod-plugins/blob/master/scripting/auto_steam_update.sp
+
+## Building
+
+This project is configured for building via [Ninja][]; see `BUILD.md` for detailed
+instructions on how to build it.
+
+If you'd like to use the build system for your own projects,
+[the template is available here](https://github.com/nosoop/NinjaBuild-SMPlugin).
+
+[Ninja]: https://ninja-build.org/

+ 117 - 0
configure.py

@@ -0,0 +1,117 @@
+#!/usr/bin/python
+
+# plugin names, relative to `scripting/`
+plugins = [
+	'auto_steam_update.sp',
+]
+
+# files to copy to builddir, relative to root
+# plugin names from previous list will be copied automatically
+copy_files = [ ]
+
+# additional directories for sourcepawn include lookup
+# `scripting/include` is explicitly included
+include_dirs = [
+	'third_party/vendored',
+]
+
+# required version of spcomp (presumably pinned to SM version)
+spcomp_min_version = (1, 10)
+
+########################
+# build.ninja script generation below.
+
+import contextlib
+import misc.ninja_syntax as ninja_syntax
+import misc.spcomp_util
+import os
+import sys
+import argparse
+import platform
+import shutil
+
+parser = argparse.ArgumentParser('Configures the project.')
+parser.add_argument('--spcomp-dir',
+		help = 'Directory with the SourcePawn compiler.  Will check PATH if not specified.')
+
+args = parser.parse_args()
+
+print("""Checking for SourcePawn compiler...""")
+spcomp = shutil.which('spcomp', path = args.spcomp_dir)
+if 'x86_64' in platform.machine():
+	# Use 64-bit spcomp if architecture supports it
+	spcomp = shutil.which('spcomp64', path = args.spcomp_dir) or spcomp
+if not spcomp:
+	raise FileNotFoundError('Could not find SourcePawn compiler.')
+
+available_version = misc.spcomp_util.extract_version(spcomp)
+version_string = '.'.join(map(str, available_version))
+print('Found SourcePawn compiler version', version_string, 'at', os.path.abspath(spcomp))
+
+if spcomp_min_version > available_version:
+	raise ValueError("Failed to meet required compiler version "
+			+ '.'.join(map(str, spcomp_min_version)))
+
+with contextlib.closing(ninja_syntax.Writer(open('build.ninja', 'wt'))) as build:
+	build.comment('This file is used to build SourceMod plugins with ninja.')
+	build.comment('The file is automatically generated by configure.py')
+	build.newline()
+	
+	vars = {
+		'configure_args': sys.argv[1:],
+		'root': '.',
+		'builddir': 'build',
+		'spcomp': spcomp,
+		'spcflags': [ '-i${root}/scripting/include', '-h', '-v0' ]
+	}
+	
+	vars['spcflags'] += ('-i{}'.format(d) for d in include_dirs)
+	
+	for key, value in vars.items():
+		build.variable(key, value)
+	build.newline()
+	
+	build.comment("""Regenerate build files if build script changes.""")
+	build.rule('configure',
+			command = sys.executable + ' ${root}/configure.py ${configure_args}',
+			description = 'Reconfiguring build', generator = 1)
+	
+	build.build('build.ninja', 'configure',
+			implicit = [ '${root}/configure.py', '${root}/misc/ninja_syntax.py' ])
+	build.newline()
+	
+	build.rule('spcomp', deps = 'msvc',
+			command = '${spcomp} ${in} ${spcflags} -o ${out}',
+			description = 'Compiling ${out}')
+	build.newline()
+	
+	# Platform-specific copy instructions
+	if platform.system() == "Windows":
+		build.rule('copy', command = 'cmd /c copy ${in} ${out} > NUL',
+				description = 'Copying ${out}')
+	elif platform.system() == "Linux":
+		build.rule('copy', command = 'cp ${in} ${out}', description = 'Copying ${out}')
+	build.newline()
+	
+	build.comment("""Compile plugins specified in `plugins` list""")
+	for plugin in plugins:
+		smx_plugin = os.path.splitext(plugin)[0] + '.smx'
+		
+		sp_file = os.path.normpath(os.path.join('$root', 'scripting', plugin))
+		
+		smx_file = os.path.normpath(os.path.join('$builddir', 'plugins', smx_plugin))
+		build.build(smx_file, 'spcomp', sp_file)
+	build.newline()
+	
+	build.comment("""Copy plugin sources to build output""")
+	for plugin in plugins:
+		sp_file = os.path.normpath(os.path.join('$root', 'scripting', plugin))
+		
+		dist_sp = os.path.normpath(os.path.join('$builddir', 'scripting', plugin))
+		build.build(dist_sp, 'copy', sp_file)
+	build.newline()
+	
+	build.comment("""Copy other files from source tree""")
+	for filepath in copy_files:
+		build.build(os.path.normpath(os.path.join('$builddir', filepath)), 'copy',
+				os.path.normpath(os.path.join('$root', filepath)))

+ 183 - 0
misc/ninja_syntax.py

@@ -0,0 +1,183 @@
+#!/usr/bin/python
+
+"""Python module for generating .ninja files.
+
+Note that this is emphatically not a required piece of Ninja; it's
+just a helpful utility for build-file-generation systems that already
+use Python.
+"""
+
+import re
+import textwrap
+
+def escape_path(word):
+    return word.replace('$ ', '$$ ').replace(' ', '$ ').replace(':', '$:')
+
+class Writer(object):
+    def __init__(self, output, width=78):
+        self.output = output
+        self.width = width
+
+    def newline(self):
+        self.output.write('\n')
+
+    def comment(self, text):
+        for line in textwrap.wrap(text, self.width - 2, break_long_words=False,
+                                  break_on_hyphens=False):
+            self.output.write('# ' + line + '\n')
+
+    def variable(self, key, value, indent=0):
+        if value is None:
+            return
+        if isinstance(value, list):
+            value = ' '.join(filter(None, value))  # Filter out empty strings.
+        self._line('%s = %s' % (key, value), indent)
+
+    def pool(self, name, depth):
+        self._line('pool %s' % name)
+        self.variable('depth', depth, indent=1)
+
+    def rule(self, name, command, description=None, depfile=None,
+             generator=False, pool=None, restat=False, rspfile=None,
+             rspfile_content=None, deps=None):
+        self._line('rule %s' % name)
+        self.variable('command', command, indent=1)
+        if description:
+            self.variable('description', description, indent=1)
+        if depfile:
+            self.variable('depfile', depfile, indent=1)
+        if generator:
+            self.variable('generator', '1', indent=1)
+        if pool:
+            self.variable('pool', pool, indent=1)
+        if restat:
+            self.variable('restat', '1', indent=1)
+        if rspfile:
+            self.variable('rspfile', rspfile, indent=1)
+        if rspfile_content:
+            self.variable('rspfile_content', rspfile_content, indent=1)
+        if deps:
+            self.variable('deps', deps, indent=1)
+
+    def build(self, outputs, rule, inputs=None, implicit=None, order_only=None,
+              variables=None, implicit_outputs=None, pool=None):
+        outputs = as_list(outputs)
+        out_outputs = [escape_path(x) for x in outputs]
+        all_inputs = [escape_path(x) for x in as_list(inputs)]
+
+        if implicit:
+            implicit = [escape_path(x) for x in as_list(implicit)]
+            all_inputs.append('|')
+            all_inputs.extend(implicit)
+        if order_only:
+            order_only = [escape_path(x) for x in as_list(order_only)]
+            all_inputs.append('||')
+            all_inputs.extend(order_only)
+        if implicit_outputs:
+            implicit_outputs = [escape_path(x)
+                                for x in as_list(implicit_outputs)]
+            out_outputs.append('|')
+            out_outputs.extend(implicit_outputs)
+
+        self._line('build %s: %s' % (' '.join(out_outputs),
+                                     ' '.join([rule] + all_inputs)))
+        if pool is not None:
+            self._line('  pool = %s' % pool)
+
+        if variables:
+            if isinstance(variables, dict):
+                iterator = iter(variables.items())
+            else:
+                iterator = iter(variables)
+
+            for key, val in iterator:
+                self.variable(key, val, indent=1)
+
+        return outputs
+
+    def include(self, path):
+        self._line('include %s' % path)
+
+    def subninja(self, path):
+        self._line('subninja %s' % path)
+
+    def default(self, paths):
+        self._line('default %s' % ' '.join(as_list(paths)))
+
+    def _count_dollars_before_index(self, s, i):
+        """Returns the number of '$' characters right in front of s[i]."""
+        dollar_count = 0
+        dollar_index = i - 1
+        while dollar_index > 0 and s[dollar_index] == '$':
+            dollar_count += 1
+            dollar_index -= 1
+        return dollar_count
+
+    def _line(self, text, indent=0):
+        """Write 'text' word-wrapped at self.width characters."""
+        leading_space = '  ' * indent
+        while len(leading_space) + len(text) > self.width:
+            # The text is too wide; wrap if possible.
+
+            # Find the rightmost space that would obey our width constraint and
+            # that's not an escaped space.
+            available_space = self.width - len(leading_space) - len(' $')
+            space = available_space
+            while True:
+                space = text.rfind(' ', 0, space)
+                if (space < 0 or
+                    self._count_dollars_before_index(text, space) % 2 == 0):
+                    break
+
+            if space < 0:
+                # No such space; just use the first unescaped space we can find.
+                space = available_space - 1
+                while True:
+                    space = text.find(' ', space + 1)
+                    if (space < 0 or
+                        self._count_dollars_before_index(text, space) % 2 == 0):
+                        break
+            if space < 0:
+                # Give up on breaking.
+                break
+
+            self.output.write(leading_space + text[0:space] + ' $\n')
+            text = text[space+1:]
+
+            # Subsequent lines are continuations, so indent them.
+            leading_space = '  ' * (indent+2)
+
+        self.output.write(leading_space + text + '\n')
+
+    def close(self):
+        self.output.close()
+
+
+def as_list(input):
+    if input is None:
+        return []
+    if isinstance(input, list):
+        return input
+    return [input]
+
+
+def escape(string):
+    """Escape a string such that it can be embedded into a Ninja file without
+    further interpretation."""
+    assert '\n' not in string, 'Ninja syntax does not allow newlines'
+    # We only have one special metacharacter: '$'.
+    return string.replace('$', '$$')
+
+
+def expand(string, vars, local_vars={}):
+    """Expand a string containing $vars as Ninja would.
+
+    Note: doesn't handle the full Ninja variable syntax, but it's enough
+    to make configure.py's use of it work.
+    """
+    def exp(m):
+        var = m.group(1)
+        if var == '$':
+            return '$'
+        return local_vars.get(var, vars.get(var, ''))
+    return re.sub(r'\$(\$|\w*)', exp, string)

+ 16 - 0
misc/spcomp_util.py

@@ -0,0 +1,16 @@
+#!/usr/bin/python3
+
+import subprocess
+import io
+
+def extract_version(spcomp):
+	"""
+	Extract version string from caption in SourcePawn compiler into a tuple.
+	The string is hardcoded in `setcaption(void)` in `sourcepawn/compiler/parser.cpp`
+	"""
+	p = subprocess.Popen([spcomp], stdout=subprocess.PIPE)
+	caption = io.TextIOWrapper(p.stdout, encoding="utf-8").readline()
+	
+	# extracts last element from output in format "SourcePawn Compiler major.minor.rev.patch"
+	*_, version = caption.split()
+	return tuple(map(int, version.split('.')))

+ 259 - 0
scripting/auto_steam_update.sp

@@ -0,0 +1,259 @@
+#pragma semicolon 1
+
+#include <sourcemod>
+#include <sdktools>
+#include <steamtools>
+
+#undef REQUIRE_PLUGIN
+#tryinclude <updater>
+
+#define UPDATE_URL    "http://hg.doctormckay.com/public-plugins/raw/default/automatic_steam_update.txt"
+#define PLUGIN_VERSION "1.9.1"
+
+#define ALERT_SOUND "ui/system_message_alert.wav"
+
+new Handle:delayCvar;
+new Handle:timerCvar;
+new Handle:messageTimeCvar;
+new Handle:lockCvar;
+new Handle:passwordCvar;
+new Handle:kickMessageCvar;
+new Handle:shutdownMessageCvar;
+new Handle:hudXCvar;
+new Handle:hudYCvar;
+new Handle:hudRCvar;
+new Handle:hudGCvar;
+new Handle:hudBCvar;
+new Handle:updaterCvar;
+new Handle:restartTimer;
+new bool:suspendPlugin = false;
+new timeRemaining = 0;
+new bool:disallowPlayers = false;
+new String:originalPassword[255];
+
+new bool:isTF = false;
+
+new Handle:hudText;
+new Handle:sv_password;
+
+public Plugin:myinfo = {
+	name        = "[ANY] Automatic Steam Update",
+	author      = "Dr. McKay",
+	description = "Automatically restarts the server to update via Steam",
+	version     = PLUGIN_VERSION,
+	url         = "http://www.doctormckay.com"
+};
+
+public APLRes:AskPluginLoad2(Handle:myself, bool:late, String:error[], err_max) {
+	MarkNativeAsOptional("Updater_AddPlugin"); 
+	return APLRes_Success;
+} 
+
+public OnPluginStart() {
+	AutoExecConfig(true, "plugin.autosteamupdate");
+	
+	delayCvar = CreateConVar("auto_steam_update_delay", "5", "How long in minutes the server should wait before starting another countdown after being postponed.");
+	timerCvar = CreateConVar("auto_steam_update_timer", "5", "How long in minutes the server should count down before restarting.");
+	messageTimeCvar = CreateConVar("auto_steam_update_message_display_time", "5", "At how much time in minutes left on the timer should the timer be displayed?");
+	lockCvar = CreateConVar("auto_steam_update_lock", "0", "0 - don't lock the server / 1 - set sv_password to auto_steam_update_password during timer / 2 - don't set a password, but kick everyone who tries to connect during the timer");
+	passwordCvar = CreateConVar("auto_steam_update_password", "", "The password to set sv_password to if auto_steam_update_lock = 1", FCVAR_PROTECTED);
+	kickMessageCvar = CreateConVar("auto_steam_update_kickmessage", "The server will shut down soon to acquire Steam updates, so no new connections are allowed", "The message to display to kicked clients if auto_steam_update_lock = 2");
+	shutdownMessageCvar = CreateConVar("auto_steam_update_shutdown_message", "Server shutting down for Steam update", "The message displayed to clients when the server restarts");
+	hudXCvar = CreateConVar("auto_steam_update_hud_text_x_pos", "0.01", "X-position for HUD timer (only on supported games) -1 = center", _, true, -1.0, true, 1.0);
+	hudYCvar = CreateConVar("auto_steam_update_hud_text_y_pos", "0.01", "Y-position for HUD timer (only on supported games) -1 = center", _, true, -1.0, true, 1.0);
+	hudRCvar = CreateConVar("auto_steam_update_hud_text_red", "0", "Amount of red for the HUD timer (only on supported games)", _, true, 0.0, true, 255.0);
+	hudGCvar = CreateConVar("auto_steam_update_hud_text_green", "255", "Amount of red for the HUD timer (only on supported games)", _, true, 0.0, true, 255.0);
+	hudBCvar = CreateConVar("auto_steam_update_hud_text_blue", "0", "Amount of red for the HUD timer (only on supported games)", _, true, 0.0, true, 255.0);
+	updaterCvar = CreateConVar("auto_steam_update_auto_update", "1", "Enables automatic plugin updating (has no effect if Updater is not installed)");
+	
+	sv_password = FindConVar("sv_password");
+	
+	RegAdminCmd("sm_postponeupdate", Command_PostponeUpdate, ADMFLAG_RCON, "Postpone a pending server restart for a Steam update");
+	RegAdminCmd("sm_updatetimer", Command_ForceRestart, ADMFLAG_RCON, "Force the server update timer to start immediately");
+	
+	hudText = CreateHudSynchronizer();
+	if(hudText == INVALID_HANDLE) {
+		LogMessage("HUD text is not supported on this mod. The persistant timer will not display.");
+	} else {
+		LogMessage("HUD text is supported on this mod. The persistant timer will display.");
+	}
+	
+	decl String:folder[16];
+	GetGameFolderName(folder, sizeof(folder));
+	if(StrEqual(folder, "tf", false)) {
+		isTF = true;
+	}
+}
+
+public OnMapStart() {
+	if(isTF) {
+		PrecacheSound(ALERT_SOUND); // this sound is in TF2 only
+	}
+}
+
+public OnClientPostAdminCheck(client) {
+	if(CheckCommandAccess(client, "BypassAutoSteamUpdateDisallow", ADMFLAG_GENERIC, true)) {
+		return;
+	}
+	if(disallowPlayers) {
+		decl String:kickMessage[255];
+		GetConVarString(kickMessageCvar, kickMessage, sizeof(kickMessage));
+		KickClient(client, kickMessage);
+	}
+}
+
+public Action:Steam_RestartRequested() {
+	startTimer();
+	return Plugin_Continue;
+}
+
+public Action:Command_ForceRestart(client, args) {
+	suspendPlugin = false;
+	LogAction(client, -1, "%L manually triggered an update timer", client);
+	startTimer(true);
+	return Plugin_Handled;
+}
+
+startTimer(bool:forced = false) {
+	if(suspendPlugin) {
+		return;
+	}
+	if(!IsServerPopulated()) { // If there's no clients in the server, go ahead and restart it
+		LogMessage("Received a master server restart request, and there are no players in the server. Restarting to update.");
+		ServerCommand("_restart");
+		return;
+	}
+	new lock = GetConVarInt(lockCvar);
+	if(lock == 1) {
+		decl String:password[255];
+		GetConVarString(passwordCvar, password, sizeof(password));
+		GetConVarString(sv_password, originalPassword, sizeof(originalPassword));
+		SetConVarString(sv_password, password);
+	}
+	if(lock == 2) {
+		disallowPlayers = true;
+	}
+	if(!forced) {
+		LogMessage("Received a master server restart request, beginning restart timer.");
+	}
+	timeRemaining = GetConVarInt(timerCvar) * 60;
+	timeRemaining++;
+	restartTimer = CreateTimer(1.0, DoTimer, INVALID_HANDLE, TIMER_REPEAT);
+	suspendPlugin = true;
+	return;
+}
+
+public Action:DoTimer(Handle:timer) {
+	timeRemaining--;
+	if(timeRemaining <= -1) {
+		LogMessage("Restarting server for Steam update.");
+		for(new i = 1; i <= MaxClients; i++) {
+			if (!IsClientAuthorized(i) || !IsClientInGame(i) || IsFakeClient(i)) {
+				continue;
+			}
+			new String:kickMessage[255];
+			GetConVarString(shutdownMessageCvar, kickMessage, sizeof(kickMessage));
+			KickClient(i, kickMessage);
+		}
+		ServerCommand("_restart");
+		return Plugin_Stop;
+	}
+	if(timeRemaining / 60 <= GetConVarInt(messageTimeCvar)) {
+		if(hudText != INVALID_HANDLE) {
+			for(new i = 1; i <= MaxClients; i++) {
+				if(!IsClientConnected(i) || !IsClientInGame(i) || IsFakeClient(i)) {
+					continue;
+				}
+				SetHudTextParams(GetConVarFloat(hudXCvar), GetConVarFloat(hudYCvar), 1.0, GetConVarInt(hudRCvar), GetConVarInt(hudGCvar), GetConVarInt(hudBCvar), 255);
+				ShowSyncHudText(i, hudText, "Update: %i:%02i", timeRemaining / 60, timeRemaining % 60);
+			}
+		}
+		if(timeRemaining > 60 && timeRemaining % 60 == 0) {
+			PrintHintTextToAll("A game update has been released.\nThis server will shut down to update in %i minutes.", timeRemaining / 60);
+			PrintToServer("[SM] A game update has been released. This server will shut down to update in %i minutes.", timeRemaining / 60);
+			if(isTF) {
+				EmitSoundToAll(ALERT_SOUND);
+			}
+		}
+		if(timeRemaining == 60) {
+			PrintHintTextToAll("A game update has been released.\nThis server will shut down to update in 1 minute.");
+			PrintToServer("[SM] A game update has been released. This server will shut down to update in 1 minute.");
+			if(isTF) {
+				EmitSoundToAll(ALERT_SOUND);
+			}
+		}
+	}
+	if(timeRemaining <= 60 && hudText == INVALID_HANDLE) {
+		PrintCenterTextAll("Update: %i:%02i", timeRemaining / 60, timeRemaining % 60);
+	}
+	return Plugin_Continue;
+}
+
+public Action:Command_PostponeUpdate(client, args) {
+	if(restartTimer == INVALID_HANDLE) {
+		ReplyToCommand(client, "[SM] There is no update timer currently running.");
+		return Plugin_Handled;
+	}
+	CloseHandle(restartTimer);
+	restartTimer = INVALID_HANDLE;
+	LogAction(client, -1, "%L aborted the update timer.", client);
+	new Float:delay = GetConVarInt(delayCvar) * 60.0;
+	CreateTimer(delay, ReenablePlugin);
+	ReplyToCommand(client, "[SM] The update timer has been cancelled for %i minutes.", GetConVarInt(delayCvar));
+	PrintHintTextToAll("The update timer has been cancelled for %i minutes.", GetConVarInt(delayCvar));
+	disallowPlayers = false;
+	if(GetConVarInt(lockCvar) == 1) {
+		SetConVarString(sv_password, originalPassword);
+	}
+	return Plugin_Handled;
+}
+
+public Action:ReenablePlugin(Handle:timer) {
+	suspendPlugin = false;
+	return Plugin_Stop;
+}
+
+IsServerPopulated() {
+	for(new i = 1; i <= MaxClients; i++) {
+		if(IsClientConnected(i) && !IsFakeClient(i)) {
+			return true;
+		}
+	}
+	return false;
+}
+
+/////////////////////////////////
+
+public OnAllPluginsLoaded() {
+	new Handle:convar;
+	if(LibraryExists("updater")) {
+		Updater_AddPlugin(UPDATE_URL);
+		new String:newVersion[10];
+		Format(newVersion, sizeof(newVersion), "%sA", PLUGIN_VERSION);
+		convar = CreateConVar("auto_steam_update_version", newVersion, "Automatic Steam Update Version", FCVAR_DONTRECORD|FCVAR_NOTIFY|FCVAR_CHEAT);
+	} else {
+		convar = CreateConVar("auto_steam_update_version", PLUGIN_VERSION, "Automatic Steam Update Version", FCVAR_DONTRECORD|FCVAR_NOTIFY|FCVAR_CHEAT);	
+	}
+	HookConVarChange(convar, Callback_VersionConVarChanged);
+}
+
+public Callback_VersionConVarChanged(Handle:convar, const String:oldValue[], const String:newValue[]) {
+	ResetConVar(convar);
+}
+
+public Action:Updater_OnPluginDownloading() {
+	if(!GetConVarBool(updaterCvar)) {
+		return Plugin_Handled;
+	}
+	return Plugin_Continue;
+}
+
+public OnLibraryAdded(const String:name[]) {
+	if(StrEqual(name, "updater")) {
+		Updater_AddPlugin(UPDATE_URL);
+	}
+}
+
+public Updater_OnPluginUpdated() {
+	ReloadPlugin();
+}