From d6c03438e40e12595b11b456c1ed23c29b6d6882 Mon Sep 17 00:00:00 2001 From: rio Date: Fri, 21 Sep 2018 22:38:46 -0500 Subject: [PATCH] Release commit --- .gitignore | 1 + README.md | 72 +- extension/AMBuildScript | 448 ++++++++ extension/AMBuilder | 31 + extension/configure.py | 23 + extension/extension.cpp | 43 + extension/extension.h | 81 ++ extension/smsdk_config.h | 45 + plugin/gamedata/gamemovement.games.txt | 31 + plugin/gamedata/triggerfilters.games.txt | 42 + plugin/scripting/include/marktouching.inc | 38 + plugin/scripting/rngfix.sp | 1234 +++++++++++++++++++++ tech.md | 91 ++ 13 files changed, 2178 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 extension/AMBuildScript create mode 100644 extension/AMBuilder create mode 100644 extension/configure.py create mode 100644 extension/extension.cpp create mode 100644 extension/extension.h create mode 100644 extension/smsdk_config.h create mode 100644 plugin/gamedata/gamemovement.games.txt create mode 100644 plugin/gamedata/triggerfilters.games.txt create mode 100644 plugin/scripting/include/marktouching.inc create mode 100644 plugin/scripting/rngfix.sp create mode 100644 tech.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3971b04 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +package/* diff --git a/README.md b/README.md index f6b8367..0f07d80 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,70 @@ -# rngfix -A SourceMod plugin for movement game modes +# RNGFix + +[![RNGFix Demo Video](http://img.youtube.com/vi/PlMjHAQ90G8/mqdefault.jpg)](https://www.youtube.com/watch?v=PlMjHAQ90G8) + +RNGFix is a [SourceMod](https://www.sourcemod.net/about.php) plugin that fixes a number of physics bugs that show up in movement-based game modes like bhop and surf. These issues are related in that they all appear to happen at random -- as far as a human player can tell. + +Another plugin, [Slope Landing Fix (Slopefix)](https://forums.alliedmods.net/showthread.php?p=2322788), fixes the first of these issues (downhill inclines) and is seen as a necessity for both bhop and surf. RNGFix follows the spirit of this plugin by expanding on it with fixes for many more pseudo-random bugs. + +Nothing this plugin does is impossible otherwise -- it just keeps random chance from mattering. + +## Dependencies + +* **SourceMod 1.10 - Build 6326 or newer** +The trigger jumping fix makes use of ray trace functionality added to SourceMod in in August 2018. + +* [**DHooks**](https://forums.alliedmods.net/showthread.php?t=180114) + +* MarkTouching Extension (included) +This simply exposes the function `IServerGameEnts::MarkEntitiesAsTouching` for this plugin to use. + +* (Optional, CS:GO) [Movement Unlocker](https://forums.alliedmods.net/showthread.php?t=255298) +Enables sliding on CS:GO. If you don't care about sliding on surf and the stair sliding fix, you don't need this. + +Also, remember that you should stop using Slopefix if using RNGFix. + +## Fixes + +**Downhill Inclines** + +Sometimes a player will not be "boosted" when falling onto an inclined surface, specifically while moving downhill. This fix results in the player always getting boosted. This is the scenario addressed by the original slopefix. RNGFix also implements this fix in a way that does not cause double boosts when a `trigger_push` is on the incline, which is a problem the original slopefix had. + + +**Uphill Inclines** + +When bhopping *up* an incline, sometimes the player loses speed on the initial jump, and sometimes they do not. This fix makes it so the player never loses speed in this scenario, as long as it was possible for the player to not lose speed, if not for the "luck" factor that makes this random. On shallow inclines and uneven ground, this means you will no longer randomly lose small amounts of speed when jumping, and on steep inclines this means you no longer need to land sideways and then turn directly up them, which was just a method for maximizing favorable odds. + + +**Trigger Jumping** + +Triggers that extend less than 2 units above the ground can sometimes be "jumped on" without activating them. This fix prevents this bug from occuring. This fixes annoyances like jumping on thin boosters without activating them, as well as exploitable behavior such as jumping on thin teleport triggers without activating them. + + +**Telehops** + +It is possible to pass through a teleport trigger so quickly that you also collide with the wall (or floor) behind it before actually being teleported, despite touching the teleporter "first". This fix makes it impossible to both collide with a surface and activate a teleport in the same tick. This is most notably useful on staged bhop maps with thin stage-end teleports positioned against walls; with this fix you no longer need to go through them at an angle just to maximize the odds of keeping your speed. + + +**Edge Bugs** + +When moving at high speed and landing on the extreme trailing edge of a platform, it is possible to collide with the surface -- resulting in a loss of vertical speed -- but without jumping, despite pressing jump in time (or holding jump with auto-bhop enabled). This fix causes the player to always be able to jump in this scenario. Note that you are still able to slide off by not pressing jump, if you wish to do so. + +**Stair Sliding** (Surf Only) + +The Source engine lets you move up stairs without requiring you to actually jump up each step -- as if the stairs were a simple incline -- and if you are moving fast this means you can slide up them quickly as well (on CSGO, sliding requires [Movement Unlocker](https://forums.alliedmods.net/showthread.php?t=255298)). However, if you are airborne and try to land on them at high speed, you may collide with the vertical face of a stair step before landing on top of a step, which results in a loss of speed and likely no slide. In the interest of making the incline-like behavior of stairs more consistent, this fix lets you slide up stairs when landing on them even if you hit the side of a stair step before the top of one. + +This fix will only be applied on surf maps (maps starting with `surf_`) because it has undesirable side-effects on bhop maps. It is also unlikely to be useful on bhop. + +--- +A more technical explanation of these fixes can be found [here](../tech.md). + +## Settings + +The fixes can be disabled individually by setting the following cvars to `0` in `cfg/sourcemod/plugin.rngfix.cfg`. All fixes are enabled by default. + +`rngfix_downhill` +`rngfix_uphill` +`rngfix_triggerjump` +`rngfix_telehop` +`rngfix_edge` +`rngfix_stairs` diff --git a/extension/AMBuildScript b/extension/AMBuildScript new file mode 100644 index 0000000..291cce9 --- /dev/null +++ b/extension/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', '21', 'CSGO', WinLinuxMac, 'csgo'), + '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'), + 'doi': SDK('HL2SDKDOI', '2.doi', '20', 'DOI', WinLinuxMac, 'doi'), +} + +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', 'doi', 'csgo']: + compiler.defines += ['NETWORK_VARS_ENABLED'] + + if sdk.name in ['css', 'hl2dm', 'dods', 'sdk2013', 'bms', 'tf2', 'l4d', 'nucleardawn', 'l4d2']: + 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', 'doi', 'csgo']: + 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', 'doi']: + dynamic_libs = ['libtier0_srv.so', 'libvstdlib_srv.so'] + elif sdk.name in ['l4d', 'blade', 'insurgency', 'doi', 'csgo']: + 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', 'doi', 'csgo']: + 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/extension/AMBuilder b/extension/AMBuilder new file mode 100644 index 0000000..3e8a218 --- /dev/null +++ b/extension/AMBuilder @@ -0,0 +1,31 @@ +# vim: set sts=2 ts=8 sw=2 tw=99 et ft=python: +import os, sys + +projectName = 'marktouching' + +# smsdk_ext.cpp will be automatically added later +sourceFiles = [ + 'extension.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 Extension.sdks: + sdk = Extension.sdks[sdk_name] + + binary = Extension.HL2Config(project, projectName + '.ext.' + sdk.ext, sdk) + +Extension.extensions = builder.Add(project) diff --git a/extension/configure.py b/extension/configure.py new file mode 100644 index 0000000..57910e8 --- /dev/null +++ b/extension/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/extension.cpp b/extension/extension.cpp new file mode 100644 index 0000000..88f4f21 --- /dev/null +++ b/extension/extension.cpp @@ -0,0 +1,43 @@ +#include "extension.h" + +MarkTouching g_MarkTouching; /**< Global singleton for extension's main interface */ +IServerGameEnts *gameents = NULL; + +SMEXT_LINK(&g_MarkTouching); + +void MarkTouching::SDK_OnAllLoaded() +{ + sharesys->AddNatives(myself, MyNatives); +} + +bool MarkTouching::SDK_OnMetamodLoad(ISmmAPI *ismm, char *error, size_t maxlen, bool late) +{ + GET_V_IFACE_ANY(GetServerFactory, gameents, IServerGameEnts, INTERFACEVERSION_SERVERGAMEENTS); + + return true; +} + +cell_t MarkEntitiesAsTouching(IPluginContext *pContext, const cell_t *params) +{ + edict_t *pEdict1 = gamehelpers->EdictOfIndex(params[1]); + if (!pEdict1 || pEdict1->IsFree()) + { + return pContext->ThrowNativeError("Entity %d is invalid", params[1]); + } + + edict_t *pEdict2 = gamehelpers->EdictOfIndex(params[2]); + if (!pEdict2 || pEdict2->IsFree()) + { + return pContext->ThrowNativeError("Entity %d is invalid", params[2]); + } + + gameents->MarkEntitiesAsTouching(pEdict1, pEdict2); + + return true; +} + +sp_nativeinfo_t MyNatives[] = +{ + {"MarkEntitiesAsTouching", MarkEntitiesAsTouching}, + {NULL, NULL}, +}; diff --git a/extension/extension.h b/extension/extension.h new file mode 100644 index 0000000..f49be11 --- /dev/null +++ b/extension/extension.h @@ -0,0 +1,81 @@ +#ifndef _INCLUDE_SOURCEMOD_EXTENSION_PROPER_H_ +#define _INCLUDE_SOURCEMOD_EXTENSION_PROPER_H_ + +#include "smsdk_ext.h" + +extern IServerGameEnts *gameents; +extern sp_nativeinfo_t MyNatives[]; + +class MarkTouching : 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 +}; + +#endif // _INCLUDE_SOURCEMOD_EXTENSION_PROPER_H_ diff --git a/extension/smsdk_config.h b/extension/smsdk_config.h new file mode 100644 index 0000000..b525d5c --- /dev/null +++ b/extension/smsdk_config.h @@ -0,0 +1,45 @@ +#ifndef _INCLUDE_SOURCEMOD_EXTENSION_CONFIG_H_ +#define _INCLUDE_SOURCEMOD_EXTENSION_CONFIG_H_ + +/* Basic information exposed publicly */ +#define SMEXT_CONF_NAME "MarkTouching" +#define SMEXT_CONF_DESCRIPTION "Exposes IServerGameEnts::MarkEntitiesAsTouching" +#define SMEXT_CONF_VERSION "1.0.0.0" +#define SMEXT_CONF_AUTHOR "rio" +#define SMEXT_CONF_URL "" +#define SMEXT_CONF_LOGTAG "MARKTOUCHING" +#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/plugin/gamedata/gamemovement.games.txt b/plugin/gamedata/gamemovement.games.txt new file mode 100644 index 0000000..50c85f7 --- /dev/null +++ b/plugin/gamedata/gamemovement.games.txt @@ -0,0 +1,31 @@ +"Games" +{ + "#default" + { + "Keys" + { + "IGameMovement" "GameMovement001" + } + "Signatures" + { + "CreateInterface" + { + "library" "server" + "windows" "@CreateInterface" + "linux" "@CreateInterface" + } + } + } + + "#default" + { + "Offsets" + { + "ProcessMovement" + { + "windows" "1" + "linux" "2" + } + } + } +} diff --git a/plugin/gamedata/triggerfilters.games.txt b/plugin/gamedata/triggerfilters.games.txt new file mode 100644 index 0000000..198f39b --- /dev/null +++ b/plugin/gamedata/triggerfilters.games.txt @@ -0,0 +1,42 @@ +"Games" +{ + "csgo" + { + "Offsets" + { + // applies to trigger_vphysics_motion and trigger_wind + "CBaseVPhysicsTrigger::PassesTriggerFilters" + { + "windows" "196" + "linux" "197" + } + + // applies to all other triggers + "CBaseTrigger::PassesTriggerFilters" + { + "windows" "206" + "linux" "207" + } + } + } + + "cstrike" + { + "Offsets" + { + // applies to trigger_vphysics_motion and trigger_wind + "CBaseVPhysicsTrigger::PassesTriggerFilters" + { + "windows" "188" + "linux" "189" + } + + // applies to all other triggers + "CBaseTrigger::PassesTriggerFilters" + { + "windows" "197" + "linux" "198" + } + } + } +} diff --git a/plugin/scripting/include/marktouching.inc b/plugin/scripting/include/marktouching.inc new file mode 100644 index 0000000..f0e50a6 --- /dev/null +++ b/plugin/scripting/include/marktouching.inc @@ -0,0 +1,38 @@ +#if defined _marktouching_included + #endinput +#endif +#define _marktouching_included + +/** + * Mark two entities as touching each other + * This is the same call that is triggered when two entities touch, and triggers StartTouch, Touch, and later EndTouch. + * Note that this will fire Touch() even if the entities were already touching, meaning Touch() will be called multiple times in one tick + * + * @param entity1 Entity index. + * @param entity2 Entity index. + * @noreturn + */ +native void MarkEntitiesAsTouching(int entity1, int entity2); + +#if !defined REQUIRE_EXTENSIONS +public __ext_marktouching_SetNTVOptional() +{ + MarkNativeAsOptional("MarkEntitiesAsTouching"); +} +#endif + +public Extension __ext_marktouching = +{ + name = "MarkTouching", + file = "marktouching.ext", +#if defined AUTOLOAD_EXTENSIONS + autoload = 1, +#else + autoload = 0, +#endif +#if defined REQUIRE_EXTENSIONS + required = 1, +#else + required = 0, +#endif +}; diff --git a/plugin/scripting/rngfix.sp b/plugin/scripting/rngfix.sp new file mode 100644 index 0000000..df075d8 --- /dev/null +++ b/plugin/scripting/rngfix.sp @@ -0,0 +1,1234 @@ +#include +#include + +#include +#include + +#pragma semicolon 1 +#pragma newdecls required + +#define PLUGIN_VERSION "1.0.0" + +public Plugin myinfo = +{ + name = "RNGFix", + author = "rio", + description = "Fixes physics bugs in movement game modes", + version = PLUGIN_VERSION, + url = "https://github.com/jason-e/rngfix" +} + +// Engine constants, NOT settings (do not change) +#define LAND_HEIGHT 2.0 // Maximum height above ground at which you can "land" +#define NON_JUMP_VELOCITY 140.0 // Maximum Z velocity you are allowed to have and still land +#define MIN_STANDABLE_ZNRM 0.7 // Minimum surface normal Z component of a walkable surface +#define AIR_SPEED_CAP 30.0 // Constant used to limit air acceleration +#define DUCK_MIN_DUCKSPEED 1.5 // Minimum duckspeed to start ducking +#define DEFAULT_JUMP_IMPULSE 301.99337741 // sqrt(2 * 57.0 units * 800.0 u/s^2) + +float g_vecMins[3]; +float g_vecMaxsUnducked[3]; +float g_vecMaxsDucked[3]; +float g_flDuckDelta; + +int g_iTick[MAXPLAYERS+1]; +float g_flFrameTime[MAXPLAYERS+1]; + +bool g_bTouchingTrigger[MAXPLAYERS+1][2048]; + +int g_iButtons[MAXPLAYERS+1]; +float g_vVel[MAXPLAYERS+1][3]; +float g_vAngles[MAXPLAYERS+1][3]; +int g_iOldButtons[MAXPLAYERS+1]; + +int g_iLastTickPredicted[MAXPLAYERS+1]; + +float g_vPreCollisionVelocity[MAXPLAYERS+1][3]; +float g_vLastBaseVelocity[MAXPLAYERS+1][3]; +int g_iLastGroundEnt[MAXPLAYERS+1]; +int g_iLastLandTick[MAXPLAYERS+1]; +int g_iLastCollisionTick[MAXPLAYERS+1]; +int g_iLastMapTeleportTick[MAXPLAYERS+1]; +float g_vCollisionPoint[MAXPLAYERS+1][3]; +float g_vCollisionNormal[MAXPLAYERS+1][3]; + +enum +{ + UPHILL_LOSS = -1, // Force a jump, AND negatively affect speed as if a collision occurred (fix RNG not in player's favor) + UPHILL_DEFAULT = 0, // Do nothing (retain RNG) + UPHILL_NEUTRAL = 1 // Force a jump (respecting NON_JUMP_VELOCITY) (fix RNG in player's favor) +} + +// Plugin settings +ConVar g_cvDownhill; +ConVar g_cvUphill; +ConVar g_cvEdge; +ConVar g_cvTriggerjump; +ConVar g_cvTelehop; +ConVar g_cvStairs; +ConVar g_cvUseOldSlopefixLogic; +ConVar g_cvDebug; + +// Core physics ConVars +ConVar g_cvMaxVelocity; +ConVar g_cvGravity; +ConVar g_cvAirAccelerate; + +// In CSS and CSGO but apparently not used in CSS +ConVar g_cvTimeBetweenDucks; + +// CSGO-only +ConVar g_cvJumpImpulse; +ConVar g_cvAutoBunnyHopping; + +Handle g_hPassesTriggerFilters; +Handle g_hProcessMovementHookPre; + +bool g_bIsSurfMap; + +bool g_bLateLoad; + +int g_iLaserIndex; +int g_color1[] = {0, 100, 255, 255}; +int g_color2[] = {0, 255, 0, 255}; + +void DebugMsg(int client, const char[] fmt, any ...) +{ + if (!g_cvDebug.BoolValue) return; + + char output[1024]; + VFormat(output, sizeof(output), fmt, 3); + PrintToConsole(client, "[%i] %s", g_iTick[client], output); +} + +void DebugLaser(int client, const float p1[3], const float p2[3], float life, float width, const int color[4]) +{ + if (g_cvDebug.IntValue < 2) return; + + TE_SetupBeamPoints(p2, p1, g_iLaserIndex, 0, 0, 0, life, width, width, 10, 0.0, color, 0); + TE_SendToClient(client); +} + +public APLRes AskPluginLoad2(Handle myself, bool late, char[] error, int err_max) +{ + g_bLateLoad = late; + return APLRes_Success; +} + +public void OnPluginStart() +{ + EngineVersion engine = GetEngineVersion(); + + if (engine != Engine_CSGO && engine != Engine_CSS) + { + SetFailState("Game is not supported"); + } + + g_vecMins = view_as({-16.0, -16.0, 0.0}); + g_vecMaxsUnducked = view_as({16.0, 16.0, 0.0}); + g_vecMaxsDucked = view_as({16.0, 16.0, 0.0}); + + switch (engine) + { + case Engine_CSGO: + { + g_vecMaxsUnducked[2] = 72.0; + g_vecMaxsDucked[2] = 64.0; + } + case Engine_CSS: + { + g_vecMaxsUnducked[2] = 62.0; + g_vecMaxsDucked[2] = 45.0; + } + } + + g_flDuckDelta = (g_vecMaxsUnducked[2]-g_vecMaxsDucked[2]) / 2; + + CreateConVar("rngfix_version", PLUGIN_VERSION, "RNGFix Version", FCVAR_NOTIFY|FCVAR_REPLICATED); + + g_cvDownhill = CreateConVar("rngfix_downhill", "1", "Enable downhill incline fix.", FCVAR_NOTIFY, true, 0.0, true, 1.0); + g_cvUphill = CreateConVar("rngfix_uphill", "1", "Enable uphill incline fix. Set to -1 to normalize effects not in the player's favor (not recommended).", FCVAR_NOTIFY, true, -1.0, true, 1.0); + g_cvEdge = CreateConVar("rngfix_edge", "1", "Enable edgebug fix.", FCVAR_NOTIFY, true, 0.0, true, 1.0); + g_cvTriggerjump = CreateConVar("rngfix_triggerjump", "1", "Enable trigger jump fix.", FCVAR_NOTIFY, true, 0.0, true, 1.0); + g_cvTelehop = CreateConVar("rngfix_telehop", "1", "Enable telehop fix.", FCVAR_NOTIFY, true, 0.0, true, 1.0); + g_cvStairs = CreateConVar("rngfix_stairs", "1", "Enable stair slide fix (surf only). You must have Movement Unlocker for sliding to work on CSGO.", FCVAR_NOTIFY, true, 0.0, true, 1.0); + + g_cvUseOldSlopefixLogic = CreateConVar("rngfix_useoldslopefixlogic", "0", "Old Slopefix had some logic errors that could cause double boosts. Enable this on a per-map basis to retain old behavior. (NOT RECOMMENDED)", FCVAR_NOTIFY, true, 0.0, true, 1.0); + + g_cvDebug = CreateConVar("rngfix_debug", "0", "1 = Enable debug messages. 2 = Enable debug messages and lasers.", _, true, 0.0, true, 2.0); + + AutoExecConfig(); + + g_cvMaxVelocity = FindConVar("sv_maxvelocity"); + g_cvGravity = FindConVar("sv_gravity"); + g_cvAirAccelerate = FindConVar("sv_airaccelerate"); + + if (g_cvMaxVelocity == null || g_cvGravity == null || g_cvAirAccelerate == null) + { + SetFailState("Could not find all ConVars"); + } + + // Not required + g_cvTimeBetweenDucks = FindConVar("sv_timebetweenducks"); + g_cvJumpImpulse = FindConVar("sv_jump_impulse"); + g_cvAutoBunnyHopping = FindConVar("sv_autobunnyhopping"); + + Handle filtersConf = LoadGameConfigFile("triggerfilters.games"); + if (filtersConf == null) SetFailState("Failed to load triggerfilters gamedata"); + + StartPrepSDKCall(SDKCall_Entity); + if (!PrepSDKCall_SetFromConf(filtersConf, SDKConf_Virtual, "CBaseTrigger::PassesTriggerFilters")) + { + SetFailState("Failed to get CBaseTrigger::PassesTriggerFilters offset"); + } + PrepSDKCall_SetReturnInfo(SDKType_Bool, SDKPass_Plain); + PrepSDKCall_AddParameter(SDKType_CBaseEntity, SDKPass_Pointer); + g_hPassesTriggerFilters = EndPrepSDKCall(); + + if (g_hPassesTriggerFilters == null) SetFailState("Unable to prepare SDKCall for CBaseTrigger::PassesTriggerFilters"); + + delete filtersConf; + + // Thanks SlidyBat and ici + Handle gameMovementConf = LoadGameConfigFile("gamemovement.games"); + if (gameMovementConf == null) SetFailState("Failed to load gamemovement gamedata"); + + StartPrepSDKCall(SDKCall_Static); + if (!PrepSDKCall_SetFromConf(gameMovementConf, SDKConf_Signature, "CreateInterface")) + { + SetFailState("Failed to get CreateInterface"); + } + PrepSDKCall_AddParameter(SDKType_String, SDKPass_Pointer); + PrepSDKCall_AddParameter(SDKType_PlainOldData, SDKPass_Pointer, VDECODE_FLAG_ALLOWNULL); + PrepSDKCall_SetReturnInfo(SDKType_PlainOldData, SDKPass_Plain); + + char interfaceName[64]; + if (!GameConfGetKeyValue(gameMovementConf, "IGameMovement", interfaceName, sizeof(interfaceName))) + { + SetFailState("Failed to get IGameMovement interface name"); + } + + Handle CreateInterface = EndPrepSDKCall(); + Address gameMovement = SDKCall(CreateInterface, interfaceName, 0); + delete CreateInterface; + + if (!gameMovement) + { + SetFailState("Failed to get IGameMovement pointer"); + } + + int offset = GameConfGetOffset(gameMovementConf, "ProcessMovement"); + if (offset == -1) SetFailState("Failed to get ProcessMovement offset"); + + g_hProcessMovementHookPre = DHookCreate(offset, HookType_Raw, ReturnType_Void, ThisPointer_Ignore, DHook_ProcessMovementPre); + DHookAddParam(g_hProcessMovementHookPre, HookParamType_CBaseEntity); + DHookAddParam(g_hProcessMovementHookPre, HookParamType_ObjectPtr); + DHookRaw(g_hProcessMovementHookPre, false, gameMovement); + + delete gameMovementConf; + + if (g_bLateLoad) + { + for (int client = 1; client <= MaxClients; client++) + { + if (IsClientInGame(client)) OnClientPutInServer(client); + } + + char classname[64]; + for (int entity = MaxClients+1; entity < sizeof(g_bTouchingTrigger[]); entity++) + { + if (!IsValidEntity(entity)) continue; + GetEntPropString(entity, Prop_Data, "m_iClassname", classname, sizeof(classname)); + HookTrigger(entity, classname); + } + } +} + +public void OnMapStart() +{ + g_iLaserIndex = PrecacheModel("materials/sprites/laserbeam.vmt", true); + + char map[PLATFORM_MAX_PATH]; + GetCurrentMap(map, sizeof(map)); + g_bIsSurfMap = StrContains(map, "surf_", false) == 0; +} + +public void OnEntityCreated(int entity, const char[] classname) +{ + if (entity >= sizeof(g_bTouchingTrigger[])) return; + HookTrigger(entity, classname); +} + +void HookTrigger(int entity, const char[] classname) +{ + if (StrContains(classname, "trigger_") != -1) + { + SDKHook(entity, SDKHook_StartTouchPost, Hook_TriggerStartTouch); + SDKHook(entity, SDKHook_EndTouchPost, Hook_TriggerEndTouch); + } + + if (StrContains(classname, "trigger_teleport") != -1) + { + SDKHook(entity, SDKHook_TouchPost, Hook_TriggerTeleportTouchPost); + } +} + +public void OnClientConnected(int client) +{ + g_iTick[client] = 0; + for (int i = 0; i < sizeof(g_bTouchingTrigger[]); i++) g_bTouchingTrigger[client][i] = false; +} + +public void OnClientPutInServer(int client) +{ + SDKHook(client, SDKHook_GroundEntChangedPost, Hook_PlayerGroundEntChanged); + SDKHook(client, SDKHook_PostThink, Hook_PlayerPostThink); +} + +public Action Hook_TriggerStartTouch(int entity, int other) +{ + if (1 <= other <= MaxClients) + { + g_bTouchingTrigger[other][entity] = true; + DebugMsg(other, "StartTouch %i", entity); + } + + return Plugin_Continue; +} + +// TODO Would be nice to have IServerTools::FindEntityByName / CGlobalEntityList::FindEntityByName +bool NameExists(const char[] targetname) +{ + // Assume special types exist + if (targetname[0] == '!') return true; + + char targetname2[128]; + + int max = GetMaxEntities(); + for (int entity = 1; entity < max; entity++) + { + if (!IsValidEntity(entity)) continue; + if (GetEntPropString(entity, Prop_Data, "m_iName", targetname2, sizeof(targetname2)) == 0) continue; + + if (StrEqual(targetname, targetname2)) return true; + } + + return false; +} + +public void Hook_TriggerTeleportTouchPost(int entity, int other) +{ + if (!(1 <= other <= MaxClients)) return; + + if (!SDKCall(g_hPassesTriggerFilters, entity, other)) return; + + char targetstring[128]; + if (GetEntPropString(entity, Prop_Data, "m_target", targetstring, sizeof(targetstring)) == 0) return; + + if (!NameExists(targetstring)) return; + + g_iLastMapTeleportTick[other] = g_iTick[other]; + + DebugMsg(other, "Triggered teleport %i", entity); +} + +public Action Hook_TriggerEndTouch(int entity, int other) +{ + if (1 <= other <= MaxClients) + { + g_bTouchingTrigger[other][entity] = false; + DebugMsg(other, "EndTouch %i", entity); + } + return Plugin_Continue; +} + +public bool PlayerFilter(int entity, int mask) +{ + return !(1 <= entity <= MaxClients); +} + +float GetJumpImpulse() +{ + if (g_cvJumpImpulse != null) + { + return g_cvJumpImpulse.FloatValue; + } + else + { + return DEFAULT_JUMP_IMPULSE; + } +} + +bool IsDuckCoolingDown(int client) +{ + // TODO Is this stuff in MoveData? + + // Ducking is prevented if the last switch to a ducked state from an unducked state is sooner than sv_timebetweenducks ago. + // Note: This cooldown is based on client's curtime (GetGameTime() in this context) and thus is unaffected by m_flLaggedMovementValue. + if (g_cvTimeBetweenDucks != null && HasEntProp(client, Prop_Data, "m_flLastDuckTime")) + { + if (GetGameTime() - GetEntPropFloat(client, Prop_Data, "m_flLastDuckTime") < g_cvTimeBetweenDucks.FloatValue) return true; + } + + // m_flDuckSpeed is decreased by 2.0 to a minimum of 0.0 every time the duck key is pressed OR released. + // It recovers at a rate of 3.0 * m_flLaggedMovementValue per second and caps at 8.0. + // Switching to a ducked state from an unducked state is prevented if it is less than 1.5. + if (HasEntProp(client, Prop_Data, "m_flDuckSpeed")) + { + if (GetEntPropFloat(client, Prop_Data, "m_flDuckSpeed") < DUCK_MIN_DUCKSPEED) return true; + } + + return false; +} + +void Duck(int client, float origin[3], float mins[3], float maxs[3]) +{ + bool ducking = GetEntityFlags(client) & FL_DUCKING != 0; + + bool nextDucking = ducking; + + if (g_iButtons[client] & IN_DUCK != 0 && !ducking) + { + if (!IsDuckCoolingDown(client)) + { + origin[2] += g_flDuckDelta; + nextDucking = true; + } + } + else if (g_iButtons[client] & IN_DUCK == 0 && ducking) + { + origin[2] -= g_flDuckDelta; + + TR_TraceHullFilter(origin, origin, g_vecMins, g_vecMaxsUnducked, MASK_PLAYERSOLID, PlayerFilter); + + // Cannot unduck in air, not enough room + if (TR_DidHit()) origin[2] += g_flDuckDelta; + else nextDucking = false; + } + + mins = g_vecMins; + maxs = nextDucking ? g_vecMaxsDucked : g_vecMaxsUnducked; +} + +bool CanJump(int client) +{ + if (g_iButtons[client] & IN_JUMP == 0) return false; + if (g_iOldButtons[client] & IN_JUMP != 0 && !(g_cvAutoBunnyHopping != null && g_cvAutoBunnyHopping.BoolValue)) return false; + + return true; +} + +void CheckJumpButton(int client, float velocity[3]) +{ + // Skip dead and water checks since we already did them + + // We need to check for ground somewhere so stick it here + if (GetEntityFlags(client) & FL_ONGROUND == 0) return; + + if (!CanJump(client)) return; + + // TODO Incorporate surfacedata jump factor + + // This conditional is why jumping while crouched jumps higher! Bad! + if (GetEntProp(client, Prop_Data, "m_bDucking") != 0 || GetEntityFlags(client) & FL_DUCKING != 0) + { + velocity[2] = GetJumpImpulse(); + } + else + { + velocity[2] += GetJumpImpulse(); + } + + // Jumping does an extra half tick of gravity! Bad! + FinishGravity(client, velocity); +} + +void AirAccelerate(int client, float velocity[3], Handle hParams) +{ + // This also includes the initial parts of AirMove() + + float fore[3], side[3]; + float wishvel[3], wishdir[3]; + + GetAngleVectors(g_vAngles[client], fore, side, NULL_VECTOR); + + fore[2] = 0.0; + side[2] = 0.0; + NormalizeVector(fore, fore); + NormalizeVector(side, side); + + for (int i = 0; i < 2; i++) wishvel[i] = fore[i] * g_vVel[client][0] + side[i] * g_vVel[client][1]; + + float wishspeed = NormalizeVector(wishvel, wishdir); + float m_flMaxSpeed = DHookGetParamObjectPtrVar(hParams, 2, 56, ObjectValueType_Float); + if (wishspeed > m_flMaxSpeed && m_flMaxSpeed != 0.0) wishspeed = m_flMaxSpeed; + + if (wishspeed) + { + float wishspd = wishspeed; + if (wishspd > AIR_SPEED_CAP) wishspd = AIR_SPEED_CAP; + + float currentspeed = GetVectorDotProduct(velocity, wishdir); + float addspeed = wishspd - currentspeed; + + if (addspeed > 0) + { + float accelspeed = g_cvAirAccelerate.FloatValue * wishspeed * g_flFrameTime[client]; + if (accelspeed > addspeed) accelspeed = addspeed; + + for (int i = 0; i < 2; i++) velocity[i] += accelspeed * wishdir[i]; + } + } +} + +void CheckVelocity(float velocity[3]) +{ + for (int i = 0; i < 3; i++) + { + if (velocity[i] > g_cvMaxVelocity.FloatValue) velocity[i] = g_cvMaxVelocity.FloatValue; + else if (velocity[i] < -g_cvMaxVelocity.FloatValue) velocity[i] = -g_cvMaxVelocity.FloatValue; + } +} + +void StartGravity(int client, float velocity[3]) +{ + float localGravity = GetEntPropFloat(client, Prop_Data, "m_flGravity"); + if (localGravity == 0.0) localGravity = 1.0; + + velocity[2] -= localGravity * g_cvGravity.FloatValue * 0.5 * g_flFrameTime[client]; + + float baseVelocity[3]; + GetEntPropVector(client, Prop_Data, "m_vecBaseVelocity", baseVelocity); + velocity[2] += baseVelocity[2] * g_flFrameTime[client]; + + // baseVelocity[2] would get cleared here but we shouldn't do that since this is just a prediction + + CheckVelocity(velocity); +} + +void FinishGravity(int client, float velocity[3]) +{ + float localGravity = GetEntPropFloat(client, Prop_Data, "m_flGravity"); + if (localGravity == 0.0) localGravity = 1.0; + + velocity[2] -= localGravity * g_cvGravity.FloatValue * 0.5 * g_flFrameTime[client]; + + CheckVelocity(velocity); +} + +bool CheckWater(int client) +{ + // The cached water level is updated multiple times per tick, including after movement happens, + // so we can just check the cached value here. + return GetEntProp(client, Prop_Data, "m_nWaterLevel") > 1; +} + +void PreventCollision(int client, Handle hParams, const float origin[3], const float collisionPoint[3], const float velocity_tick[3]) +{ + DebugLaser(client, origin, collisionPoint, 15.0, 0.5, g_color1); + + // Rewind part of a tick so at the end of this tick we will end up close to the ground without colliding with it. + // This effectively simulates a mid-tick jump (we lose part of a tick but its a miniscule trade-off). + // This is also only an approximation of a partial tick rewind but it's good enough. + float newOrigin[3]; + SubtractVectors(collisionPoint, velocity_tick, newOrigin); + + // Add a little space between us and the ground so we don't accidentally hit it anyway, maybe due to floating point error or something. + // I don't know if this is necessary but I would rather be safe. + newOrigin[2] += 0.1; + + // Since the MoveData for this tick has already been filled and is about to be used, we need + // to modify it directly instead of changing the player entity's actual position (such as with TeleportEntity) + DHookSetParamObjectPtrVarVector(hParams, 2, GetEngineVersion() == Engine_CSGO ? 172 : 152, ObjectValueType_Vector, newOrigin); + + DebugLaser(client, origin, newOrigin, 15.0, 0.5, g_color2); + + float adjustment[3]; + SubtractVectors(newOrigin, origin, adjustment); + DebugMsg(client, "Moved: %.2f %.2f %.2f", adjustment[0], adjustment[1], adjustment[2]); + + // No longer colliding this tick, clear our prediction flag + g_iLastCollisionTick[client] = 0; +} + +void ClipVelocity(const float velocity[3], const float nrm[3], float out[3]) +{ + float backoff = GetVectorDotProduct(velocity, nrm); + + for (int i = 0; i < 3; i++) + { + out[i] = velocity[i] - nrm[i]*backoff; + } + + // The adjust step only matters with overbounce which doesnt apply to walkable surfaces. +} + +void SetVelocity(int client, float velocity[3]) +{ + // Pull out basevelocity from desired true velocity + // Use the pre-tick basevelocity because that is what influenced this tick's movement and the desired new velocity. + SubtractVectors(velocity, g_vLastBaseVelocity[client], velocity); + + float baseVelocity[3]; + GetEntPropVector(client, Prop_Data, "m_vecBaseVelocity", baseVelocity); + + TeleportEntity(client, NULL_VECTOR, NULL_VECTOR, velocity); + + // TeleportEntity with non-null velocity wipes out basevelocity, so restore it after. + // Since we didn't change position, nothing should change regarding influences on basevelocity. + SetEntPropVector(client, Prop_Data, "m_vecBaseVelocity", baseVelocity); +} + +public MRESReturn DHook_ProcessMovementPre(Handle hParams) +{ + int client = DHookGetParam(hParams, 1); + + g_iTick[client]++; + g_flFrameTime[client] = GetTickInterval() * GetEntPropFloat(client, Prop_Data, "m_flLaggedMovementValue"); + + // If we are actually not doing ANY of the fixes that rely on pre-tick collision prediction, skip all this. + if (!g_cvUphill.BoolValue && !g_cvEdge.BoolValue && !g_cvStairs.BoolValue && !g_cvTelehop.BoolValue && !g_cvDownhill.BoolValue) + { + return MRES_Ignored; + } + + RunPreTickChecks(client, hParams); + + return MRES_Ignored; +} + +void RunPreTickChecks(int client, Handle hParams) +{ + // Recreate enough of CGameMovement::ProcessMovement to predict if fixes are needed. + // We only really care about a limited set of scenarios (less than waist-deep in water, MOVETYPE_WALK, air movement) + + if (!IsPlayerAlive(client)) return; + if (GetEntityMoveType(client) != MOVETYPE_WALK) return; + if (CheckWater(client)) return; + + g_iLastGroundEnt[client] = GetEntPropEnt(client, Prop_Data, "m_hGroundEntity"); + + // If we are definitely staying on the ground this tick, don't predict it. + if (g_iLastGroundEnt[client] != -1 && !CanJump(client)) return; + + g_iLastTickPredicted[client] = g_iTick[client]; + + g_iButtons[client] = DHookGetParamObjectPtrVar(hParams, 2, 36, ObjectValueType_Int); + g_iOldButtons[client] = DHookGetParamObjectPtrVar(hParams, 2, 40, ObjectValueType_Int); + DHookGetParamObjectPtrVarVector(hParams, 2, 44, ObjectValueType_Vector, g_vVel[client]); + DHookGetParamObjectPtrVarVector(hParams, 2, 12, ObjectValueType_Vector, g_vAngles[client]); + + float velocity[3]; + DHookGetParamObjectPtrVarVector(hParams, 2, 64, ObjectValueType_Vector, velocity); + + float baseVelocity[3]; + // basevelocity is not stored in MoveData + GetEntPropVector(client, Prop_Data, "m_vecBaseVelocity", baseVelocity); + + float origin[3]; + DHookGetParamObjectPtrVarVector(hParams, 2, GetEngineVersion() == Engine_CSGO ? 172 : 152, ObjectValueType_Vector, origin); + + float nextOrigin[3], mins[3], maxs[3]; + + nextOrigin = origin; + + // These roughly replicate the behavior of their equivalent CGameMovement functions. + + Duck(client, nextOrigin, mins, maxs); + + StartGravity(client, velocity); + + CheckJumpButton(client, velocity); + + CheckVelocity(velocity); + + AirAccelerate(client, velocity, hParams); + + // StartGravity dealt with Z basevelocity. + baseVelocity[2] = 0.0; + g_vLastBaseVelocity[client] = baseVelocity; + AddVectors(velocity, baseVelocity, velocity); + + // Store this for later in case we need to undo the effects of a collision. + g_vPreCollisionVelocity[client] = velocity; + + // This is basically where TryPlayerMove happens. + // We don't really care about anything after TryPlayerMove either. + + float velocity_tick[3]; + velocity_tick = velocity; + ScaleVector(velocity_tick, g_flFrameTime[client]); + + AddVectors(nextOrigin, velocity_tick, nextOrigin); + + // Check if we will hit something this tick. + TR_TraceHullFilter(origin, nextOrigin, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (TR_DidHit()) + { + float nrm[3]; + TR_GetPlaneNormal(null, nrm); + + if (g_iLastCollisionTick[client] < g_iTick[client]-1) + { + DebugMsg(client, "Collision predicted! (normal: %.3f %.3f %.3f)", nrm[0], nrm[1], nrm[2]); + } + + float collisionPoint[3]; + TR_GetEndPosition(collisionPoint); + + // Store this result for post-tick fixes. + g_iLastCollisionTick[client] = g_iTick[client]; + g_vCollisionPoint[client] = collisionPoint; + g_vCollisionNormal[client] = nrm; + + // If we are moving up too fast, we can't land anyway so these fixes aren't needed. + if (velocity[2] > NON_JUMP_VELOCITY) return; + + // Landing also requires a walkable surface. + // This will give false negatives if the surface initially collided + // is too steep but the final one isn't (rare and unlikely to matter). + if (nrm[2] < MIN_STANDABLE_ZNRM) return; + + // Check uphill incline fix first since it's more common and faster. + if (g_cvUphill.IntValue == UPHILL_NEUTRAL) + { + // Make sure it's not flat, and that we are actually going uphill (X/Y dot product < 0.0) + if (nrm[2] < 1.0 && nrm[0]*velocity[0] + nrm[1]*velocity[1] < 0.0) + { + bool shouldDoDownhillFixInstead = false; + + if (g_cvDownhill.BoolValue) + { + // We also want to make sure this isn't a case where it's actually more beneficial to do the downhill fix. + float newVelocity[3]; + ClipVelocity(velocity, nrm, newVelocity); + + if (newVelocity[0]*newVelocity[0] + newVelocity[1]*newVelocity[1] > velocity[0]*velocity[0] + velocity[1]*velocity[1]) + { + shouldDoDownhillFixInstead = true; + } + } + + if (!shouldDoDownhillFixInstead) + { + DebugMsg(client, "DO FIX: Uphill Incline"); + PreventCollision(client, hParams, origin, collisionPoint, velocity_tick); + + // This naturally prevents any edge bugs so we can skip the edge fix. + return; + } + } + } + + if (g_cvEdge.BoolValue) + { + // Do a rough estimate of where we will be at the end of the tick after colliding. + // This method assumes no more collisions will take place after the first. + // There are some very extreme circumstances where this will give false positives (unlikely to come into play). + + float tickEnd[3]; + float fraction_left = 1.0 - TR_GetFraction(); + + if (nrm[2] == 1.0) + { + // If the ground is level, all that changes is Z velocity becomes zero. + tickEnd[0] = collisionPoint[0] + velocity_tick[0]*fraction_left; + tickEnd[1] = collisionPoint[1] + velocity_tick[1]*fraction_left; + tickEnd[2] = collisionPoint[2]; + } + else + { + float velocity2[3]; + ClipVelocity(velocity, nrm, velocity2); + + if (velocity2[2] > NON_JUMP_VELOCITY) + { + // This would be an "edge bug" (slide without landing at the end of the tick) + // 100% of the time due to the Z velocity restriction. + return; + } + else + { + ScaleVector(velocity2, g_flFrameTime[client]*fraction_left); + AddVectors(collisionPoint, velocity2, tickEnd); + } + } + + // Check if there's something close enough to land on below the player at the end of this tick. + float tickEndBelow[3]; + tickEndBelow[0] = tickEnd[0]; + tickEndBelow[1] = tickEnd[1]; + tickEndBelow[2] = tickEnd[2] - LAND_HEIGHT; + + TR_TraceHullFilter(tickEnd, tickEndBelow, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (TR_DidHit()) + { + // There's something there, can we land on it? + float nrm2[3]; + TR_GetPlaneNormal(null, nrm2); + + // Yes, it's not too steep. + if (nrm2[2] >= MIN_STANDABLE_ZNRM) return; + // Yes, the quadrant check finds ground that isn't too steep. + if (TracePlayerBBoxForGround(tickEnd, tickEndBelow, mins, maxs)) return; + } + + DebugMsg(client, "DO FIX: Edge Bug"); + DebugLaser(client, collisionPoint, tickEnd, 15.0, 0.5, g_color1); + + PreventCollision(client, hParams, origin, collisionPoint, velocity_tick); + } + } +} + +public void Hook_PlayerGroundEntChanged(int client) +{ + // We cannot get the new ground entity at this point, + // but if the previous value was -1, it must be something else now, so we landed. + if (GetEntPropEnt(client, Prop_Data, "m_hGroundEntity") == -1) + { + g_iLastLandTick[client] = g_iTick[client]; + DebugMsg(client, "Landed"); + } +} + +bool DoTriggerjumpFix(int client, const float landingPoint[3], const float landingMins[3], const float landingMaxs[3]) +{ + if (!g_cvTriggerjump.BoolValue) return false; + + // It's possible to land above a trigger but also in another trigger_teleport, have the teleport move you to + // another location, and then the trigger jumping fix wouldn't fire the other trigger you technically landed above, + // but I can't imagine a mapper would ever actually stack triggers like that. + + float origin[3]; + GetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", origin); + + float landingMaxsBelow[3]; + landingMaxsBelow[0] = landingMaxs[0]; + landingMaxsBelow[1] = landingMaxs[1]; + landingMaxsBelow[2] = origin[2] - landingPoint[2]; + + ArrayList triggers = new ArrayList(); + + // Find triggers that are between us and the ground (using the bounding box quadrant we landed with if applicable). + TR_EnumerateEntitiesHull(landingPoint, landingPoint, landingMins, landingMaxsBelow, true, AddTrigger, triggers); + + bool didSomething = false; + + for (int i = 0; i < triggers.Length; i++) + { + int trigger = triggers.Get(i); + + // MarkEntitiesAsTouching always fires the Touch function even if it was already fired this tick. + // In case that could cause side-effects, manually keep track of triggers we are actually touching + // and don't re-touch them. + if (g_bTouchingTrigger[client][trigger]) continue; + + DebugMsg(client, "DO FIX: Trigger Jumping (entity %i)", trigger); + + MarkEntitiesAsTouching(client, trigger); + didSomething = true; + } + + delete triggers; + + return didSomething; +} + +bool DoStairsFix(int client) +{ + if (!g_cvStairs.BoolValue) return false; + if (g_iLastTickPredicted[client] != g_iTick[client]) return false; + + // This fix has undesirable side-effects on bhop. It is also very unlikely to help on bhop. + if (!g_bIsSurfMap) return false; + + // Let teleports take precedence (including teleports activated by the trigger jumping fix). + if (g_iLastMapTeleportTick[client] == g_iTick[client]) return false; + + // If moving upward, the player would never be able to slide up with any current position. + if (g_vPreCollisionVelocity[client][2] > 0.0) return false; + + // Stair step faces don't necessarily have to be completely vertical, but, if they are not, + // sliding up them at high speed -- or even just walking up -- usually doesn't work. + // Plus, it's really unlikely that there are actual stairs shaped like that. + if (g_iLastCollisionTick[client] == g_iTick[client] && g_vCollisionNormal[client][2] == 0.0) + { + // Do this first and stop if we are moving slowly (less than 1 unit per tick). + float velocity_dir[3]; + velocity_dir = g_vPreCollisionVelocity[client]; + velocity_dir[2] = 0.0; + if (NormalizeVector(velocity_dir, velocity_dir) * g_flFrameTime[client] < 1.0) return false; + + float mins[3], maxs[3]; + GetEntPropVector(client, Prop_Data, "m_vecMins", mins); + GetEntPropVector(client, Prop_Data, "m_vecMaxs", maxs); + + // We seem to have collided with a "wall", now figure out if it's a stair step. + + // Look for ground below us + float stepsize = GetEntPropFloat(client, Prop_Data, "m_flStepSize"); + + float end[3]; + end = g_vCollisionPoint[client]; + end[2] -= stepsize; + + TR_TraceHullFilter(g_vCollisionPoint[client], end, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (TR_DidHit()) + { + float nrm[3]; + TR_GetPlaneNormal(null, nrm); + + // Ground below is not walkable, not stairs + if (nrm[2] < MIN_STANDABLE_ZNRM) return false; + + float start[3]; + TR_GetEndPosition(start); + + // Find triggers that we would trigger if we did touch the ground here. + ArrayList triggers = new ArrayList(); + + TR_EnumerateEntitiesHull(start, start, mins, maxs, true, AddTrigger, triggers); + + for (int i = 0; i < triggers.Length; i++) + { + int trigger = triggers.Get(i); + + if (SDKCall(g_hPassesTriggerFilters, trigger, client)) + { + // We would have triggered something on the ground here, so we cant be sure the stairs fix is safe to do. + // The most likely scenario here is this isn't stairs, but just a short ledge with a fail teleport in front. + delete triggers; + return false; + } + } + + delete triggers; + + // Now follow CGameMovement::StepMove behavior. + + // Trace up + end = start; + end[2] += stepsize; + TR_TraceHullFilter(start, end, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (TR_DidHit()) TR_GetEndPosition(end); + + // Trace over (only 1 unit, just to find a stair step) + start = end; + AddVectors(start, velocity_dir, end); + + TR_TraceHullFilter(start, end, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (TR_DidHit()) + { + // The plane we collided with is too tall to be a stair step (i.e. it's a wall, not stairs). + // Or possibly: the ceiling is too low to get on top of it. + return false; + } + else + { + // Trace downward + start = end; + end[2] -= stepsize; + + TR_TraceHullFilter(start, end, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (!TR_DidHit()) return false; // Shouldn't happen + + TR_GetPlaneNormal(null, nrm); + + // Ground atop "stair" is not walkable, not stairs + if (nrm[2] < MIN_STANDABLE_ZNRM) return false; + + // It looks like we actually collided with a stair step. + // Put the player just barely on top of the stair step we found and restore their speed + TR_GetEndPosition(end); + + DebugMsg(client, "DO FIX: Stair Sliding"); + + TeleportEntity(client, end, NULL_VECTOR, NULL_VECTOR); + SetVelocity(client, g_vPreCollisionVelocity[client]); + + return true; + } + } + } + + return false; +} + +bool DoInclineCollisionFixes(int client, const float nrm[3]) +{ + if (!g_cvDownhill.BoolValue && g_cvUphill.IntValue != UPHILL_LOSS) return false; + if (g_iLastTickPredicted[client] != g_iTick[client]) return false; + + // There's no point in checking for fix if we were moving up, unless we want to do an uphill collision + if (g_vPreCollisionVelocity[client][2] > 0.0 && g_cvUphill.IntValue != UPHILL_LOSS) return false; + + // If a collision was predicted this tick (and wasn't prevented by another fix alrady), no fix is needed. + // It's possible we actually have to run the edge bug fix and an incline fix in the same tick. + if (g_iLastCollisionTick[client] == g_iTick[client]) return false; + + // Make sure the ground is not level, otherwise a collision would do nothing important anyway. + if (nrm[2] == 1.0) return false; + + // This velocity includes changes from player input this tick as well as + // the half tick of gravity applied before collision would occur. + float velocity[3]; + velocity = g_vPreCollisionVelocity[client]; + + if (g_cvUseOldSlopefixLogic.BoolValue) + { + // The old slopefix did not consider basevelocity when calculating deflected velocity + SubtractVectors(velocity, g_vLastBaseVelocity[client], velocity); + } + + float dot = nrm[0]*velocity[0] + nrm[1]*velocity[1]; + + if (dot >= 0) + { + // If going downhill, only adjust velocity if the downhill incline fix is on. + if (!g_cvDownhill.BoolValue) return false; + } + + bool downhillFixIsBeneficial = false; + + float newVelocity[3]; + ClipVelocity(velocity, nrm, newVelocity); + + if (newVelocity[0]*newVelocity[0] + newVelocity[1]*newVelocity[1] > velocity[0]*velocity[0] + velocity[1]*velocity[1]) + { + downhillFixIsBeneficial = true; + } + + if (dot < 0) + { + // If going uphill, only adjust velocity if uphill incline fix is set to loss mode + // OR if this is actually a case where the downhill incline fix is better. + if (!((downhillFixIsBeneficial && g_cvDownhill.BoolValue) || g_cvUphill.IntValue == UPHILL_LOSS)) return false; + } + + DebugMsg(client, "DO FIX: Incline Collision (%s) (z-normal: %.3f)", downhillFixIsBeneficial ? "Downhill" : "Uphill", nrm[2]); + + // Make sure Z velocity is zero since we are on the ground. + newVelocity[2] = 0.0; + + // Since we are on the ground, we also don't need to FinishGravity(). + + if (g_cvUseOldSlopefixLogic.BoolValue) + { + // The old slopefix immediately moves basevelocity into local velocity to keep it from getting cleared. + // This results in double boosts as the player is likely still being influenced by the source of the basevelocity. + if (GetEntityFlags(client) & FL_BASEVELOCITY != 0) + { + float baseVelocity[3]; + GetEntPropVector(client, Prop_Data, "m_vecBaseVelocity", baseVelocity); + AddVectors(newVelocity, baseVelocity, newVelocity); + } + + TeleportEntity(client, NULL_VECTOR, NULL_VECTOR, newVelocity); + } + else + { + SetVelocity(client, newVelocity); + } + + return true; +} + +bool DoTelehopFix(int client) +{ + if (!g_cvTelehop.BoolValue) return false; + if (g_iLastTickPredicted[client] != g_iTick[client]) return false; + + if (g_iLastMapTeleportTick[client] != g_iTick[client]) return false; + + // Check if we either collided this tick OR landed during this tick. + // Note that we could have landed this tick, lost Z velocity, then gotten teleported, making us no longer on the ground. + // This is why we need to remember if we landed mid-tick rather than just check ground state now. + if (!(g_iLastCollisionTick[client] == g_iTick[client] || g_iLastLandTick[client] == g_iTick[client])) return false; + + // At this point, ideally we should check if the teleport would have triggered "after" the collision (within the tick duration), + // and, if so, not restore speed, but properly doing that would involve completely duplicating TryPlayerMove but with + // multiple intermediate trigger checks which is probably a bad idea... better to just give people the benefit of the doubt sometimes. + + // Restore the velocity we would have had if we didn't collide or land. + float newVelocity[3]; + newVelocity = g_vPreCollisionVelocity[client]; + + // Don't forget to add the second half-tick of gravity ourselves. + FinishGravity(client, newVelocity); + + DebugMsg(client, "DO FIX: Telehop"); + + SetVelocity(client, newVelocity); + + return true; +} + +// PostThink works a little better than a ProcessMovement post hook because we need to wait for ProcessImpacts (trigger activation) +public void Hook_PlayerPostThink(int client) +{ + if (!IsPlayerAlive(client)) return; + if (GetEntityMoveType(client) != MOVETYPE_WALK) return; + if (CheckWater(client)) return; + + bool landed = GetEntPropEnt(client, Prop_Data, "m_hGroundEntity") != -1 && g_iLastGroundEnt[client] == -1; + + float origin[3], landingMins[3], landingMaxs[3], nrm[3], landingPoint[3]; + + // Get info about the ground we landed on (if we need to do landing fixes). + if (landed && (g_cvTriggerjump.BoolValue || g_cvDownhill.BoolValue || g_cvUphill.IntValue == UPHILL_LOSS)) + { + GetEntPropVector(client, Prop_Data, "m_vecAbsOrigin", origin); + + GetEntPropVector(client, Prop_Data, "m_vecMins", landingMins); + GetEntPropVector(client, Prop_Data, "m_vecMaxs", landingMaxs); + + float originBelow[3]; + originBelow[0] = origin[0]; + originBelow[1] = origin[1]; + originBelow[2] = origin[2] - LAND_HEIGHT; + + TR_TraceHullFilter(origin, originBelow, landingMins, landingMaxs, MASK_PLAYERSOLID, PlayerFilter); + + if (!TR_DidHit()) + { + // This should never happen, since we know we are on the ground. + landed = false; + } + else + { + TR_GetPlaneNormal(null, nrm); + + if (nrm[2] < MIN_STANDABLE_ZNRM) + { + // This is rare, and how the incline fix should behave isn't entirely clear because maybe we should + // collide with multiple faces at once in this case, but let's just get the ground we officially + // landed on and use that for our ground normal. + + // landingMins and landingMaxs will contain the final values used to find the ground after returning. + if (TracePlayerBBoxForGround(origin, originBelow, landingMins, landingMaxs)) + { + TR_GetPlaneNormal(null, nrm); + } + else + { + // This should also never happen. + landed = false; + } + + DebugMsg(client, "Used bounding box quadrant to find ground (z-normal: %.3f)", nrm[2]); + } + + TR_GetEndPosition(landingPoint); + } + } + + if (landed && TR_GetFraction() > 0.0) + { + DoTriggerjumpFix(client, landingPoint, landingMins, landingMaxs); + + // Check if a trigger we just touched put us in the air (probably due to a teleport). + if (GetEntityFlags(client) & FL_ONGROUND == 0) landed = false; + } + + // The stair sliding fix changes the outcome of this tick more significantly, so it doesn't really make sense to do incline fixes too. + if (DoStairsFix(client)) return; + + if (landed) + { + DoInclineCollisionFixes(client, nrm); + } + + DoTelehopFix(client); +} + +public bool AddTrigger(int entity, ArrayList triggers) +{ + TR_ClipCurrentRayToEntity(MASK_ALL, entity); + if (TR_DidHit()) triggers.Push(entity); + + return true; +} + +bool TracePlayerBBoxForGround(const float origin[3], const float originBelow[3], float mins[3], float maxs[3]) +{ + // See CGameMovement::TracePlayerBBoxForGround() + + float origMins[3], origMaxs[3]; + origMins = mins; + origMaxs = maxs; + + float nrm[3]; + + mins = origMins; + + // -x -y + maxs[0] = origMaxs[0] > 0.0 ? 0.0 : origMaxs[0]; + maxs[1] = origMaxs[1] > 0.0 ? 0.0 : origMaxs[1]; + maxs[2] = origMaxs[2]; + + TR_TraceHullFilter(origin, originBelow, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (TR_DidHit()) + { + TR_GetPlaneNormal(null, nrm); + if (nrm[2] >= MIN_STANDABLE_ZNRM) return true; + } + + // +x +y + mins[0] = origMins[0] < 0.0 ? 0.0 : origMins[0]; + mins[1] = origMins[1] < 0.0 ? 0.0 : origMins[1]; + mins[2] = origMins[2]; + + maxs = origMaxs; + + TR_TraceHullFilter(origin, originBelow, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (TR_DidHit()) + { + TR_GetPlaneNormal(null, nrm); + if (nrm[2] >= MIN_STANDABLE_ZNRM) return true; + } + + // -x +y + mins[0] = origMins[0]; + mins[1] = origMins[1] < 0.0 ? 0.0 : origMins[1]; + mins[2] = origMins[2]; + + maxs[0] = origMaxs[0] > 0.0 ? 0.0 : origMaxs[0]; + maxs[1] = origMaxs[1]; + maxs[2] = origMaxs[2]; + + TR_TraceHullFilter(origin, originBelow, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (TR_DidHit()) + { + TR_GetPlaneNormal(null, nrm); + if (nrm[2] >= MIN_STANDABLE_ZNRM) return true; + } + + // +x -y + mins[0] = origMins[0] < 0.0 ? 0.0 : origMins[0]; + mins[1] = origMins[1]; + mins[2] = origMins[2]; + + maxs[0] = origMaxs[0]; + maxs[1] = origMaxs[1] > 0.0 ? 0.0 : origMaxs[1]; + maxs[2] = origMaxs[2]; + + TR_TraceHullFilter(origin, originBelow, mins, maxs, MASK_PLAYERSOLID, PlayerFilter); + + if (TR_DidHit()) + { + TR_GetPlaneNormal(null, nrm); + if (nrm[2] >= MIN_STANDABLE_ZNRM) return true; + } + + return false; +} diff --git a/tech.md b/tech.md new file mode 100644 index 0000000..7162bde --- /dev/null +++ b/tech.md @@ -0,0 +1,91 @@ +## Tick Simulation + +This is a simple overview of some key engine steps for reference. Not all of these are used by RNGFix, nor is this all-inclusive. + +1. **`OnPlayerRunCmd`** (SourceMod's public forward) - This function should really only be used for modifying player inputs. If you need to run some code for each client for each command that is actually executed (i.e. where the server is not overloaded), you should probably use a PreThink or PreThinkPost hook. In many cases doing stuff here (or in OnPlayerRunCmdPost) is fine as long as you are aware that this does not have a 1:1 relationship with physically simulated client ticks. + + (Note that if the server is overloaded, the command is dropped and the following steps are skipped) + + 1. **`PreThink/PreThinkPost`** - This is a convenient function to hook that is run right before the command is processed and the player is moved, triggers are touched, etc. + 2. **`CGameMovement::ProcessMovement`** - This is where the key and mouse data for this user command are applied and the player is moved one tick + 3. **`CGameMovement::ProcessImpacts`** - This is where triggers are actually "touched", note that this is enitrely after movement for this tick has completed + 4. **`PostThink/PostThinkPost`** - This is a convenient function to hook that is run right after the command is processed and the player is moved, triggers are touched, etc. + + +2. **`OnPlayerRunCmdPost`** - This should most correctly be used as a convenient way to check the final inputs for this command after all plugins have potentially modified them. This forward is fired even if the command was actually dropped. + + + +## Technical Overview of Fixes + +These fixes are split into two groups: pre-tick fixes -- which are detected and applied immediately *before* a user command is run and a tick is simulated -- and post-tick fixes, which correct the results of a tick immediately *after* it is simulated. This just depends on what I decided was the best way to apply these fixes. + +The actions of this plugin are coupled fairly tightly to the engine's movement processing (the most important parts are executed immediately before `CGameMovement::ProcessMovement` is run, the rest happens before each player's `PostThink`) and thus this plugin is unlikely to interfere with other plugins, or be negatively impacted by them. To put it another way, it is safe for other plugins to do whatever they want in `OnPlayerRunCmd/OnPlayerRunCmdPost` and player `PreThink` calls without interfering with these fixes. + +In order to understand some of these problems and their fixes, it is relevant to know that the engine will only consider a player "on the ground", and thus able to walk and jump, if their Z velocity is less than positive 140.0. If this is not the case, surfaces that are otherwise shallow enough to walk on will essentially behave like surf ramps because the player is considered in the air rather than standing on them. This is why the player is not able to rapidly bhop up steep inclines when moving faster than a certain speed. + +--- + + +**Downhill Inclines** [Post-tick] + +If the plugin detects that the player just landed on the ground, but did so without ever colliding with it this tick, the player's velocity is updated to reflect what it would have been had the player actually collided with it. This fix is only applied if a collision would result in the player having a larger absolute amount of horizontal speed than before, which is always the case when falling straight down or moving "downhill", and is *sometimes* the case when moving uphill, especially for steep inclines. This is the same criteria that the original slopefix used to determine when to apply the fix. + +The reason it is possible to "land" on the ground without actually touching it is because the engine will consider a player "on the ground" if there is walkable ground *within 2 units* below them at certain times within the simulation of a tick. If it just so happens that you end up within 2 units of a walkable surface at the end of a tick, the engine effectively considers you to have landed on it, which zeroes out your Z velocity immediately with no consideration for the interaction between that Z velocity and the angle of the ground. This issue is more prevalent on **higher** tickrates. + +The original slopefix plugin handled this fix slightly differently. Its deflection velocity calculation does not take basevelocity into effect, and more importantly: when the new velocity is applied, any existing basevelocity is baked into the player's velocity immediately, which unfortunately results in a "double boost" if the player jumps on an incline while touching a `trigger_push`. RNGFix handles these things more accurately which eliminates this side-effect, but if you *really* want the old behavior (double boosts) for legacy reasons, set `rngfix_useoldslopefixlogic` to `1` on a case-by-case, per-map basis. + +--- +**Uphill Inclines** [Pre-tick] + +This fix is very much the opposite of the downhill incline fix and aims to guarantee the result that is the opposite of what the downhill incline fix does. Occurrences of this issue are more prevalent on **lower** tickrates. + +If the plugin detects that the player *will* collide with an incline (in an "uphill" direction, or into the incline) once this tick is simulated, and it is possible to land on this surface (that is, the surface is not too steep to walk on, and the player's Z velocity at the time of collision is less than positive 140.0), then the player is moved *away* from the incline such that they will barely not collide with the incline by the end of the tick. Note that this adjustment is often only a few units or less and is totally imperceptible to the player in real-time. + +This change means that instead of colliding and deflecting along the incline, the player will instead land on the incline without colliding with it. This is desirable because landing immediately zeroes out Z velocity, and the player is able to jump while having the full horizontal velocity they started with. In the event of going up a moderately steep incline, this results in the most favorable possible collision with the surface on the following tick and the greatest possible amount of retained speed when "launching" off the incline. + +Note that this fix will not be applied if the downhill fix is enabled *and* that fix would result in horizontal speed gain as explained above. + +This plugin also gives you the option of normalizing the random behavior of jumping uphill in the opposite way, such that doing so always results in a *collision* with the surface -- and thus a loss of speed. This setting is not recommended, as jumping up even the slightest of inclines can quickly sap player speed, while doing so without the plugin would almost never result in lost speed. To enable this, set `rngfix_uphill` to `-1`. This effectively makes the uphill incline fix function identically to the downhill incline fix, except it is executed even when moving uphill and when doing so results in horizontal speed loss. + +--- +**Edge Bugs** [Pre-tick] + +The upcoming tick is simulated to determine if the following are true: +1. The player will collide with a walkable surface -- This is important because the general possibility of being able to land/jump but not actually doing so is what defines this bug +2. After colliding, the player's Z velocity is less than positive 140.0 (a requirement to be able to land, and thus jump rather than slide) +3. Once the *remainder* of the tick is simulated following the collision, the player ends up in a location where there is no ground to land on below them + +If all of these are true, the player's position is adjusted such that they will barely avoid colliding with the ground by the end of the tick. This is effectively the same solution as the uphill incline fix, but with different activating conditions. Occurrences of this issue are more prevalent on **lower** tickrates. + +--- +**Trigger Jumping** [Post-tick] + +If the plugin detects that the player just landed on the ground, it determines how far below the player the ground is (which could be as many as 2 units below), finds any triggers that are in this space between the player and the ground, and manually signals to the engine that the player is touching these triggers (if the player was not already touching them). + +The rationale behind this is that, if the player is "landed" on the ground, then their hitbox logically must extend all the way to the ground, and thus any triggers in such space should activate. This fix is pretty easy to justify and likely would have been handled better in the engine itself if not for the fact that it *really* only matters in maps made for movement game modes, as thin ground triggers do not come into play in first-party content (and neither does autobhop). Occurrences of this issue are more prevalent on **higher** tickrates. + +--- +**Telehops** [Post-tick] + +If the plugin detects that a `trigger_teleport` was activated during this tick, and either: +* The plugin predicted right before the tick that a collision would occur during this tick (resulting in a change / loss of velocity) +*or* +* The plugin detected that the client landed during the simulation of the tick (resulting in an instant removal of Z velocity) + +Then the player's velocity is restored to the velocity they would have had after this tick (including any influence from key and mouse inputs) had the player not collided with -- or landed on -- anything. + +The engine simulates each tick in a sequence of discrete steps, which to put it simply starts with a complete simulation of player movement including collisions with any solids, and only *after* this has finished does the engine check to see if the client is touching any triggers and activates them. This means it is not all that unlikely that a player will collide with something inside of or behind a thin `trigger_teleport` before triggering it, despite passing through it to even reach the point of collision. Occurrences of this issue are more prevalent on **lower** tickrates. + +--- +**Stair Sliding** [Post-tick] + +The plugin checks if the following conditions are true: +1. The plugin predicted that a collision with a vertical surface would occur during this tick. +2. There is walkable ground directly below the point of collision, within the maximum step size (generally, 18.0 units). +3. If the player were to stand on the ground below the point of collision, they would activate no triggers. +4. From the ground below the point of collision, the surface collided with can be stepped up (the step must be as high as the maximum step size at most, there is nothing above the player that prevents the player from traveling up that distance, and the surface on top of the step must be walkable). + +If all of these conditions are true, then the player is placed just barely on top of the stair step they just collided with, and the velocity they would have had if they had not collided with the face of the stair step is restored. This issue is mostly unaffected by tickrate. + +This fix is only applied on surf maps (maps starting with `surf_`) because it can save a bhopping player from losing all of their speed if they barely hit a small single step even if they had no intention of sliding. Stairs are very uncommonly found on bhop maps, and even then I can't say I've ever seen a staircase that was worth sliding up as part of an optimal route, so the fix is really not needed on bhop anyway.