diff --git a/AMBuildScript b/AMBuildScript
new file mode 100644
index 0000000..9855935
--- /dev/null
+++ b/AMBuildScript
@@ -0,0 +1,448 @@
+# vim: set sts=2 ts=8 sw=2 tw=99 et ft=python:
+import os, sys
+
+# Simple extensions do not need to modify this file.
+
+class SDK(object):
+ def __init__(self, sdk, ext, aDef, name, platform, dir):
+ self.folder = 'hl2sdk-' + dir
+ self.envvar = sdk
+ self.ext = ext
+ self.code = aDef
+ self.define = name
+ self.platform = platform
+ self.name = dir
+ self.path = None # Actual path
+
+WinOnly = ['windows']
+WinLinux = ['windows', 'linux']
+WinLinuxMac = ['windows', 'linux', 'mac']
+
+PossibleSDKs = {
+ 'episode1': SDK('HL2SDK', '1.ep1', '1', 'EPISODEONE', WinLinux, 'episode1'),
+ 'ep2': SDK('HL2SDKOB', '2.ep2', '3', 'ORANGEBOX', WinLinux, 'orangebox'),
+ 'css': SDK('HL2SDKCSS', '2.css', '6', 'CSS', WinLinuxMac, 'css'),
+ 'hl2dm': SDK('HL2SDKHL2DM', '2.hl2dm', '7', 'HL2DM', WinLinuxMac, 'hl2dm'),
+ 'dods': SDK('HL2SDKDODS', '2.dods', '8', 'DODS', WinLinuxMac, 'dods'),
+ 'sdk2013': SDK('HL2SDK2013', '2.sdk2013', '9', 'SDK2013', WinLinuxMac, 'sdk2013'),
+ 'tf2': SDK('HL2SDKTF2', '2.tf2', '11', 'TF2', WinLinuxMac, 'tf2'),
+ 'l4d': SDK('HL2SDKL4D', '2.l4d', '12', 'LEFT4DEAD', WinLinuxMac, 'l4d'),
+ 'nucleardawn': SDK('HL2SDKND', '2.nd', '13', 'NUCLEARDAWN', WinLinuxMac, 'nucleardawn'),
+ 'l4d2': SDK('HL2SDKL4D2', '2.l4d2', '15', 'LEFT4DEAD2', WinLinuxMac, 'l4d2'),
+ 'darkm': SDK('HL2SDK-DARKM', '2.darkm', '2', 'DARKMESSIAH', WinOnly, 'darkm'),
+ 'swarm': SDK('HL2SDK-SWARM', '2.swarm', '16', 'ALIENSWARM', WinOnly, 'swarm'),
+ 'bgt': SDK('HL2SDK-BGT', '2.bgt', '4', 'BLOODYGOODTIME', WinOnly, 'bgt'),
+ 'eye': SDK('HL2SDK-EYE', '2.eye', '5', 'EYE', WinOnly, 'eye'),
+ 'csgo': SDK('HL2SDKCSGO', '2.csgo', '20', 'CSGO', WinLinuxMac, 'csgo'),
+ 'dota': SDK('HL2SDKDOTA', '2.dota', '21', 'DOTA', [], 'dota'),
+ 'portal2': SDK('HL2SDKPORTAL2', '2.portal2', '17', 'PORTAL2', [], 'portal2'),
+ 'blade': SDK('HL2SDKBLADE', '2.blade', '18', 'BLADE', WinLinux, 'blade'),
+ 'insurgency': SDK('HL2SDKINSURGENCY', '2.insurgency', '19', 'INSURGENCY', WinLinuxMac, 'insurgency'),
+ 'contagion': SDK('HL2SDKCONTAGION', '2.contagion', '14', 'CONTAGION', WinOnly, 'contagion'),
+ 'bms': SDK('HL2SDKBMS', '2.bms', '10', 'BMS', WinLinux, 'bms'),
+}
+
+def ResolveEnvPath(env, folder):
+ if env in os.environ:
+ path = os.environ[env]
+ if os.path.isdir(path):
+ return path
+ return None
+
+ head = os.getcwd()
+ oldhead = None
+ while head != None and head != oldhead:
+ path = os.path.join(head, folder)
+ if os.path.isdir(path):
+ return path
+ oldhead = head
+ head, tail = os.path.split(head)
+
+ return None
+
+def Normalize(path):
+ return os.path.abspath(os.path.normpath(path))
+
+class ExtensionConfig(object):
+ def __init__(self):
+ self.sdks = {}
+ self.binaries = []
+ self.extensions = []
+ self.generated_headers = None
+ self.mms_root = None
+ self.sm_root = None
+
+ @property
+ def tag(self):
+ if builder.options.debug == '1':
+ return 'Debug'
+ return 'Release'
+
+ def detectSDKs(self):
+ sdk_list = builder.options.sdks.split(',')
+ use_all = sdk_list[0] == 'all'
+ use_present = sdk_list[0] == 'present'
+
+ for sdk_name in PossibleSDKs:
+ sdk = PossibleSDKs[sdk_name]
+ if builder.target_platform in sdk.platform:
+ if builder.options.hl2sdk_root:
+ sdk_path = os.path.join(builder.options.hl2sdk_root, sdk.folder)
+ else:
+ sdk_path = ResolveEnvPath(sdk.envvar, sdk.folder)
+ if sdk_path is None or not os.path.isdir(sdk_path):
+ if use_all or sdk_name in sdk_list:
+ raise Exception('Could not find a valid path for {0}'.format(sdk.envvar))
+ continue
+ if use_all or use_present or sdk_name in sdk_list:
+ sdk.path = Normalize(sdk_path)
+ self.sdks[sdk_name] = sdk
+
+ if len(self.sdks) < 1:
+ raise Exception('At least one SDK must be available.')
+
+ if builder.options.sm_path:
+ self.sm_root = builder.options.sm_path
+ else:
+ self.sm_root = ResolveEnvPath('SOURCEMOD18', 'sourcemod-1.8')
+ if not self.sm_root:
+ self.sm_root = ResolveEnvPath('SOURCEMOD', 'sourcemod')
+ if not self.sm_root:
+ self.sm_root = ResolveEnvPath('SOURCEMOD_DEV', 'sourcemod-central')
+
+ if not self.sm_root or not os.path.isdir(self.sm_root):
+ raise Exception('Could not find a source copy of SourceMod')
+ self.sm_root = Normalize(self.sm_root)
+
+ if builder.options.mms_path:
+ self.mms_root = builder.options.mms_path
+ else:
+ self.mms_root = ResolveEnvPath('MMSOURCE110', 'mmsource-1.10')
+ if not self.mms_root:
+ self.mms_root = ResolveEnvPath('MMSOURCE', 'metamod-source')
+ if not self.mms_root:
+ self.mms_root = ResolveEnvPath('MMSOURCE_DEV', 'mmsource-central')
+
+ if not self.mms_root or not os.path.isdir(self.mms_root):
+ raise Exception('Could not find a source copy of Metamod:Source')
+ self.mms_root = Normalize(self.mms_root)
+
+ def configure(self):
+ cxx = builder.DetectCompilers()
+
+ if cxx.like('gcc'):
+ self.configure_gcc(cxx)
+ elif cxx.vendor == 'msvc':
+ self.configure_msvc(cxx)
+
+ # Optimizaiton
+ if builder.options.opt == '1':
+ cxx.defines += ['NDEBUG']
+
+ # Debugging
+ if builder.options.debug == '1':
+ cxx.defines += ['DEBUG', '_DEBUG']
+
+ # Platform-specifics
+ if builder.target_platform == 'linux':
+ self.configure_linux(cxx)
+ elif builder.target_platform == 'mac':
+ self.configure_mac(cxx)
+ elif builder.target_platform == 'windows':
+ self.configure_windows(cxx)
+
+ # Finish up.
+ cxx.includes += [
+ os.path.join(self.sm_root, 'public'),
+ ]
+
+ def configure_gcc(self, cxx):
+ cxx.defines += [
+ 'stricmp=strcasecmp',
+ '_stricmp=strcasecmp',
+ '_snprintf=snprintf',
+ '_vsnprintf=vsnprintf',
+ 'HAVE_STDINT_H',
+ 'GNUC',
+ ]
+ cxx.cflags += [
+ '-pipe',
+ '-fno-strict-aliasing',
+ '-Wall',
+ '-Werror',
+ '-Wno-unused',
+ '-Wno-switch',
+ '-Wno-array-bounds',
+ '-msse',
+ '-m32',
+ '-fvisibility=hidden',
+ ]
+ cxx.cxxflags += [
+ '-std=c++11',
+ '-fno-exceptions',
+ '-fno-threadsafe-statics',
+ '-Wno-non-virtual-dtor',
+ '-Wno-overloaded-virtual',
+ '-fvisibility-inlines-hidden',
+ ]
+ cxx.linkflags += ['-m32']
+
+ have_gcc = cxx.vendor == 'gcc'
+ have_clang = cxx.vendor == 'clang'
+ if cxx.version >= 'clang-3.6':
+ cxx.cxxflags += ['-Wno-inconsistent-missing-override']
+ if have_clang or (cxx.version >= 'gcc-4.6'):
+ cxx.cflags += ['-Wno-narrowing']
+ if have_clang or (cxx.version >= 'gcc-4.7'):
+ cxx.cxxflags += ['-Wno-delete-non-virtual-dtor']
+ if cxx.version >= 'gcc-4.8':
+ cxx.cflags += ['-Wno-unused-result']
+
+ if have_clang:
+ cxx.cxxflags += ['-Wno-implicit-exception-spec-mismatch']
+ if cxx.version >= 'apple-clang-5.1' or cxx.version >= 'clang-3.4':
+ cxx.cxxflags += ['-Wno-deprecated-register']
+ else:
+ cxx.cxxflags += ['-Wno-deprecated']
+ cxx.cflags += ['-Wno-sometimes-uninitialized']
+
+ if have_gcc:
+ cxx.cflags += ['-mfpmath=sse']
+
+ if builder.options.opt == '1':
+ cxx.cflags += ['-O3']
+
+ def configure_msvc(self, cxx):
+ if builder.options.debug == '1':
+ cxx.cflags += ['/MTd']
+ cxx.linkflags += ['/NODEFAULTLIB:libcmt']
+ else:
+ cxx.cflags += ['/MT']
+ cxx.defines += [
+ '_CRT_SECURE_NO_DEPRECATE',
+ '_CRT_SECURE_NO_WARNINGS',
+ '_CRT_NONSTDC_NO_DEPRECATE',
+ '_ITERATOR_DEBUG_LEVEL=0',
+ ]
+ cxx.cflags += [
+ '/W3',
+ ]
+ cxx.cxxflags += [
+ '/EHsc',
+ '/GR-',
+ '/TP',
+ ]
+ cxx.linkflags += [
+ '/MACHINE:X86',
+ 'kernel32.lib',
+ 'user32.lib',
+ 'gdi32.lib',
+ 'winspool.lib',
+ 'comdlg32.lib',
+ 'advapi32.lib',
+ 'shell32.lib',
+ 'ole32.lib',
+ 'oleaut32.lib',
+ 'uuid.lib',
+ 'odbc32.lib',
+ 'odbccp32.lib',
+ ]
+
+ if builder.options.opt == '1':
+ cxx.cflags += ['/Ox', '/Zo']
+ cxx.linkflags += ['/OPT:ICF', '/OPT:REF']
+
+ if builder.options.debug == '1':
+ cxx.cflags += ['/Od', '/RTC1']
+
+ # This needs to be after our optimization flags which could otherwise disable it.
+ # Don't omit the frame pointer.
+ cxx.cflags += ['/Oy-']
+
+ def configure_linux(self, cxx):
+ cxx.defines += ['_LINUX', 'POSIX']
+ cxx.linkflags += ['-Wl,--exclude-libs,ALL', '-lm']
+ if cxx.vendor == 'gcc':
+ cxx.linkflags += ['-static-libgcc']
+ elif cxx.vendor == 'clang':
+ cxx.linkflags += ['-lgcc_eh']
+
+ def configure_mac(self, cxx):
+ cxx.defines += ['OSX', '_OSX', 'POSIX']
+ cxx.cflags += ['-mmacosx-version-min=10.5']
+ cxx.linkflags += [
+ '-mmacosx-version-min=10.5',
+ '-arch', 'i386',
+ '-lstdc++',
+ '-stdlib=libstdc++',
+ ]
+ cxx.cxxflags += ['-stdlib=libstdc++']
+
+ def configure_windows(self, cxx):
+ cxx.defines += ['WIN32', '_WINDOWS']
+
+ def ConfigureForExtension(self, context, compiler):
+ compiler.cxxincludes += [
+ os.path.join(context.currentSourcePath),
+ os.path.join(context.currentSourcePath, 'sdk'),
+ os.path.join(self.sm_root, 'public'),
+ os.path.join(self.sm_root, 'public', 'extensions'),
+ os.path.join(self.sm_root, 'sourcepawn', 'include'),
+ os.path.join(self.sm_root, 'public', 'amtl', 'amtl'),
+ os.path.join(self.sm_root, 'public', 'amtl'),
+ ]
+ return compiler
+
+ def ConfigureForHL2(self, binary, sdk):
+ compiler = binary.compiler
+
+ if sdk.name == 'episode1':
+ mms_path = os.path.join(self.mms_root, 'core-legacy')
+ else:
+ mms_path = os.path.join(self.mms_root, 'core')
+
+ compiler.cxxincludes += [
+ os.path.join(mms_path),
+ os.path.join(mms_path, 'sourcehook'),
+ ]
+
+ defines = ['SE_' + PossibleSDKs[i].define + '=' + PossibleSDKs[i].code for i in PossibleSDKs]
+ compiler.defines += defines
+
+ paths = [
+ ['public'],
+ ['public', 'engine'],
+ ['public', 'mathlib'],
+ ['public', 'vstdlib'],
+ ['public', 'tier0'],
+ ['public', 'tier1']
+ ]
+ if sdk.name == 'episode1' or sdk.name == 'darkm':
+ paths.append(['public', 'dlls'])
+ paths.append(['game_shared'])
+ else:
+ paths.append(['public', 'game', 'server'])
+ paths.append(['public', 'toolframework'])
+ paths.append(['game', 'shared'])
+ paths.append(['common'])
+
+ compiler.defines += ['SOURCE_ENGINE=' + sdk.code]
+
+ if sdk.name in ['sdk2013', 'bms'] and compiler.like('gcc'):
+ # The 2013 SDK already has these in public/tier0/basetypes.h
+ compiler.defines.remove('stricmp=strcasecmp')
+ compiler.defines.remove('_stricmp=strcasecmp')
+ compiler.defines.remove('_snprintf=snprintf')
+ compiler.defines.remove('_vsnprintf=vsnprintf')
+
+ if compiler.like('msvc'):
+ compiler.defines += ['COMPILER_MSVC', 'COMPILER_MSVC32']
+ else:
+ compiler.defines += ['COMPILER_GCC']
+
+ # For everything after Swarm, this needs to be defined for entity networking
+ # to work properly with sendprop value changes.
+ if sdk.name in ['blade', 'insurgency', 'csgo', 'dota']:
+ compiler.defines += ['NETWORK_VARS_ENABLED']
+
+ if sdk.name in ['css', 'hl2dm', 'dods', 'sdk2013', 'bms', 'tf2', 'l4d', 'nucleardawn', 'l4d2', 'dota']:
+ if builder.target_platform in ['linux', 'mac']:
+ compiler.defines += ['NO_HOOK_MALLOC', 'NO_MALLOC_OVERRIDE']
+
+ if sdk.name == 'csgo' and builder.target_platform == 'linux':
+ compiler.linkflags += ['-lstdc++']
+
+ for path in paths:
+ compiler.cxxincludes += [os.path.join(sdk.path, *path)]
+
+ if builder.target_platform == 'linux':
+ if sdk.name == 'episode1':
+ lib_folder = os.path.join(sdk.path, 'linux_sdk')
+ elif sdk.name in ['sdk2013', 'bms']:
+ lib_folder = os.path.join(sdk.path, 'lib', 'public', 'linux32')
+ else:
+ lib_folder = os.path.join(sdk.path, 'lib', 'linux')
+ elif builder.target_platform == 'mac':
+ if sdk.name in ['sdk2013', 'bms']:
+ lib_folder = os.path.join(sdk.path, 'lib', 'public', 'osx32')
+ else:
+ lib_folder = os.path.join(sdk.path, 'lib', 'mac')
+
+ if builder.target_platform in ['linux', 'mac']:
+ if sdk.name in ['sdk2013', 'bms']:
+ compiler.postlink += [
+ compiler.Dep(os.path.join(lib_folder, 'tier1.a')),
+ compiler.Dep(os.path.join(lib_folder, 'mathlib.a'))
+ ]
+ else:
+ compiler.postlink += [
+ compiler.Dep(os.path.join(lib_folder, 'tier1_i486.a')),
+ compiler.Dep(os.path.join(lib_folder, 'mathlib_i486.a'))
+ ]
+
+ if sdk.name in ['blade', 'insurgency', 'csgo', 'dota']:
+ compiler.postlink += [compiler.Dep(os.path.join(lib_folder, 'interfaces_i486.a'))]
+
+ dynamic_libs = []
+ if builder.target_platform == 'linux':
+ if sdk.name in ['css', 'hl2dm', 'dods', 'tf2', 'sdk2013', 'bms', 'nucleardawn', 'l4d2', 'insurgency']:
+ dynamic_libs = ['libtier0_srv.so', 'libvstdlib_srv.so']
+ elif sdk.name in ['l4d', 'blade', 'insurgency', 'csgo', 'dota']:
+ dynamic_libs = ['libtier0.so', 'libvstdlib.so']
+ else:
+ dynamic_libs = ['tier0_i486.so', 'vstdlib_i486.so']
+ elif builder.target_platform == 'mac':
+ compiler.linkflags.append('-liconv')
+ dynamic_libs = ['libtier0.dylib', 'libvstdlib.dylib']
+ elif builder.target_platform == 'windows':
+ libs = ['tier0', 'tier1', 'vstdlib', 'mathlib']
+ if sdk.name in ['swarm', 'blade', 'insurgency', 'csgo', 'dota']:
+ libs.append('interfaces')
+ for lib in libs:
+ lib_path = os.path.join(sdk.path, 'lib', 'public', lib) + '.lib'
+ compiler.linkflags.append(compiler.Dep(lib_path))
+
+ for library in dynamic_libs:
+ source_path = os.path.join(lib_folder, library)
+ output_path = os.path.join(binary.localFolder, library)
+
+ def make_linker(source_path, output_path):
+ def link(context, binary):
+ cmd_node, (output,) = context.AddSymlink(source_path, output_path)
+ return output
+ return link
+
+ linker = make_linker(source_path, output_path)
+ compiler.linkflags[0:0] = [compiler.Dep(library, linker)]
+
+ return binary
+
+ def HL2Library(self, context, name, sdk):
+ binary = context.compiler.Library(name)
+ self.ConfigureForExtension(context, binary.compiler)
+ return self.ConfigureForHL2(binary, sdk)
+
+ def HL2Project(self, context, name):
+ project = context.compiler.LibraryProject(name)
+ self.ConfigureForExtension(context, project.compiler)
+ return project
+
+ def HL2Config(self, project, name, sdk):
+ binary = project.Configure(name, '{0} - {1}'.format(self.tag, sdk.name))
+ return self.ConfigureForHL2(binary, sdk)
+
+Extension = ExtensionConfig()
+Extension.detectSDKs()
+Extension.configure()
+
+# Add additional buildscripts here
+BuildScripts = [
+ 'AMBuilder',
+]
+
+if builder.backend == 'amb2':
+ BuildScripts += [
+ 'PackageScript',
+ ]
+
+builder.RunBuildScripts(BuildScripts, { 'Extension': Extension})
diff --git a/AMBuilder b/AMBuilder
new file mode 100644
index 0000000..0fa4b7d
--- /dev/null
+++ b/AMBuilder
@@ -0,0 +1,35 @@
+# vim: set sts=2 ts=8 sw=2 tw=99 et ft=python:
+import os, sys
+
+projectName = 'sourcetvmanager'
+
+# smsdk_ext.cpp will be automatically added later
+sourceFiles = [
+ 'extension.cpp',
+ 'natives.cpp',
+ 'forwards.cpp'
+]
+
+###############
+# Make sure to edit PackageScript, which copies your files to their appropriate locations
+# Simple extensions do not need to modify past this point.
+
+project = Extension.HL2Project(builder, projectName + '.ext')
+
+if os.path.isfile(os.path.join(builder.currentSourcePath, 'sdk', 'smsdk_ext.cpp')):
+ # Use the copy included in the project
+ project.sources += [os.path.join('sdk', 'smsdk_ext.cpp')]
+else:
+ # Use the copy included with SM 1.6 and newer
+ project.sources += [os.path.join(Extension.sm_root, 'public', 'smsdk_ext.cpp')]
+
+project.sources += sourceFiles
+
+for sdk_name in ['csgo']:
+ if sdk_name not in Extension.sdks:
+ continue
+ sdk = Extension.sdks[sdk_name]
+
+ binary = Extension.HL2Config(project, projectName + '.ext.' + sdk.ext, sdk)
+
+Extension.extensions = builder.Add(project)
diff --git a/PackageScript b/PackageScript
new file mode 100644
index 0000000..f768acb
--- /dev/null
+++ b/PackageScript
@@ -0,0 +1,50 @@
+# vim: set ts=8 sts=2 sw=2 tw=99 et ft=python:
+import os
+
+# This is where the files will be output to
+# package is the default
+builder.SetBuildFolder('package')
+
+# Add any folders you need to this list
+folder_list = [
+ 'addons/sourcemod/extensions',
+ 'addons/sourcemod/scripting/include',
+ 'addons/sourcemod/gamedata',
+ #'addons/sourcemod/configs',
+]
+
+# Create the distribution folder hierarchy.
+folder_map = {}
+for folder in folder_list:
+ norm_folder = os.path.normpath(folder)
+ folder_map[folder] = builder.AddFolder(norm_folder)
+
+# Do all straight-up file copies from the source tree.
+def CopyFiles(src, dest, files):
+ if not dest:
+ dest = src
+ dest_entry = folder_map[dest]
+ for source_file in files:
+ source_path = os.path.join(builder.sourcePath, src, source_file)
+ builder.AddCopy(source_path, dest_entry)
+
+# Include files
+CopyFiles('', 'addons/sourcemod/scripting/include',
+ [ 'sourcetvmanager.inc', ]
+)
+
+# GameData files
+CopyFiles('', 'addons/sourcemod/gamedata',
+ [ 'sourcetvmanager.games.txt' ]
+)
+
+# Config Files
+#CopyFiles('configs', 'addons/sourcemod/configs',
+# [ 'configfile.cfg',
+# 'otherconfig.cfg,
+# ]
+#)
+
+# Copy binaries.
+for cxx_task in Extension.extensions:
+ builder.AddCopy(cxx_task.binary, folder_map['addons/sourcemod/extensions'])
diff --git a/configure.py b/configure.py
new file mode 100644
index 0000000..57910e8
--- /dev/null
+++ b/configure.py
@@ -0,0 +1,23 @@
+# vim: set sts=2 ts=8 sw=2 tw=99 et:
+import sys
+from ambuild2 import run
+
+# Simple extensions do not need to modify this file.
+
+builder = run.PrepareBuild(sourcePath = sys.path[0])
+
+builder.options.add_option('--hl2sdk-root', type=str, dest='hl2sdk_root', default=None,
+ help='Root search folder for HL2SDKs')
+builder.options.add_option('--mms-path', type=str, dest='mms_path', default=None,
+ help='Path to Metamod:Source')
+builder.options.add_option('--sm-path', type=str, dest='sm_path', default=None,
+ help='Path to SourceMod')
+builder.options.add_option('--enable-debug', action='store_const', const='1', dest='debug',
+ help='Enable debugging symbols')
+builder.options.add_option('--enable-optimize', action='store_const', const='1', dest='opt',
+ help='Enable optimization')
+builder.options.add_option('-s', '--sdks', default='all', dest='sdks',
+ help='Build against specified SDKs; valid args are "all", "present", or '
+ 'comma-delimited list of engine names (default: %default)')
+
+builder.Configure()
diff --git a/extension.cpp b/extension.cpp
new file mode 100644
index 0000000..bc02a78
--- /dev/null
+++ b/extension.cpp
@@ -0,0 +1,257 @@
+/**
+ * vim: set ts=4 :
+ * =============================================================================
+ * SourceMod SourceTV Manager Extension
+ * Copyright (C) 2004-2016 AlliedModders LLC. All rights reserved.
+ * =============================================================================
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, version 3.0, as published by the
+ * Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see .
+ *
+ * As a special exception, AlliedModders LLC gives you permission to link the
+ * code of this program (as well as its derivative works) to "Half-Life 2," the
+ * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software
+ * by the Valve Corporation. You must obey the GNU General Public License in
+ * all respects for all other code used. Additionally, AlliedModders LLC grants
+ * this exception to all derivative works. AlliedModders LLC defines further
+ * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
+ * or .
+ *
+ * Version: $Id$
+ */
+
+#include "extension.h"
+#include "forwards.h"
+
+IHLTVDirector *hltvdirector = nullptr;
+IHLTVServer *hltvserver = nullptr;
+IDemoRecorder *demorecorder = nullptr;
+void *host_client = nullptr;
+void *old_host_client = nullptr;
+bool g_HostClientOverridden = false;
+
+IGameEventManager2 *gameevents = nullptr;
+
+IBinTools *bintools = nullptr;
+ISDKTools *sdktools = nullptr;
+IServer *iserver = nullptr;
+IGameConfig *g_pGameConf = nullptr;
+
+SH_DECL_HOOK1_void(IHLTVDirector, AddHLTVServer, SH_NOATTRIB, 0, IHLTVServer *);
+SH_DECL_HOOK1_void(IHLTVDirector, RemoveHLTVServer, SH_NOATTRIB, 0, IHLTVServer *);
+
+SH_DECL_HOOK1(IClient, ExecuteStringCommand, SH_NOATTRIB, 0, bool, const char *);
+
+/**
+ * @file extension.cpp
+ * @brief Implement extension code here.
+ */
+
+SourceTVManager g_STVManager; /**< Global singleton for extension's main interface */
+
+SMEXT_LINK(&g_STVManager);
+
+extern const sp_nativeinfo_t sourcetv_natives[];
+
+bool SourceTVManager::SDK_OnLoad(char *error, size_t maxlength, bool late)
+{
+ sharesys->AddDependency(myself, "bintools.ext", true, true);
+ sharesys->AddDependency(myself, "sdktools.ext", true, true);
+
+ char conf_error[255];
+ if (!gameconfs->LoadGameConfigFile("sourcetvmanager.games", &g_pGameConf, conf_error, sizeof(conf_error)))
+ {
+ if (error)
+ {
+ snprintf(error, maxlength, "Could not read sourcetvmanager.games: %s", conf_error);
+ }
+ return false;
+ }
+
+ // Get the host_client pointer
+ // This is used to fix a null pointer crash when executing fake commands on bots.
+ if (!g_pGameConf->GetAddress("host_client", &host_client) || !host_client)
+ {
+ smutils->LogError(myself, "Failed to find host_client pointer. Server might crash when executing commands on SourceTV bot.");
+ }
+
+ sharesys->AddNatives(myself, sourcetv_natives);
+ sharesys->RegisterLibrary(myself, "sourcetvmanager");
+
+ return true;
+}
+
+void SourceTVManager::SDK_OnAllLoaded()
+{
+ SH_ADD_HOOK(IHLTVDirector, AddHLTVServer, hltvdirector, SH_MEMBER(this, &SourceTVManager::OnAddHLTVServer_Post), true);
+ SH_ADD_HOOK(IHLTVDirector, RemoveHLTVServer, hltvdirector, SH_MEMBER(this, &SourceTVManager::OnRemoveHLTVServer_Post), true);
+
+ SM_GET_LATE_IFACE(BINTOOLS, bintools);
+ SM_GET_LATE_IFACE(SDKTOOLS, sdktools);
+
+ g_pSTVForwards.Init();
+
+ iserver = sdktools->GetIServer();
+ if (!iserver)
+ smutils->LogError(myself, "Failed to get IServer interface from SDKTools. Some functions won't work.");
+
+ if (hltvdirector->GetHLTVServerCount() > 0)
+ SelectSourceTVServer(hltvdirector->GetHLTVServer(0));
+
+ // Hook all the exisiting servers.
+ for (int i = 0; i < hltvdirector->GetHLTVServerCount(); i++)
+ {
+ g_pSTVForwards.HookRecorder(GetDemoRecorderPtr(hltvdirector->GetHLTVServer(i)));
+ }
+}
+
+bool SourceTVManager::SDK_OnMetamodLoad(ISmmAPI *ismm, char *error, size_t maxlen, bool late)
+{
+ GET_V_IFACE_CURRENT(GetServerFactory, hltvdirector, IHLTVDirector, INTERFACEVERSION_HLTVDIRECTOR);
+ GET_V_IFACE_CURRENT(GetEngineFactory, gameevents, IGameEventManager2, INTERFACEVERSION_GAMEEVENTSMANAGER2);
+
+ return true;
+}
+
+void SourceTVManager::SDK_OnUnload()
+{
+ SH_REMOVE_HOOK(IHLTVDirector, AddHLTVServer, hltvdirector, SH_MEMBER(this, &SourceTVManager::OnAddHLTVServer_Post), true);
+ SH_REMOVE_HOOK(IHLTVDirector, RemoveHLTVServer, hltvdirector, SH_MEMBER(this, &SourceTVManager::OnRemoveHLTVServer_Post), true);
+
+ gameconfs->CloseGameConfigFile(g_pGameConf);
+
+ // Unhook all the existing servers.
+ for (int i = 0; i < hltvdirector->GetHLTVServerCount(); i++)
+ {
+ g_pSTVForwards.UnhookRecorder(GetDemoRecorderPtr(hltvdirector->GetHLTVServer(i)));
+ }
+ g_pSTVForwards.Shutdown();
+}
+
+bool SourceTVManager::QueryRunning(char *error, size_t maxlength)
+{
+ SM_CHECK_IFACE(BINTOOLS, bintools);
+ SM_CHECK_IFACE(SDKTOOLS, sdktools);
+
+ return true;
+}
+
+void SourceTVManager::SelectSourceTVServer(IHLTVServer *hltv)
+{
+ // Need to unhook the old server first?
+ if (hltvserver != nullptr && iserver != nullptr)
+ {
+ IClient *pClient = iserver->GetClient(hltvserver->GetHLTVSlot());
+ if (pClient)
+ {
+ SH_REMOVE_HOOK(IClient, ExecuteStringCommand, pClient, SH_MEMBER(this, &SourceTVManager::OnHLTVBotExecuteStringCommand), false);
+ SH_REMOVE_HOOK(IClient, ExecuteStringCommand, pClient, SH_MEMBER(this, &SourceTVManager::OnHLTVBotExecuteStringCommand_Post), true);
+ }
+ }
+
+ // Select the new server.
+ hltvserver = hltv;
+
+ UpdateDemoRecorderPointer();
+
+ if (!hltvserver)
+ return;
+
+ if (!iserver)
+ return;
+ IClient *pClient = iserver->GetClient(hltvserver->GetHLTVSlot());
+ if (!pClient)
+ return;
+
+ SH_ADD_HOOK(IClient, ExecuteStringCommand, pClient, SH_MEMBER(this, &SourceTVManager::OnHLTVBotExecuteStringCommand), false);
+ SH_ADD_HOOK(IClient, ExecuteStringCommand, pClient, SH_MEMBER(this, &SourceTVManager::OnHLTVBotExecuteStringCommand_Post), true);
+}
+
+IDemoRecorder *SourceTVManager::GetDemoRecorderPtr(IHLTVServer *hltvserver)
+{
+ static int offset = -1;
+ if (offset == -1 && !g_pGameConf->GetOffset("CHLTVServer::m_DemoRecorder", &offset))
+ {
+ smutils->LogError(myself, "Failed to get CHLTVServer::m_DemoRecorder offset.");
+ return nullptr;
+ }
+
+ if (hltvserver)
+ return (IDemoRecorder *)((intptr_t)hltvserver + offset);
+ else
+ return nullptr;
+}
+
+void SourceTVManager::UpdateDemoRecorderPointer()
+{
+ demorecorder = GetDemoRecorderPtr(hltvserver);
+}
+
+void SourceTVManager::OnAddHLTVServer_Post(IHLTVServer *hltv)
+{
+ g_pSTVForwards.HookRecorder(GetDemoRecorderPtr(hltv));
+
+ // We already selected some SourceTV server. Keep it.
+ if (hltvserver != nullptr)
+ RETURN_META(MRES_IGNORED);
+
+ // This is the first SourceTV server to be added. Hook it.
+ SelectSourceTVServer(hltv);
+}
+
+void SourceTVManager::OnRemoveHLTVServer_Post(IHLTVServer *hltv)
+{
+ g_pSTVForwards.UnhookRecorder(GetDemoRecorderPtr(hltv));
+
+ // We got this SourceTV server selected. Now it's gone :(
+ if (hltvserver == hltv)
+ {
+ // Is there another one available? Try to keep us operable.
+ if (hltvdirector->GetHLTVServerCount() > 0)
+ SelectSourceTVServer(hltvdirector->GetHLTVServer(0));
+ else
+ SelectSourceTVServer(nullptr);
+ }
+}
+
+bool SourceTVManager::OnHLTVBotExecuteStringCommand(const char *s)
+{
+ if (!hltvserver || !iserver || !host_client)
+ RETURN_META_VALUE(MRES_IGNORED, 0);
+
+ if (*(void **)host_client)
+ RETURN_META_VALUE(MRES_IGNORED, 0);
+
+ IClient *pClient = iserver->GetClient(hltvserver->GetHLTVSlot());
+ if (!pClient)
+ RETURN_META_VALUE(MRES_IGNORED, 0);
+
+ // The IClient vtable is +4 from the CBaseClient vtable due to multiple inheritance.
+ void *pGameClient = (void *)((intptr_t)pClient - 4);
+
+ old_host_client = *(void **)host_client;
+ *(void **)host_client = pGameClient;
+ g_HostClientOverridden = true;
+
+ RETURN_META_VALUE(MRES_IGNORED, 0);
+}
+
+bool SourceTVManager::OnHLTVBotExecuteStringCommand_Post(const char *s)
+{
+ if (!host_client || !g_HostClientOverridden)
+ RETURN_META_VALUE(MRES_IGNORED, 0);
+
+ *(void **)host_client = old_host_client;
+ g_HostClientOverridden = false;
+ RETURN_META_VALUE(MRES_IGNORED, 0);
+}
+
diff --git a/extension.h b/extension.h
new file mode 100644
index 0000000..fe4689b
--- /dev/null
+++ b/extension.h
@@ -0,0 +1,152 @@
+/**
+ * vim: set ts=4 :
+ * =============================================================================
+ * SourceMod Sample Extension
+ * Copyright (C) 2004-2008 AlliedModders LLC. All rights reserved.
+ * =============================================================================
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, version 3.0, as published by the
+ * Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see .
+ *
+ * As a special exception, AlliedModders LLC gives you permission to link the
+ * code of this program (as well as its derivative works) to "Half-Life 2," the
+ * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software
+ * by the Valve Corporation. You must obey the GNU General Public License in
+ * all respects for all other code used. Additionally, AlliedModders LLC grants
+ * this exception to all derivative works. AlliedModders LLC defines further
+ * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
+ * or .
+ *
+ * Version: $Id$
+ */
+
+#ifndef _INCLUDE_SOURCEMOD_EXTENSION_PROPER_H_
+#define _INCLUDE_SOURCEMOD_EXTENSION_PROPER_H_
+
+/**
+ * @file extension.h
+ * @brief Sample extension code header.
+ */
+
+#include "smsdk_ext.h"
+#include
+#include
+#include "ihltvdirector.h"
+#include "ihltv.h"
+#include "iserver.h"
+#include "ihltvdemorecorder.h"
+#include "igameevents.h"
+
+/**
+ * @brief Sample implementation of the SDK Extension.
+ * Note: Uncomment one of the pre-defined virtual functions in order to use it.
+ */
+class SourceTVManager : public SDKExtension
+{
+public:
+ /**
+ * @brief This is called after the initial loading sequence has been processed.
+ *
+ * @param error Error message buffer.
+ * @param maxlength Size of error message buffer.
+ * @param late Whether or not the module was loaded after map load.
+ * @return True to succeed loading, false to fail.
+ */
+ virtual bool SDK_OnLoad(char *error, size_t maxlength, bool late);
+
+ /**
+ * @brief This is called right before the extension is unloaded.
+ */
+ virtual void SDK_OnUnload();
+
+ /**
+ * @brief This is called once all known extensions have been loaded.
+ * Note: It is is a good idea to add natives here, if any are provided.
+ */
+ virtual void SDK_OnAllLoaded();
+
+ /**
+ * @brief Called when the pause state is changed.
+ */
+ //virtual void SDK_OnPauseChange(bool paused);
+
+ /**
+ * @brief this is called when Core wants to know if your extension is working.
+ *
+ * @param error Error message buffer.
+ * @param maxlength Size of error message buffer.
+ * @return True if working, false otherwise.
+ */
+ virtual bool QueryRunning(char *error, size_t maxlength);
+public:
+#if defined SMEXT_CONF_METAMOD
+ /**
+ * @brief Called when Metamod is attached, before the extension version is called.
+ *
+ * @param error Error buffer.
+ * @param maxlength Maximum size of error buffer.
+ * @param late Whether or not Metamod considers this a late load.
+ * @return True to succeed, false to fail.
+ */
+ virtual bool SDK_OnMetamodLoad(ISmmAPI *ismm, char *error, size_t maxlength, bool late);
+
+ /**
+ * @brief Called when Metamod is detaching, after the extension version is called.
+ * NOTE: By default this is blocked unless sent from SourceMod.
+ *
+ * @param error Error buffer.
+ * @param maxlength Maximum size of error buffer.
+ * @return True to succeed, false to fail.
+ */
+ //virtual bool SDK_OnMetamodUnload(char *error, size_t maxlength);
+
+ /**
+ * @brief Called when Metamod's pause state is changing.
+ * NOTE: By default this is blocked unless sent from SourceMod.
+ *
+ * @param paused Pause state being set.
+ * @param error Error buffer.
+ * @param maxlength Maximum size of error buffer.
+ * @return True to succeed, false to fail.
+ */
+ //virtual bool SDK_OnMetamodPauseChange(bool paused, char *error, size_t maxlength);
+#endif
+
+public:
+ void SelectSourceTVServer(IHLTVServer *hltv);
+ IDemoRecorder *GetDemoRecorderPtr(IHLTVServer *hltvserver);
+
+private:
+ void OnAddHLTVServer_Post(IHLTVServer *hltv);
+ void OnRemoveHLTVServer_Post(IHLTVServer *hltv);
+ bool OnHLTVBotSendNetMsg_Post(INetMessage &msg, bool bForceReliable, bool bVoice);
+ bool OnHLTVBotExecuteStringCommand(const char *s);
+ bool OnHLTVBotExecuteStringCommand_Post(const char *s);
+
+private:
+ void UpdateDemoRecorderPointer();
+};
+
+/* Interfaces from SourceMod */
+extern SourceTVManager g_STVManager;
+extern IBinTools *bintools;
+extern ISDKTools *sdktools;
+extern IGameConfig *g_pGameConf;
+
+extern IServer *iserver;
+extern IGameEventManager2 *gameevents;
+
+extern IHLTVDirector *hltvdirector;
+extern IHLTVServer *hltvserver;
+extern IDemoRecorder *demorecorder;
+
+#endif // _INCLUDE_SOURCEMOD_EXTENSION_PROPER_H_
diff --git a/forwards.cpp b/forwards.cpp
new file mode 100644
index 0000000..c3b7cc3
--- /dev/null
+++ b/forwards.cpp
@@ -0,0 +1,94 @@
+/**
+* vim: set ts=4 :
+* =============================================================================
+* SourceMod SourceTV Manager Extension
+* Copyright (C) 2004-2016 AlliedModders LLC. All rights reserved.
+* =============================================================================
+*
+* This program is free software; you can redistribute it and/or modify it under
+* the terms of the GNU General Public License, version 3.0, as published by the
+* Free Software Foundation.
+*
+* This program is distributed in the hope that it will be useful, but WITHOUT
+* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+* details.
+*
+* You should have received a copy of the GNU General Public License along with
+* this program. If not, see .
+*
+* As a special exception, AlliedModders LLC gives you permission to link the
+* code of this program (as well as its derivative works) to "Half-Life 2," the
+* "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software
+* by the Valve Corporation. You must obey the GNU General Public License in
+* all respects for all other code used. Additionally, AlliedModders LLC grants
+* this exception to all derivative works. AlliedModders LLC defines further
+* exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
+* or .
+*
+* Version: $Id$
+*/
+
+#include "extension.h"
+#include "forwards.h"
+
+CForwardManager g_pSTVForwards;
+
+SH_DECL_HOOK2_void(IDemoRecorder, StartRecording, SH_NOATTRIB, 0, const char *, bool)
+SH_DECL_HOOK1_void(IDemoRecorder, StopRecording, SH_NOATTRIB, 0, CGameInfo const *)
+
+void CForwardManager::Init()
+{
+ m_StartRecordingFwd = forwards->CreateForward("SourceTV_OnStartRecording", ET_Ignore, 3, NULL, Param_Cell, Param_String, Param_Cell);
+ m_StopRecordingFwd = forwards->CreateForward("SourceTV_OnStopRecording", ET_Ignore, 3, NULL, Param_Cell, Param_String, Param_Cell);
+}
+
+void CForwardManager::Shutdown()
+{
+ forwards->ReleaseForward(m_StartRecordingFwd);
+ forwards->ReleaseForward(m_StopRecordingFwd);
+}
+
+void CForwardManager::HookRecorder(IDemoRecorder *recorder)
+{
+ SH_ADD_HOOK(IDemoRecorder, StartRecording, recorder, SH_MEMBER(this, &CForwardManager::OnStartRecording_Post), false);
+ SH_ADD_HOOK(IDemoRecorder, StopRecording, recorder, SH_MEMBER(this, &CForwardManager::OnStopRecording_Post), false);
+}
+
+void CForwardManager::UnhookRecorder(IDemoRecorder *recorder)
+{
+ SH_REMOVE_HOOK(IDemoRecorder, StartRecording, recorder, SH_MEMBER(this, &CForwardManager::OnStartRecording_Post), false);
+ SH_REMOVE_HOOK(IDemoRecorder, StopRecording, recorder, SH_MEMBER(this, &CForwardManager::OnStopRecording_Post), false);
+}
+
+void CForwardManager::OnStartRecording_Post(const char *filename, bool bContinuously)
+{
+ if (m_StartRecordingFwd->GetFunctionCount() == 0)
+ RETURN_META(MRES_IGNORED);
+
+ m_StartRecordingFwd->PushCell(0); // TODO: Get current hltvserver index
+ m_StartRecordingFwd->PushString(filename);
+ m_StartRecordingFwd->PushCell(bContinuously);
+ m_StartRecordingFwd->Execute();
+
+ RETURN_META(MRES_IGNORED);
+}
+
+void CForwardManager::OnStopRecording_Post(CGameInfo const *info)
+{
+ if (m_StopRecordingFwd->GetFunctionCount() == 0)
+ RETURN_META(MRES_IGNORED);
+
+ IDemoRecorder *recorder = META_IFACEPTR(IDemoRecorder);
+ if (!recorder->IsRecording())
+ RETURN_META(MRES_IGNORED);
+
+ char *pDemoFile = (char *)recorder->GetDemoFile();
+
+ m_StopRecordingFwd->PushCell(0); // TODO: Get current hltvserver index
+ m_StopRecordingFwd->PushString(pDemoFile);
+ m_StopRecordingFwd->PushCell(recorder->GetRecordingTick());
+ m_StopRecordingFwd->Execute();
+
+ RETURN_META(MRES_IGNORED);
+}
\ No newline at end of file
diff --git a/forwards.h b/forwards.h
new file mode 100644
index 0000000..3c984e6
--- /dev/null
+++ b/forwards.h
@@ -0,0 +1,59 @@
+/**
+* vim: set ts=4 :
+* =============================================================================
+* SourceMod Sample Extension
+* Copyright (C) 2004-2008 AlliedModders LLC. All rights reserved.
+* =============================================================================
+*
+* This program is free software; you can redistribute it and/or modify it under
+* the terms of the GNU General Public License, version 3.0, as published by the
+* Free Software Foundation.
+*
+* This program is distributed in the hope that it will be useful, but WITHOUT
+* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+* details.
+*
+* You should have received a copy of the GNU General Public License along with
+* this program. If not, see .
+*
+* As a special exception, AlliedModders LLC gives you permission to link the
+* code of this program (as well as its derivative works) to "Half-Life 2," the
+* "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software
+* by the Valve Corporation. You must obey the GNU General Public License in
+* all respects for all other code used. Additionally, AlliedModders LLC grants
+* this exception to all derivative works. AlliedModders LLC defines further
+* exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
+* or .
+*
+* Version: $Id$
+*/
+
+#ifndef _INCLUDE_SOURCEMOD_EXTENSION_FORWARDS_H_
+#define _INCLUDE_SOURCEMOD_EXTENSION_FORWARDS_H_
+
+#include "extension.h"
+
+class CGameInfo;
+
+class CForwardManager
+{
+public:
+ void Init();
+ void Shutdown();
+
+ void HookRecorder(IDemoRecorder *recorder);
+ void UnhookRecorder(IDemoRecorder *recorder);
+
+private:
+ void OnStartRecording_Post(const char *filename, bool bContinuously);
+ void OnStopRecording_Post(CGameInfo const *info);
+
+private:
+ IForward *m_StartRecordingFwd;
+ IForward *m_StopRecordingFwd;
+};
+
+extern CForwardManager g_pSTVForwards;
+
+#endif // _INCLUDE_SOURCEMOD_EXTENSION_FORWARDS_H_
\ No newline at end of file
diff --git a/ihltvdemorecorder.h b/ihltvdemorecorder.h
new file mode 100644
index 0000000..f9d11d5
--- /dev/null
+++ b/ihltvdemorecorder.h
@@ -0,0 +1,33 @@
+#ifndef _INCLUDE_DEMORECORDER_H
+#define _INCLUDE_DEMORECORDER_H
+
+
+class CDemoFile;
+class bf_read;
+class ServerClass;
+
+class IDemoRecorder
+{
+public:
+
+ virtual CDemoFile *GetDemoFile() = 0;
+ virtual int GetRecordingTick(void) = 0;
+
+ virtual void StartRecording(const char *filename, bool bContinuously) = 0;
+ virtual void SetSignonState(int state) = 0;
+ virtual bool IsRecording(void) = 0;
+ virtual void PauseRecording(void) = 0;
+ virtual void ResumeRecording(void) = 0;
+ virtual void StopRecording(CGameInfo const *info) = 0;
+
+ virtual void RecordCommand(const char *cmdstring) = 0;
+ virtual void RecordUserInput(int cmdnumber) = 0;
+ virtual void RecordMessages(bf_read &data, int bits) = 0;
+ virtual void RecordPacket(void) = 0;
+ virtual void RecordServerClasses(ServerClass *pClasses) = 0;
+ virtual void RecordStringTables(void);
+ virtual void RecordCustomData(int, void const *, unsigned int);
+
+ virtual void ResetDemoInterpolation(void) = 0;
+};
+#endif
\ No newline at end of file
diff --git a/natives.cpp b/natives.cpp
new file mode 100644
index 0000000..8046b11
--- /dev/null
+++ b/natives.cpp
@@ -0,0 +1,492 @@
+/**
+* vim: set ts=4 :
+* =============================================================================
+* SourceMod SourceTV Manager Extension
+* Copyright (C) 2004-2016 AlliedModders LLC. All rights reserved.
+* =============================================================================
+*
+* This program is free software; you can redistribute it and/or modify it under
+* the terms of the GNU General Public License, version 3.0, as published by the
+* Free Software Foundation.
+*
+* This program is distributed in the hope that it will be useful, but WITHOUT
+* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+* details.
+*
+* You should have received a copy of the GNU General Public License along with
+* this program. If not, see .
+*
+* As a special exception, AlliedModders LLC gives you permission to link the
+* code of this program (as well as its derivative works) to "Half-Life 2," the
+* "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software
+* by the Valve Corporation. You must obey the GNU General Public License in
+* all respects for all other code used. Additionally, AlliedModders LLC grants
+* this exception to all derivative works. AlliedModders LLC defines further
+* exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
+* or .
+*
+* Version: $Id$
+*/
+
+#include "extension.h"
+
+extern const sp_nativeinfo_t sourcetv_natives[];
+
+// native SourceTV_GetHLTVServerCount();
+static cell_t Native_GetHLTVServerCount(IPluginContext *pContext, const cell_t *params)
+{
+ return hltvdirector->GetHLTVServerCount();
+}
+
+// native SourceTV_SelectHLTVServer();
+static cell_t Native_SelectHLTVServer(IPluginContext *pContext, const cell_t *params)
+{
+ if (params[1] < 0 || params[1] >= hltvdirector->GetHLTVServerCount())
+ {
+ pContext->ReportError("Invalid HLTV server instance number (%d).", params[1]);
+ return 0;
+ }
+ g_STVManager.SelectSourceTVServer(hltvdirector->GetHLTVServer(params[1]));
+
+ return 0;
+}
+
+// native SourceTV_GetSelectedHLTVServer();
+static cell_t Native_GetSelectedHLTVServer(IPluginContext *pContext, const cell_t *params)
+{
+ if (hltvserver == nullptr)
+ return -1;
+
+ for (int i = 0; i < hltvdirector->GetHLTVServerCount(); i++)
+ {
+ if (hltvserver == hltvdirector->GetHLTVServer(i))
+ return i;
+ }
+
+ // We should have found it in the above loop :S
+ hltvserver = nullptr;
+ return -1;
+}
+
+// native SourceTV_GetBotIndex();
+static cell_t Native_GetBotIndex(IPluginContext *pContext, const cell_t *params)
+{
+ if (hltvserver == nullptr)
+ return 0;
+
+ return hltvserver->GetHLTVSlot() + 1;
+}
+
+// native bool:SourceTV_GetLocalStats(&proxies, &slots, &specs);
+static cell_t Native_GetLocalStats(IPluginContext *pContext, const cell_t *params)
+{
+ if (hltvserver == nullptr)
+ return 0;
+
+ int proxies, slots, specs;
+ hltvserver->GetLocalStats(proxies, slots, specs);
+
+ cell_t *plProxies, *plSlots, *plSpecs;
+ pContext->LocalToPhysAddr(params[1], &plProxies);
+ pContext->LocalToPhysAddr(params[2], &plSlots);
+ pContext->LocalToPhysAddr(params[3], &plSpecs);
+
+ *plProxies = proxies;
+ *plSlots = slots;
+ *plSpecs = specs;
+ return 1;
+}
+
+// native SourceTV_GetBroadcastTick();
+static cell_t Native_GetBroadcastTick(IPluginContext *pContext, const cell_t *params)
+{
+ if (hltvserver == nullptr)
+ return -1;
+
+ return hltvdirector->GetDirectorTick();
+}
+
+// native Float:SourceTV_GetDelay();
+static cell_t Native_GetDelay(IPluginContext *pContext, const cell_t *params)
+{
+ if (hltvserver == nullptr)
+ return -1;
+
+ return sp_ftoc(hltvdirector->GetDelay());
+}
+
+// native bool:SourceTV_BroadcastHintMessage(const String:format[], any:...);
+static cell_t Native_BroadcastHintMessage(IPluginContext *pContext, const cell_t *params)
+{
+ if (hltvserver == nullptr)
+ return 0;
+
+ char buffer[1024];
+ size_t len;
+ {
+ DetectExceptions eh(pContext);
+ len = smutils->FormatString(buffer, sizeof(buffer), pContext, params, 1);
+ if (eh.HasException())
+ return 0;
+ }
+
+ IGameEvent *msg = gameevents->CreateEvent("hltv_message", true);
+ if (!msg)
+ return 0;
+
+ msg->SetString("text", buffer);
+ hltvserver->BroadcastEvent(msg);
+ gameevents->FreeEvent(msg);
+
+ return 1;
+}
+
+// native bool:SourceTV_BroadcastConsoleMessage(const String:format[], any:...);
+static cell_t Native_BroadcastConsoleMessage(IPluginContext *pContext, const cell_t *params)
+{
+ if (hltvserver == nullptr)
+ return 0;
+
+ static ICallWrapper *pBroadcastPrintf = nullptr;
+
+ if (!pBroadcastPrintf)
+ {
+ int offset = -1;
+ if (!g_pGameConf->GetOffset("BroadcastPrintf", &offset) || offset == -1)
+ {
+ pContext->ReportError("Failed to get CBaseServer::BroadcastPrintf offset.");
+ return 0;
+ }
+
+ PassInfo pass[3];
+ pass[0].flags = PASSFLAG_BYVAL;
+ pass[0].type = PassType_Basic;
+ pass[0].size = sizeof(void *);
+ pass[1].flags = PASSFLAG_BYVAL;
+ pass[1].type = PassType_Basic;
+ pass[1].size = sizeof(char *);
+ pass[2].flags = PASSFLAG_BYVAL;
+ pass[2].type = PassType_Basic;
+ pass[2].size = sizeof(char *);
+
+ void *iserver = (void *)hltvserver->GetBaseServer();
+ void **vtable = *(void ***)iserver;
+ void *func = vtable[offset];
+
+ pBroadcastPrintf = bintools->CreateCall(func, CallConv_Cdecl, NULL, pass, 3);
+ }
+
+ char buffer[1024];
+ size_t len;
+ {
+ DetectExceptions eh(pContext);
+ len = smutils->FormatString(buffer, sizeof(buffer) - 2, pContext, params, 1);
+ if (eh.HasException())
+ return 0;
+ }
+
+ buffer[len++] = '\n';
+ buffer[len] = '\0';
+
+ if (pBroadcastPrintf)
+ {
+ unsigned char vstk[sizeof(void *) + sizeof(char *) * 2];
+ unsigned char *vptr = vstk;
+
+ *(void **)vptr = (void *)hltvserver->GetBaseServer();
+ vptr += sizeof(void *);
+ *(char **)vptr = "%s";
+ vptr += sizeof(char *);
+ *(char **)vptr = buffer;
+
+ pBroadcastPrintf->Execute(vstk, NULL);
+ }
+
+ return 1;
+}
+
+// native SourceTV_GetViewEntity();
+static cell_t Native_GetViewEntity(IPluginContext *pContext, const cell_t *params)
+{
+ return hltvdirector->GetPVSEntity();
+}
+
+// native SourceTV_GetViewCoordinates();
+static cell_t Native_GetViewCoordinates(IPluginContext *pContext, const cell_t *params)
+{
+ Vector pvs = hltvdirector->GetPVSOrigin();
+
+ cell_t *addr;
+ pContext->LocalToPhysAddr(params[1], &addr);
+ addr[0] = sp_ftoc(pvs.x);
+ addr[1] = sp_ftoc(pvs.y);
+ addr[2] = sp_ftoc(pvs.z);
+ return 0;
+}
+
+// native bool:SourceTV_IsRecording();
+static cell_t Native_IsRecording(IPluginContext *pContext, const cell_t *params)
+{
+ if (hltvserver == nullptr)
+ return 0;
+
+ return hltvserver->IsRecording();
+}
+
+// Checks in COM_IsValidPath in the engine
+static bool IsValidPath(const char *path)
+{
+ return strlen(path) > 0 && !strstr(path, "\\\\") && !strstr(path, ":") && !strstr(path, "..") && !strstr(path, "\n") && !strstr(path, "\r");
+}
+
+// native bool:SourceTV_StartRecording(const String:sFilename[]);
+static cell_t Native_StartRecording(IPluginContext *pContext, const cell_t *params)
+{
+ if (hltvserver == nullptr)
+ return 0;
+
+ // SourceTV is not active.
+ if (!hltvserver->GetBaseServer()->IsActive())
+ return 0;
+
+ // Only SourceTV Master can record demos instantly
+ if (!hltvserver->IsMasterProxy())
+ return 0;
+
+ // already recording
+ if (hltvserver->IsRecording())
+ return 0;
+
+ char *pFile;
+ pContext->LocalToString(params[1], &pFile);
+
+ // Invalid path.
+ if (!IsValidPath(pFile))
+ return 0;
+
+ // Make sure there is a '.dem' suffix
+ char pPath[PLATFORM_MAX_PATH];
+ size_t len = strlen(pFile);
+ const char *ext = libsys->GetFileExtension(pFile);
+ if (!ext || stricmp(ext, "dem") != 0)
+ ext = ".dem";
+ else
+ ext = "";
+ smutils->Format(pPath, sizeof(pPath), "%s%s", pFile, ext);
+
+ if (hltvdirector->GetHLTVServerCount() > 1)
+ {
+ for (int i = 0; i < hltvdirector->GetHLTVServerCount(); i++)
+ {
+ IHLTVServer *otherserver = hltvdirector->GetHLTVServer(i);
+ if (!otherserver->IsRecording())
+ continue;
+
+ // Cannot record. another SourceTV is currently recording into that file.
+ if (!stricmp(pPath, otherserver->GetRecordingDemoFilename()))
+ return 0;
+ }
+ }
+
+ demorecorder->StartRecording(pPath, false);
+
+ return 1;
+}
+
+// native bool:SourceTV_StopRecording();
+static cell_t Native_StopRecording(IPluginContext *pContext, const cell_t *params)
+{
+ if (hltvserver == nullptr)
+ return 0;
+
+ if (!hltvserver->IsRecording())
+ return 0;
+
+ hltvserver->StopRecording(NULL);
+
+ // TODO: Stop recording on all other active hltvservers (tv_stoprecord in csgo does this)
+
+ return 1;
+}
+
+// native bool:SourceTV_GetDemoFileName(String:sFilename[], maxlen);
+static cell_t Native_GetDemoFileName(IPluginContext *pContext, const cell_t *params)
+{
+ if (hltvserver == nullptr)
+ return 0;
+
+ if (!hltvserver->IsRecording())
+ return 0;
+
+ char *pDemoFile = hltvserver->GetRecordingDemoFilename();
+ if (!pDemoFile)
+ return 0;
+
+ pContext->StringToLocalUTF8(params[1], params[2], pDemoFile, NULL);
+
+ return 1;
+}
+
+// native SourceTV_GetRecordingTick();
+static cell_t Native_GetRecordingTick(IPluginContext *pContext, const cell_t *params)
+{
+ if (demorecorder == nullptr)
+ return -1;
+
+ if (!demorecorder->IsRecording())
+ return -1;
+
+ return demorecorder->GetRecordingTick();
+}
+
+// native bool:SourceTV_PrintToDemoConsole(const String:format[], any:...);
+static cell_t Native_PrintToDemoConsole(IPluginContext *pContext, const cell_t *params)
+{
+ if (hltvserver == nullptr)
+ return 0;
+
+ if (!iserver)
+ return 0;
+ IClient *pClient = iserver->GetClient(hltvserver->GetHLTVSlot());
+ if (!pClient)
+ return 0;
+
+ char buffer[1024];
+ size_t len;
+ {
+ DetectExceptions eh(pContext);
+ len = smutils->FormatString(buffer, sizeof(buffer), pContext, params, 1);
+ if (eh.HasException())
+ return 0;
+ }
+
+ pClient->ClientPrintf("%s", buffer);
+
+ return 1;
+}
+
+
+
+// native SourceTV_GetSpectatorCount();
+static cell_t Native_GetSpectatorCount(IPluginContext *pContext, const cell_t *params)
+{
+ if (hltvserver == nullptr)
+ return -1;
+
+ return hltvserver->GetBaseServer()->GetNumClients();
+}
+
+// native SourceTV_GetMaxClients();
+static cell_t Native_GetMaxClients(IPluginContext *pContext, const cell_t *params)
+{
+ if (hltvserver == nullptr)
+ return -1;
+
+ return hltvserver->GetBaseServer()->GetMaxClients();
+}
+
+// native SourceTV_GetClientCount();
+static cell_t Native_GetClientCount(IPluginContext *pContext, const cell_t *params)
+{
+ if (hltvserver == nullptr)
+ return -1;
+
+ return hltvserver->GetBaseServer()->GetClientCount();
+}
+
+// native bool:SourceTV_IsClientConnected(client);
+static cell_t Native_IsClientConnected(IPluginContext *pContext, const cell_t *params)
+{
+ if (hltvserver == nullptr)
+ return 0;
+
+ cell_t client = params[1];
+ if (client < 1 || client > hltvserver->GetBaseServer()->GetClientCount())
+ {
+ pContext->ReportError("Invalid spectator client index %d.", client);
+ return 0;
+ }
+
+ IClient *pClient = hltvserver->GetBaseServer()->GetClient(client - 1);
+ return pClient->IsConnected();
+}
+
+// native SourceTV_GetSpectatorName(client, String:name[], maxlen);
+static cell_t Native_GetSpectatorName(IPluginContext *pContext, const cell_t *params)
+{
+ if (hltvserver == nullptr)
+ return 0;
+
+ cell_t client = params[1];
+ if (client < 1 || client > hltvserver->GetBaseServer()->GetClientCount())
+ {
+ pContext->ReportError("Invalid spectator client index %d.", client);
+ return 0;
+ }
+
+ IClient *pClient = hltvserver->GetBaseServer()->GetClient(client - 1);
+ if (!pClient || !pClient->IsConnected())
+ {
+ pContext->ReportError("Client %d is not connected.", client);
+ return 0;
+ }
+
+ pContext->StringToLocalUTF8(params[2], static_cast(params[3]), pClient->GetClientName(), NULL);
+ return 0;
+}
+
+// native SourceTV_KickClient(client, const String:sReason[]);
+static cell_t Native_KickClient(IPluginContext *pContext, const cell_t *params)
+{
+ if (hltvserver == nullptr)
+ return 0;
+
+ cell_t client = params[1];
+ if (client < 1 || client > hltvserver->GetBaseServer()->GetClientCount())
+ {
+ pContext->ReportError("Invalid spectator client index %d.", client);
+ return 0;
+ }
+
+ IClient *pClient = hltvserver->GetBaseServer()->GetClient(client - 1);
+ if (!pClient || !pClient->IsConnected())
+ {
+ pContext->ReportError("Client %d is not connected.", client);
+ return 0;
+ }
+
+ char *pReason;
+ pContext->LocalToString(params[2], &pReason);
+
+ hltvserver->GetBaseServer()->DisconnectClient(pClient, pReason);
+ return 0;
+}
+
+const sp_nativeinfo_t sourcetv_natives[] =
+{
+ { "SourceTV_GetHLTVServerCount", Native_GetHLTVServerCount },
+ { "SourceTV_SelectHLTVServer", Native_SelectHLTVServer },
+ { "SourceTV_GetSelectedHLTVServer", Native_GetSelectedHLTVServer },
+ { "SourceTV_GetBotIndex", Native_GetBotIndex },
+ { "SourceTV_GetLocalStats", Native_GetLocalStats },
+ { "SourceTV_GetBroadcastTick", Native_GetBroadcastTick },
+ { "SourceTV_GetDelay", Native_GetDelay },
+ { "SourceTV_BroadcastHintMessage", Native_BroadcastHintMessage },
+ { "SourceTV_BroadcastConsoleMessage", Native_BroadcastConsoleMessage },
+ { "SourceTV_GetViewEntity", Native_GetViewEntity },
+ { "SourceTV_GetViewCoordinates", Native_GetViewCoordinates },
+ { "SourceTV_StartRecording", Native_StartRecording },
+ { "SourceTV_StopRecording", Native_StopRecording },
+ { "SourceTV_IsRecording", Native_IsRecording },
+ { "SourceTV_GetDemoFileName", Native_GetDemoFileName },
+ { "SourceTV_GetRecordingTick", Native_GetRecordingTick },
+ { "SourceTV_PrintToDemoConsole", Native_PrintToDemoConsole },
+ { "SourceTV_GetSpectatorCount", Native_GetSpectatorCount },
+ { "SourceTV_GetMaxClients", Native_GetMaxClients },
+ { "SourceTV_GetClientCount", Native_GetClientCount },
+ { "SourceTV_IsClientConnected", Native_IsClientConnected },
+ { "SourceTV_GetSpectatorName", Native_GetSpectatorName },
+ { "SourceTV_KickClient", Native_KickClient },
+ { NULL, NULL },
+};
diff --git a/smsdk_config.h b/smsdk_config.h
new file mode 100644
index 0000000..7ff3527
--- /dev/null
+++ b/smsdk_config.h
@@ -0,0 +1,81 @@
+/**
+ * vim: set ts=4 :
+ * =============================================================================
+ * SourceMod Sample Extension
+ * Copyright (C) 2004-2008 AlliedModders LLC. All rights reserved.
+ * =============================================================================
+ *
+ * This program is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, version 3.0, as published by the
+ * Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * this program. If not, see .
+ *
+ * As a special exception, AlliedModders LLC gives you permission to link the
+ * code of this program (as well as its derivative works) to "Half-Life 2," the
+ * "Source Engine," the "SourcePawn JIT," and any Game MODs that run on software
+ * by the Valve Corporation. You must obey the GNU General Public License in
+ * all respects for all other code used. Additionally, AlliedModders LLC grants
+ * this exception to all derivative works. AlliedModders LLC defines further
+ * exceptions, found in LICENSE.txt (as of this writing, version JULY-31-2007),
+ * or .
+ *
+ * Version: $Id$
+ */
+
+#ifndef _INCLUDE_SOURCEMOD_EXTENSION_CONFIG_H_
+#define _INCLUDE_SOURCEMOD_EXTENSION_CONFIG_H_
+
+/**
+ * @file smsdk_config.h
+ * @brief Contains macros for configuring basic extension information.
+ */
+
+/* Basic information exposed publicly */
+#define SMEXT_CONF_NAME "SourceTV Manager"
+#define SMEXT_CONF_DESCRIPTION "Interface to interact with the SourceTV server."
+#define SMEXT_CONF_VERSION "1.0.0.0"
+#define SMEXT_CONF_AUTHOR "Peace-Maker"
+#define SMEXT_CONF_URL "http://www.wcfan.de/"
+#define SMEXT_CONF_LOGTAG "STVM"
+#define SMEXT_CONF_LICENSE "GPL"
+#define SMEXT_CONF_DATESTRING __DATE__
+
+/**
+ * @brief Exposes plugin's main interface.
+ */
+#define SMEXT_LINK(name) SDKExtension *g_pExtensionIface = name;
+
+/**
+ * @brief Sets whether or not this plugin required Metamod.
+ * NOTE: Uncomment to enable, comment to disable.
+ */
+#define SMEXT_CONF_METAMOD
+
+/** Enable interfaces you want to use here by uncommenting lines */
+#define SMEXT_ENABLE_FORWARDSYS
+//#define SMEXT_ENABLE_HANDLESYS
+//#define SMEXT_ENABLE_PLAYERHELPERS
+//#define SMEXT_ENABLE_DBMANAGER
+#define SMEXT_ENABLE_GAMECONF
+//#define SMEXT_ENABLE_MEMUTILS
+//#define SMEXT_ENABLE_GAMEHELPERS
+//#define SMEXT_ENABLE_TIMERSYS
+//#define SMEXT_ENABLE_THREADER
+#define SMEXT_ENABLE_LIBSYS
+//#define SMEXT_ENABLE_MENUS
+//#define SMEXT_ENABLE_ADTFACTORY
+//#define SMEXT_ENABLE_PLUGINSYS
+//#define SMEXT_ENABLE_ADMINSYS
+//#define SMEXT_ENABLE_TEXTPARSERS
+//#define SMEXT_ENABLE_USERMSGS
+//#define SMEXT_ENABLE_TRANSLATOR
+//#define SMEXT_ENABLE_ROOTCONSOLEMENU
+
+#endif // _INCLUDE_SOURCEMOD_EXTENSION_CONFIG_H_
diff --git a/sourcetv_test.sp b/sourcetv_test.sp
new file mode 100644
index 0000000..1aa5d8c
--- /dev/null
+++ b/sourcetv_test.sp
@@ -0,0 +1,250 @@
+#undef REQUIRE_EXTENSIONS
+#include "sourcetvmanager"
+
+public OnPluginStart()
+{
+ RegConsoleCmd("sm_servercount", Cmd_GetServerCount);
+ RegConsoleCmd("sm_selectserver", Cmd_SelectServer);
+ RegConsoleCmd("sm_getselectedserver", Cmd_GetSelectedServer);
+ RegConsoleCmd("sm_getbotindex", Cmd_GetBotIndex);
+ RegConsoleCmd("sm_localstats", Cmd_Localstats);
+ RegConsoleCmd("sm_getdelay", Cmd_GetDelay);
+ RegConsoleCmd("sm_spectators", Cmd_Spectators);
+ RegConsoleCmd("sm_spechintmsg", Cmd_SendHintMessage);
+ RegConsoleCmd("sm_specmsg", Cmd_SendMessage);
+ RegConsoleCmd("sm_startrecording", Cmd_StartRecording);
+ RegConsoleCmd("sm_stoprecording", Cmd_StopRecording);
+ RegConsoleCmd("sm_isrecording", Cmd_IsRecording);
+ RegConsoleCmd("sm_demofile", Cmd_GetDemoFileName);
+ RegConsoleCmd("sm_recordtick", Cmd_GetRecordTick);
+ RegConsoleCmd("sm_specstatus", Cmd_SpecStatus);
+ RegConsoleCmd("sm_democonsole", Cmd_PrintDemoConsole);
+ RegConsoleCmd("sm_botcmd", Cmd_ExecuteStringCommand);
+ RegConsoleCmd("sm_speckick", Cmd_KickClient);
+}
+
+public SourceTV_OnStartRecording(hltvinstance, const String:filename[], bool:bContinuously)
+{
+ PrintToServer("Started recording sourcetv #%d demo to %s (continuosly %d)", hltvinstance, filename, bContinuously);
+}
+
+public SourceTV_OnStopRecording(hltvinstance, const String:filename[], recordingtick)
+{
+ PrintToServer("Stopped recording sourcetv #%d demo to %s (%d ticks)", hltvinstance, filename, recordingtick);
+}
+
+public Action:Cmd_GetServerCount(client, args)
+{
+ ReplyToCommand(client, "SourceTV server count: %d", SourceTV_GetHLTVServerCount());
+ return Plugin_Handled;
+}
+
+public Action:Cmd_SelectServer(client, args)
+{
+ if (args < 1)
+ {
+ ReplyToCommand(client, "Usage: sm_selectserver ");
+ return Plugin_Handled;
+ }
+
+ new String:sArg[12];
+ GetCmdArg(1, sArg, sizeof(sArg));
+ new iInstance = StringToInt(sArg);
+
+ SourceTV_SelectHLTVServer(iInstance);
+ ReplyToCommand(client, "SourceTV selecting server: %d", iInstance);
+ return Plugin_Handled;
+}
+
+public Action:Cmd_GetSelectedServer(client, args)
+{
+ ReplyToCommand(client, "SourceTV selected server: %d", SourceTV_GetSelectedHLTVServer());
+ return Plugin_Handled;
+}
+
+public Action:Cmd_GetBotIndex(client, args)
+{
+ ReplyToCommand(client, "SourceTV bot index: %d", SourceTV_GetBotIndex());
+ return Plugin_Handled;
+}
+
+public Action:Cmd_Localstats(client, args)
+{
+ new proxies, slots, specs;
+ if (!SourceTV_GetLocalStats(proxies, slots, specs))
+ {
+ ReplyToCommand(client, "SourceTV local stats: no server selected :(");
+ return Plugin_Handled;
+ }
+ ReplyToCommand(client, "SourceTV local stats: proxies %d - slots %d - specs %d", proxies, slots, specs);
+ return Plugin_Handled;
+}
+
+public Action:Cmd_GetDelay(client, args)
+{
+ ReplyToCommand(client, "SourceTV delay: %f", SourceTV_GetDelay());
+ return Plugin_Handled;
+}
+
+public Action:Cmd_Spectators(client, args)
+{
+ ReplyToCommand(client, "SourceTV spectator count: %d/%d", SourceTV_GetSpectatorCount(), SourceTV_GetClientCount());
+ new String:sName[64];
+ for (new i=1;i<=SourceTV_GetClientCount();i++)
+ {
+ if (!SourceTV_IsClientConnected(i))
+ continue;
+
+ SourceTV_GetSpectatorName(i, sName, sizeof(sName));
+ ReplyToCommand(client, "Client %d: %s", i, sName);
+ }
+ return Plugin_Handled;
+}
+
+public Action:Cmd_SendHintMessage(client, args)
+{
+ if (args < 1)
+ {
+ ReplyToCommand(client, "Usage: sm_spechintmsg ");
+ return Plugin_Handled;
+ }
+
+ new String:sMsg[1024];
+ GetCmdArgString(sMsg, sizeof(sMsg));
+ StripQuotes(sMsg);
+
+ new bool:bSent = SourceTV_BroadcastHintMessage("%s", sMsg);
+ ReplyToCommand(client, "SourceTV sending hint message (success %d): %s", bSent, sMsg);
+ return Plugin_Handled;
+}
+
+public Action:Cmd_SendMessage(client, args)
+{
+ if (args < 1)
+ {
+ ReplyToCommand(client, "Usage: sm_specmsg ");
+ return Plugin_Handled;
+ }
+
+ new String:sMsg[1024];
+ GetCmdArgString(sMsg, sizeof(sMsg));
+ StripQuotes(sMsg);
+
+ new bool:bSent = SourceTV_BroadcastConsoleMessage("%s", sMsg);
+ ReplyToCommand(client, "SourceTV sending console message (success %d): %s", bSent, sMsg);
+ return Plugin_Handled;
+}
+
+public Action:Cmd_StartRecording(client, args)
+{
+ if (args < 1)
+ {
+ ReplyToCommand(client, "Usage: sm_startrecording ");
+ return Plugin_Handled;
+ }
+
+ new String:sFilename[PLATFORM_MAX_PATH];
+ GetCmdArgString(sFilename, sizeof(sFilename));
+ StripQuotes(sFilename);
+
+ if (SourceTV_StartRecording(sFilename))
+ {
+ SourceTV_GetDemoFileName(sFilename, sizeof(sFilename));
+ ReplyToCommand(client, "SourceTV started recording to: %s", sFilename);
+ }
+ else
+ ReplyToCommand(client, "SourceTV failed to start recording to: %s", sFilename);
+ return Plugin_Handled;
+}
+
+public Action:Cmd_StopRecording(client, args)
+{
+ ReplyToCommand(client, "SourceTV stopped recording %d", SourceTV_StopRecording());
+ return Plugin_Handled;
+}
+
+public Action:Cmd_IsRecording(client, args)
+{
+ ReplyToCommand(client, "SourceTV is recording: %d", SourceTV_IsRecording());
+ return Plugin_Handled;
+}
+
+public Action:Cmd_GetDemoFileName(client, args)
+{
+ new String:sFileName[PLATFORM_MAX_PATH];
+ ReplyToCommand(client, "SourceTV demo file name (%d): %s", SourceTV_GetDemoFileName(sFileName, sizeof(sFileName)), sFileName);
+ return Plugin_Handled;
+}
+
+public Action:Cmd_GetRecordTick(client, args)
+{
+ ReplyToCommand(client, "SourceTV recording tick: %d", SourceTV_GetRecordingTick());
+ return Plugin_Handled;
+}
+
+public Action:Cmd_SpecStatus(client, args)
+{
+ new iSourceTV = SourceTV_GetBotIndex();
+ if (!iSourceTV)
+ return Plugin_Handled;
+ FakeClientCommand(iSourceTV, "status");
+ ReplyToCommand(client, "Sent status bot console.");
+ return Plugin_Handled;
+}
+
+public Action:Cmd_PrintDemoConsole(client, args)
+{
+ if (args < 1)
+ {
+ ReplyToCommand(client, "Usage: sm_democonsole ");
+ return Plugin_Handled;
+ }
+
+ new String:sMsg[1024];
+ GetCmdArgString(sMsg, sizeof(sMsg));
+ StripQuotes(sMsg);
+
+ new bool:bSent = SourceTV_PrintToDemoConsole("%s", sMsg);
+ ReplyToCommand(client, "SourceTV printing to demo console (success %d): %s", bSent, sMsg);
+ return Plugin_Handled;
+}
+
+public Action:Cmd_ExecuteStringCommand(client, args)
+{
+ if (args < 1)
+ {
+ ReplyToCommand(client, "Usage: sm_botcmd ");
+ return Plugin_Handled;
+ }
+
+ new String:sCmd[1024];
+ GetCmdArgString(sCmd, sizeof(sCmd));
+ StripQuotes(sCmd);
+
+ new iSourceTV = SourceTV_GetBotIndex();
+ if (!iSourceTV)
+ return Plugin_Handled;
+ FakeClientCommand(iSourceTV, sCmd);
+ ReplyToCommand(client, "SourceTV executing command on bot: %s", sCmd);
+ return Plugin_Handled;
+}
+
+public Action:Cmd_KickClient(client, args)
+{
+ if (args < 1)
+ {
+ ReplyToCommand(client, "Usage: sm_speckick ");
+ return Plugin_Handled;
+ }
+
+ new String:sIndex[16], String:sMsg[1024];
+ GetCmdArg(1, sIndex, sizeof(sIndex));
+ StripQuotes(sIndex);
+ GetCmdArg(2, sMsg, sizeof(sMsg));
+ StripQuotes(sMsg);
+
+ new iTarget = StringToInt(sIndex);
+ SourceTV_KickClient(iTarget, sMsg);
+ ReplyToCommand(client, "SourceTV kicking spectator %d with reason %s", iTarget, sMsg);
+ return Plugin_Handled;
+}
\ No newline at end of file
diff --git a/sourcetvmanager.games.txt b/sourcetvmanager.games.txt
new file mode 100644
index 0000000..52724d9
--- /dev/null
+++ b/sourcetvmanager.games.txt
@@ -0,0 +1,46 @@
+"Games"
+{
+ "csgo"
+ {
+ "Addresses"
+ {
+ "host_client"
+ {
+ "windows"
+ {
+ "signature" "host_client"
+ "read" "58"
+ }
+ "linux"
+ {
+ "signature" "host_client"
+ }
+ }
+ }
+ "Offsets"
+ {
+ "BroadcastPrintf"
+ {
+ "windows" "39"
+ "linux" "40"
+ "mac" "40"
+ }
+
+ "CHLTVServer::m_DemoRecorder"
+ {
+ "windows" "19600"
+ "linux" "20496"
+ }
+ }
+ "Signatures"
+ {
+ "host_client"
+ {
+ "library" "engine"
+ "linux" "@host_client"
+ // ping(CCommand const&) "Client ping times:\n"
+ "windows" "\x55\x8B\xEC\x83\xE4\xC0\x83\xEC\x34\x83\x3D\x2A\x2A\x2A\x2A\x01\x53\x56\x57\x75\x2A"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/sourcetvmanager.inc b/sourcetvmanager.inc
new file mode 100644
index 0000000..c07f846
--- /dev/null
+++ b/sourcetvmanager.inc
@@ -0,0 +1,81 @@
+#if defined _stvmngr_included
+ #endinput
+#endif
+#define _stvmngr_included
+
+native SourceTV_GetHLTVServerCount();
+native SourceTV_SelectHLTVServer(instance);
+native SourceTV_GetSelectedHLTVServer();
+native SourceTV_GetBotIndex();
+native bool:SourceTV_GetLocalStats(&proxies, &slots, &specs);
+native SourceTV_GetBroadcastTick();
+native Float:SourceTV_GetDelay();
+native bool:SourceTV_BroadcastHintMessage(const String:format[], any:...);
+native bool:SourceTV_BroadcastConsoleMessage(const String:format[], any:...);
+// get current view entity (PVS), 0 if coords are used
+native SourceTV_GetViewEntity();
+// get current PVS origin
+native SourceTV_GetViewCoordinates(Float:view[3]);
+
+native bool:SourceTV_StartRecording(const String:sFilename[]);
+native bool:SourceTV_StopRecording();
+native bool:SourceTV_IsRecording();
+native bool:SourceTV_GetDemoFileName(String:sFilename[], maxlen);
+native SourceTV_GetRecordingTick();
+native bool:SourceTV_PrintToDemoConsole(const String:format[], any:...);
+
+native SourceTV_GetSpectatorCount();
+native SourceTV_GetMaxClients();
+native SourceTV_GetClientCount();
+native bool:SourceTV_IsClientConnected(client);
+native SourceTV_GetSpectatorName(client, String:name[], maxlen);
+native SourceTV_KickClient(client, const String:sReason[]);
+
+forward SourceTV_OnStartRecording(hltvinstance, const String:filename[], bool:bContinuously);
+forward SourceTV_OnStopRecording(hltvinstance, const String:filename[], recordingtick);
+
+/**
+ * Do not edit below this line!
+ */
+public Extension:__ext_stvmngr =
+{
+ name = "SourceTV Manager",
+ file = "sourcetvmanager.ext",
+#if defined AUTOLOAD_EXTENSIONS
+ autoload = 1,
+#else
+ autoload = 0,
+#endif
+#if defined REQUIRE_EXTENSIONS
+ required = 1,
+#else
+ required = 0,
+#endif
+};
+
+#if !defined REQUIRE_EXTENSIONS
+public __ext_stvmngr_SetNTVOptional()
+{
+ MarkNativeAsOptional("SourceTV_GetHLTVServerCount");
+ MarkNativeAsOptional("SourceTV_SelectHLTVServer");
+ MarkNativeAsOptional("SourceTV_GetSelectedHLTVServer");
+ MarkNativeAsOptional("SourceTV_GetBotIndex");
+ MarkNativeAsOptional("SourceTV_GetLocalStats");
+ MarkNativeAsOptional("SourceTV_GetBroadcastTick");
+ MarkNativeAsOptional("SourceTV_GetDelay");
+ MarkNativeAsOptional("SourceTV_SendHintMessage");
+ MarkNativeAsOptional("SourceTV_BroadcastConsoleMessage");
+ MarkNativeAsOptional("SourceTV_StartRecording");
+ MarkNativeAsOptional("SourceTV_StopRecording");
+ MarkNativeAsOptional("SourceTV_IsRecording");
+ MarkNativeAsOptional("SourceTV_GetDemoFileName");
+ MarkNativeAsOptional("SourceTV_GetRecordingTick");
+ MarkNativeAsOptional("SourceTV_PrintBotConsole");
+ MarkNativeAsOptional("SourceTV_GetSpectatorCount");
+ MarkNativeAsOptional("SourceTV_GetMaxClients");
+ MarkNativeAsOptional("SourceTV_GetClientCount");
+ MarkNativeAsOptional("SourceTV_IsClientConnected");
+ MarkNativeAsOptional("SourceTV_GetSpectatorName");
+ MarkNativeAsOptional("SourceTV_KickClient");
+}
+#endif