diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f99795020..989219ea8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -79,7 +79,7 @@ jobs: uses: actions/upload-artifact@v4 with: name: ${{ runner.os }} - path: CS2Fixes/build/package + path: CS2Fixes/build/package/cs2 release: name: Release diff --git a/AMBuildScript b/AMBuildScript index abe002b1c..7e67b9ba4 100644 --- a/AMBuildScript +++ b/AMBuildScript @@ -1,20 +1,21 @@ # vim: set sts=2 ts=8 sw=2 tw=99 et ft=python: -import os, sys +import os, json, re, subprocess +from ambuild2.frontend.v2_2.cpp.builders import TargetSuffix # Edit the functions below for the extra functionality, the return should be # a list of path's to wanted locations -def additional_libs(context, binary, sdk): +def additionalLibs(context, binary, sdk): return [ # Path should be relative either to hl2sdk folder or to build folder # 'path/to/lib/example.lib', ] -def additional_defines(context, binary, sdk): +def additionalDefines(context, binary, sdk): return [ - # 'EXAMPLE_DEFINE=2' + 'AMBUILD=1' ] -def additional_includes(context, binary, sdk): +def additionalIncludes(context, binary, sdk): return [ # Path should be absolute only! # os.path.join(sdk['path'], 'game', 'server'), @@ -22,12 +23,40 @@ def additional_includes(context, binary, sdk): # 'D:/absolute/path/to/include/folder/' ] -def ResolveEnvPath(env, folder): +def getGitHeadPath(): + curr_source = builder.currentSourcePath + direct_head = os.path.join(curr_source, '.git', 'HEAD') + if not os.path.exists(direct_head): + return (None, None) + + git_head_path = None + with open(direct_head, 'r') as fp: + head_contents = fp.read().strip() + if re.search('^[a-fA-F0-9]{40}$', head_contents): + git_head_path = direct_head + else: + git_state = head_contents.split(':')[1].strip() + git_head_path = os.path.join(curr_source, '.git', git_state) + if not os.path.exists(git_head_path): + git_head_path = direct_head + + return (direct_head, git_head_path) + +def runAndReturn(argv): + p = subprocess.Popen(argv, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd = builder.currentSourcePath) + output, ignored = p.communicate() + rval = p.poll() + if rval: + raise subprocess.CalledProcessError(rval, argv) + text = output.decode('utf8') + return text.strip() + +def resolveEnvPath(env, folder = None): if env in os.environ: path = os.environ[env] if os.path.isdir(path): return path - else: + elif folder is not None: head = os.getcwd() oldhead = None while head != None and head != oldhead: @@ -38,34 +67,39 @@ def ResolveEnvPath(env, folder): head, tail = os.path.split(head) return None -def ResolveMMSRoot(): +def resolveMMSRoot(): prenormalized_path = None if builder.options.mms_path: prenormalized_path = builder.options.mms_path else: - prenormalized_path = ResolveEnvPath('MMSOURCE20', 'mmsource-2.0') - if not prenormalized_path: - prenormalized_path = ResolveEnvPath('MMSOURCE112', 'mmsource-1.12') - if not prenormalized_path: - prenormalized_path = ResolveEnvPath('MMSOURCE111', 'mmsource-1.11') - if not prenormalized_path: - prenormalized_path = ResolveEnvPath('MMSOURCE110', 'mmsource-1.10') + prenormalized_path = resolveEnvPath('MMSOURCE20', 'mmsource-2.0') if not prenormalized_path: - prenormalized_path = ResolveEnvPath('MMSOURCE_DEV', 'metamod-source') + prenormalized_path = resolveEnvPath('MMSOURCE_DEV', 'metamod-source') if not prenormalized_path: - prenormalized_path = ResolveEnvPath('MMSOURCE_DEV', 'mmsource-central') + prenormalized_path = resolveEnvPath('MMSOURCE_DEV', 'mmsource-central') + if not prenormalized_path: # Legacy fallback to avoid breakages, even though 1.12 is not supported, many old dev setups are likely using this env variable + prenormalized_path = resolveEnvPath('MMSOURCE112', 'mmsource-1.12') if not prenormalized_path or not os.path.isdir(prenormalized_path): raise Exception('Could not find a source copy of Metamod:Source') return os.path.abspath(os.path.normpath(prenormalized_path)) -mms_root = ResolveMMSRoot() +mms_root = resolveMMSRoot() -if not builder.options.hl2sdk_manifests: - raise Exception('Could not find a source copy of HL2SDK manifests') -hl2sdk_manifests = builder.options.hl2sdk_manifests +def resolveHL2SDKManifestsRoot(): + prenormalized_path = None + if builder.options.hl2sdk_manifests: + prenormalized_path = builder.options.hl2sdk_manifests + else: + prenormalized_path = resolveEnvPath('HL2SDKMANIFESTS', 'hl2sdk-manifests') + if not prenormalized_path or not os.path.isdir(prenormalized_path): + raise Exception('Could not find a source copy of HL2SDK manifests') + + return os.path.abspath(os.path.normpath(prenormalized_path)) -SdkHelpers = builder.Eval(os.path.join(hl2sdk_manifests, 'SdkHelpers.ambuild'), { +hl2sdk_manifests_root = resolveHL2SDKManifestsRoot() + +SdkHelpers = builder.Eval(os.path.join(hl2sdk_manifests_root, 'SdkHelpers.ambuild'), { 'Project': 'metamod' }) @@ -76,18 +110,22 @@ class MMSPluginConfig(object): self.sdk_targets = [] self.binaries = [] self.mms_root = mms_root + self.version_header_deps = [] + self.versionlib_deps = dict() self.all_targets = [] self.target_archs = set() - if builder.options.plugin_name is not None: - self.plugin_name = builder.options.plugin_name - else: - self.plugin_name = 'sample_mm' - - if builder.options.plugin_alias is not None: - self.plugin_alias = builder.options.plugin_alias - else: - self.plugin_alias = 'sample' + self.metadata = { + 'name': 'cs2fixes', + 'alias': 'cs2fixes', + 'display_name': 'CS2Fixes', + 'description': 'A Metamod plugin with fixes and features aimed but not limited to zombie escape', + 'author': 'xen, Poggu, Vauff, Ice, lonefang, Kxnrl, tilgep, EasterLee and various contributors', + 'url': 'https://github.com/Source2ZE/CS2Fixes', + 'log_tag': 'CS2Fixes', + 'license': 'GPL v3 License', + 'version': '{{parsed-version}}', + } if builder.options.targets: target_archs = builder.options.targets.split(',') @@ -111,11 +149,15 @@ class MMSPluginConfig(object): def findSdkPath(self, sdk_name): dir_name = 'hl2sdk-{}'.format(sdk_name) - if builder.options.hl2sdk_root: - sdk_path = os.path.join(builder.options.hl2sdk_root, dir_name) + hl2sdk_root = builder.options.hl2sdk_root + if not hl2sdk_root: + hl2sdk_root = resolveEnvPath('HL2SDKROOT') + + if hl2sdk_root: + sdk_path = os.path.abspath(os.path.normpath(os.path.join(hl2sdk_root, dir_name))) if os.path.exists(sdk_path): return sdk_path - return ResolveEnvPath('HL2SDK{}'.format(sdk_name.upper()), dir_name) + return resolveEnvPath('HL2SDK{}'.format(sdk_name.upper()), dir_name) def detectSDKs(self): sdk_list = [s for s in builder.options.sdks.split(',') if s] @@ -126,17 +168,127 @@ class MMSPluginConfig(object): self.sdk_manifests = SdkHelpers.sdk_manifests self.sdk_targets = SdkHelpers.sdk_targets - if len(self.sdks) > 1: - raise Exception('Only one sdk at a time is supported, for multi-sdk approach use loader based solution.') + for sdk_target in self.sdk_targets: + if not sdk_target.sdk['source2']: + raise Exception('Only Source2 games are supported by this script.') + + def addVersioning(self, cxx): + cxx.includes += [ + os.path.join(builder.buildPath, 'versioning') + ] + + # Nasty hack to force use lib from ambuild in the vs solution + # otherwise it'll be required to compile versionlib separately as a vs solution + # which is very annoying to have + versionlib = self.versionlib_deps[cxx.target.arch] + if builder.options.generator == 'vs': + target_path = TargetSuffix(cxx.target) + cxx.postlink += [ os.path.join(builder.buildPath, 'versionlib', target_path, 'versionlib.lib') ] + cxx.sourcedeps += [ versionlib ] + else: + cxx.postlink += [ versionlib ] + + cxx.sourcedeps += self.version_header_deps + + def generateVersioningHeaders(self): + builder.SetBuildFolder('/') + + # Generate metadata plugin header, which would cause plugin recompilation if changed + self.version_header_deps += [ + builder.AddOutputFile(os.path.join('versioning', 'version_gen.h'), f""" +#ifndef _PLUGIN_METADATA_INFORMATION_H_ +#define _PLUGIN_METADATA_INFORMATION_H_ + +#define PLUGIN_NAME \"{self.metadata['name']}\" +#define PLUGIN_ALIAS \"{self.metadata['alias']}\" +#define PLUGIN_DISPLAY_NAME \"{self.metadata['display_name']}\" +#define PLUGIN_DESCRIPTION \"{self.metadata['description']}\" +#define PLUGIN_AUTHOR \"{self.metadata['author']}\" +#define PLUGIN_URL \"{self.metadata['url']}\" +#define PLUGIN_LOGTAG \"{self.metadata['log_tag']}\" +#define PLUGIN_LICENSE \"{self.metadata['license']}\" + +extern const char* PLUGIN_FULL_VERSION; + +#endif /* _PLUGIN_METADATA_INFORMATION_H_ */ + """.encode('utf-8')) + ] + + def generateVersionLib(self): + builder.SetBuildFolder('/') + + version = self.metadata['version'] + + direct_head, git_head_path = getGitHeadPath() + parsed_version = 'DEV-untracked' + if direct_head and git_head_path: + # Force track head changes to trigger recompilation + builder.AddConfigureFile(git_head_path) + builder.AddConfigureFile(direct_head) + + try: + parsed_version = runAndReturn(['git', 'describe', '--tags', '--long']) + except subprocess.SubprocessError as e: + print("git describe failed to find any tags") + try: + parsed_version = runAndReturn(['git', 'log', '--pretty=format:%h:%H', '-n', '1']) + parsed_version = parsed_version.split(':')[0] + except subprocess.SubprocessError as e: + print("git log failed to find any commit hash, falling back to placeholder version") + + if '{{parsed-version}}' in version: + version = version.replace('{{parsed-version}}', parsed_version) + + # For the version strings and git hashes compile that to a static library instead, + # to prevent a full recompilation on version/git changes which would only trigger this cpp + # and linking to be performed + versionlib_cpp = builder.AddOutputFile(os.path.join('versioning', 'versionlib.cpp'), f""" +const char *PLUGIN_FULL_VERSION = \"{version}\"; + """.encode('utf-8')) + + for cxx in self.all_targets: + lib = cxx.StaticLibrary(f'versionlib_{cxx.target.arch}') + + self.configureCXX(lib.compiler) + + # VS generator doesn't like having non string sources, but we need that for correct + # dependency tracking + if builder.options.generator == 'vs': + lib.sources += [ os.path.join(builder.buildPath, 'versioning', 'versionlib.cpp') ] + lib.compiler.sourcedeps += [ versionlib_cpp ] + else: + lib.sources += [ versionlib_cpp ] + + self.versionlib_deps[cxx.target.arch] = builder.Add(lib).binary def configure(self): for cxx in self.all_targets: if cxx.target.arch not in ['x86', 'x86_64']: raise Exception('Unknown target architecture: {0}'.format(arch)) - self.configure_cxx(cxx) + self.configureCXX(cxx) + + def configurePluginMetadata(self): + plugin_metadata_path = os.path.join(builder.sourcePath, 'plugin-metadata.json') + + if not os.path.exists(plugin_metadata_path): + with open(plugin_metadata_path, 'w') as f: + json.dump(self.metadata, f, indent = 4) + else: + obj = None + with open(plugin_metadata_path, 'r') as f: + obj = json.load(f) + self.metadata.update(obj) + + # If some fields are missing, add them back in from a sample metadata + if len(obj) < len(self.metadata): + with open(plugin_metadata_path, 'w') as f: + json.dump(self.metadata, f, indent = 4) - def configure_cxx(self, cxx): + self.generateVersioningHeaders() + self.generateVersionLib() + + def configureCXX(self, cxx): if cxx.behavior == 'gcc': cxx.defines += [ 'stricmp=strcasecmp', @@ -150,6 +302,7 @@ class MMSPluginConfig(object): '-pipe', '-fno-strict-aliasing', '-Wall', + '-Wno-sign-compare', '-Wno-uninitialized', '-Wno-unused', '-Wno-switch', @@ -157,11 +310,7 @@ class MMSPluginConfig(object): '-fPIC', ] - if cxx.family == 'clang': - cxx.cxxflags += ['-std=c++20'] - elif cxx.family == 'gcc': - cxx.cxxflags += ['-std=c++2a'] - + cxx.cxxflags += ['-std=c++20'] if (builder.options.asan != '1') and ((cxx.version >= 'gcc-4.0') or cxx.family == 'clang'): cxx.cflags += ['-fvisibility=hidden'] cxx.cxxflags += ['-fvisibility-inlines-hidden'] @@ -171,6 +320,7 @@ class MMSPluginConfig(object): '-Wno-non-virtual-dtor', '-Wno-overloaded-virtual', '-Wno-register', + '-Wno-invalid-offsetof', ] if (builder.options.asan == '1'): cxx.cxxflags += [ @@ -274,12 +424,6 @@ class MMSPluginConfig(object): elif cxx.target.platform == 'windows': cxx.defines += ['WIN32', '_WINDOWS'] - # Finish up. - # Custom defines here - cxx.defines += [ ] - # Custom includes here - cxx.includes += [ ] - def Library(self, cxx, name): binary = cxx.Library(name) return binary @@ -288,14 +432,15 @@ class MMSPluginConfig(object): binary = self.Library(compiler, name) mms_core_path = os.path.join(self.mms_root, 'core') cxx = binary.compiler + + self.addVersioning(cxx) cxx.cxxincludes += [ os.path.join(context.currentSourcePath), - os.path.join(mms_core_path), + os.path.join(mms_core_path), os.path.join(mms_core_path, 'sourcehook'), ] - defines = [] for other_sdk in self.sdk_manifests: cxx.defines += ['SE_{}={}'.format(other_sdk['define'], other_sdk['code'])] @@ -308,14 +453,16 @@ class MMSPluginConfig(object): SdkHelpers.configureCxx(context, binary, sdk) - cxx.linkflags += additional_libs(context, binary, sdk) - cxx.defines += additional_defines(context, binary, sdk) - cxx.cxxincludes += additional_includes(context, binary, sdk) + cxx.linkflags += additionalLibs(context, binary, sdk) + cxx.defines += additionalDefines(context, binary, sdk) + cxx.cxxincludes += additionalIncludes(context, binary, sdk) + context.AddConfigureFile(os.path.join(context.currentSourcePath, 'plugin-metadata.json')) return binary MMSPlugin = MMSPluginConfig() MMSPlugin.detectSDKs() +MMSPlugin.configurePluginMetadata() MMSPlugin.configure() BuildScripts = [ @@ -323,4 +470,4 @@ BuildScripts = [ 'PackageScript', ] -builder.Build(BuildScripts, { 'MMSPlugin': MMSPlugin }) \ No newline at end of file +builder.Build(BuildScripts, { 'MMSPlugin': MMSPlugin }) diff --git a/AMBuilder b/AMBuilder index 42915fddf..fe9ef3c21 100644 --- a/AMBuilder +++ b/AMBuilder @@ -2,31 +2,16 @@ import os import subprocess -MMSPlugin.plugin_name = 'cs2fixes' -MMSPlugin.plugin_alias = 'cs2fixes' - for sdk_target in MMSPlugin.sdk_targets: sdk = sdk_target.sdk cxx = sdk_target.cxx - binary = MMSPlugin.HL2Library(builder, cxx, MMSPlugin.plugin_name, sdk) - - try: - version = subprocess.check_output(['git', 'describe', '--tags', '--long']).decode('ascii').strip() - except subprocess.SubprocessError as e: - version = "1.7-dev" - print("git describe failed as there are no tags") - - print(f'Setting version to "{version}"') - - binary.compiler.defines += ['CS2FIXES_VERSION="' + version + '"'] - - if binary.compiler.behavior == 'gcc': - binary.compiler.cxxflags += ['-Wno-invalid-offsetof'] + binary = MMSPlugin.HL2Library(builder, cxx, f'{MMSPlugin.metadata["name"]}.{sdk["name"]}', sdk) binary.compiler.cxxincludes += [ os.path.join(builder.sourcePath, 'src', 'cs2_sdk'), os.path.join(builder.sourcePath, 'src', 'utils'), + os.path.join(builder.sourcePath, 'public'), os.path.join(builder.sourcePath, 'sdk', 'thirdparty', 'protobuf-3.21.8', 'src'), os.path.join(builder.sourcePath, 'vendor', 'funchook', 'include'), ] @@ -50,7 +35,6 @@ for sdk_target in MMSPlugin.sdk_targets: ] binary.sources += ['src/utils/plat_win.cpp'] - binary.sources += [ 'src/cs2fixes.cpp', 'src/mempatch.cpp', @@ -64,6 +48,7 @@ for sdk_target in MMSPlugin.sdk_targets: 'src/events.cpp', 'src/utils/entity.cpp', 'src/utils/weapon.cpp', + 'src/utils/hud_manager.cpp', 'src/cs2_sdk/entity/services.cpp', 'src/cs2_sdk/entity/ccsplayerpawn.cpp', 'src/cs2_sdk/entity/cbasemodelentity.cpp', diff --git a/CS2Fixes.vcxproj b/CS2Fixes.vcxproj index e2cd6d4fe..eb93d032c 100644 --- a/CS2Fixes.vcxproj +++ b/CS2Fixes.vcxproj @@ -124,7 +124,7 @@ NotUsing pch.h stdcpp20 - protobuf/generated;src/utils;src/cs2_sdk;sdk/public;sdk/public/tier0;sdk/game/shared;sdk/game/server;sdk/public/tier1;sdk/public/mathlib;minhook/include;$(MMSOURCE112)/core;$(MMSOURCE112)/core/sourcehook;vendor/subhook;vendor/funchook/include;sdk/public/entity2;sdk/public/game/server;sdk/thirdparty/protobuf-3.21.8/src;$(SolutionDir);%(AdditionalIncludeDirectories) + protobuf/generated;src/utils;src/cs2_sdk;sdk/public;sdk/public/tier0;sdk/game/shared;sdk/game/server;sdk/public/tier1;sdk/public/mathlib;minhook/include;$(MMSOURCE112)/core;$(MMSOURCE112)/core/sourcehook;$(MMSOURCE_DEV)/core;$(MMSOURCE_DEV)/core/sourcehook;vendor/subhook;vendor/funchook/include;sdk/public/entity2;sdk/public/game/server;sdk/thirdparty/protobuf-3.21.8/src;$(SolutionDir);%(AdditionalIncludeDirectories) %(UndefinePreprocessorDefinitions) MultiThreadedDebug true @@ -150,7 +150,7 @@ NotUsing pch.h stdcpp20 - protobuf/generated;src/utils;src/cs2_sdk;sdk/public;sdk/public/tier0;sdk/game/shared;sdk/game/server;sdk/public/tier1;sdk/public/mathlib;minhook/include;$(MMSOURCE112)/core;$(MMSOURCE112)/core/sourcehook;vendor/subhook;vendor/funchook/include;sdk/public/entity2;sdk/public/game/server;sdk/thirdparty/protobuf-3.21.8/src;$(SolutionDir);%(AdditionalIncludeDirectories) + protobuf/generated;src/utils;src/cs2_sdk;sdk/public;sdk/public/tier0;sdk/game/shared;sdk/game/server;sdk/public/tier1;sdk/public/mathlib;minhook/include;$(MMSOURCE112)/core;$(MMSOURCE112)/core/sourcehook;$(MMSOURCE_DEV)/core;$(MMSOURCE_DEV)/core/sourcehook;vendor/subhook;vendor/funchook/include;sdk/public/entity2;sdk/public/game/server;sdk/thirdparty/protobuf-3.21.8/src;$(SolutionDir);%(AdditionalIncludeDirectories) MultiThreaded %(UndefinePreprocessorDefinitions) true @@ -217,6 +217,7 @@ + @@ -293,6 +294,8 @@ + + diff --git a/CS2Fixes.vcxproj.filters b/CS2Fixes.vcxproj.filters index 8ed00a3dc..ec89d9ff7 100644 --- a/CS2Fixes.vcxproj.filters +++ b/CS2Fixes.vcxproj.filters @@ -182,6 +182,9 @@ Source Files\utils + + Source Files\utils + Source Files\cs2_sdk\entity @@ -406,5 +409,11 @@ Header Files\utils + + Header Files\utils + + + Header Files\utils + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 19b85ae7e..e50f30feb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,6 +11,6 @@ RUN git config --global --add safe.directory /app COPY ./docker-entrypoint.sh ./ ENV HL2SDKCS2=/app/source/sdk -ENV MMSOURCE112=/app/metamod-source +ENV MMSOURCE_DEV=/app/metamod-source WORKDIR /app/source CMD [ "/bin/bash", "./docker-entrypoint.sh" ] \ No newline at end of file diff --git a/PackageScript b/PackageScript index 18d7aa98f..f85e3c2c9 100644 --- a/PackageScript +++ b/PackageScript @@ -1,112 +1,173 @@ # vim: set ts=2 sw=2 tw=99 noet ft=python: import os +import glob builder.SetBuildFolder('package') -metamod_folder = builder.AddFolder(os.path.join('addons', 'metamod')) -bin_folder_path = os.path.join('addons', MMSPlugin.plugin_name, 'bin') -bin_folder = builder.AddFolder(bin_folder_path) - -for cxx in MMSPlugin.all_targets: - if cxx.target.arch == 'x86_64': - if cxx.target.platform == 'windows': - bin64_folder_path = os.path.join('addons', MMSPlugin.plugin_name, 'bin', 'win64') - bin64_folder = builder.AddFolder(bin64_folder_path) - elif cxx.target.platform == 'linux': - bin64_folder_path = os.path.join('addons', MMSPlugin.plugin_name, 'bin', 'linuxsteamrt64') - bin64_folder = builder.AddFolder(bin64_folder_path) - elif cxx.target.platform == 'mac': - bin64_folder_path = os.path.join('addons', MMSPlugin.plugin_name, 'bin', 'win64') - bin64_folder = builder.AddFolder(bin64_folder_path) +class SDKPackage: + sdk_name = None + + plugin_folder_path = None + plugin_folder = None + + metamod_path = None + metamod = None + + bin_path = None + bin = None + + def __init__(self, cxx, sdk_name): + self.sdk_name = sdk_name + + self.plugin_folder_path = os.path.join(self.sdk_name, 'addons', MMSPlugin.metadata['name']) + self.plugin_folder = builder.AddFolder(self.plugin_folder_path) + + self.metamod_path = os.path.join(self.sdk_name, 'addons', 'metamod') + self.metamod = builder.AddFolder(self.metamod_path) + + self.addBinFolder(cxx) + self.generateVDF() + + def addBinFolder(self, cxx): + platform64_path_map = { + 'windows': 'win64', + 'linux': 'linuxsteamrt64', + 'mac': 'osx64' + } + + if cxx.target.arch == 'x86_64': + self.bin_path = os.path.join(self.plugin_folder_path, 'bin', platform64_path_map[cxx.target.platform]) + else: + self.bin_path = os.path.join(self.plugin_folder_path, 'bin') + + self.bin = builder.AddFolder(self.bin_path) + + def generateVDF(self): + vdf_content = '"Metamod Plugin"\n' + vdf_content += '{\n' + vdf_content += f'\t"alias"\t"{MMSPlugin.metadata["alias"]}"\n' + vdf_content += f'\t"file"\t"{os.path.join(os.path.relpath(self.bin_path, self.sdk_name), MMSPlugin.metadata["name"])}"\n' + vdf_content += '}' + + builder.AddOutputFile(os.path.join(self.metamod_path, f'{MMSPlugin.metadata["name"]}.vdf'), vdf_content.encode('utf8')) + + def addBinary(self, binary): + (_, plugin_bin_name) = os.path.split(binary.path) + plugin_bin_ext = os.path.splitext(plugin_bin_name)[1] + + builder.AddCopy(binary, os.path.join(self.bin_path, MMSPlugin.metadata['name'] + plugin_bin_ext)) + + # Adds file relative to plugin folder + def addFile(self, file_path, result_path = None): + if not os.path.isabs(file_path): + file_path = os.path.join(builder.sourcePath, file_path) + + if result_path is None: + result_path = os.path.join(self.plugin_folder_path, os.path.basename(file_path)) + else: + result_path = os.path.join(self.plugin_folder_path, result_path) + + builder.AddFolder(os.path.dirname(result_path)) + builder.AddCopy(file_path, result_path) + + # Adds directory relative to plugins folder + def addFolder(self, folder_path, result_path = None, search_ext = '*', recursive = True): + if not os.path.isabs(folder_path): + folder_path = os.path.join(builder.sourcePath, folder_path) + + if result_path is None: + result_path = os.path.join(self.plugin_folder_path, os.path.basename(folder_path)) + + search_param = f'*.{search_ext}' + if recursive: + search_param = os.path.join('**', search_param) + + for file in glob.glob(os.path.join(folder_path, search_param), recursive = recursive): + self.addFile(file, os.path.join(result_path, os.path.relpath(file, folder_path))) + +packages = dict() + +for sdk_target in MMSPlugin.sdk_targets: + sdk = sdk_target.sdk + cxx = sdk_target.cxx + + packages[sdk['name']] = SDKPackage(cxx, sdk['name']) pdb_list = [] for task in MMSPlugin.binaries: - # This hardly assumes there's only 1 targetted platform and would be overwritten - # with whatever comes last if multiple are used! - with open(os.path.join(builder.buildPath, MMSPlugin.plugin_name + '.vdf'), 'w') as fp: - fp.write('"Metamod Plugin"\n') - fp.write('{\n') - fp.write(f'\t"alias"\t"{MMSPlugin.plugin_alias}"\n') - if task.target.arch == 'x86_64': - fp.write(f'\t"file"\t"{os.path.join(bin64_folder_path, MMSPlugin.plugin_name)}"\n') - else: - fp.write(f'\t"file"\t"{os.path.join(bin_folder_path, MMSPlugin.plugin_name)}"\n') - fp.write('}\n') + # Determine which sdk this binary belongs to since we encode it in its name + binary_filename = os.path.splitext(os.path.basename(task.binary.path))[0] + sdk_name = binary_filename.split('.')[-1] + + packages[sdk_name].addBinary(task.binary) + + if cxx.target.platform == 'windows' and task.debug: + packages[sdk_name].addBinary(task.debug) + + # Add custom stuff here + builder.AddFolder(os.path.join(packages[sdk_name].sdk_name, 'addons', MMSPlugin.metadata['name'], 'data')) + configs_folder = builder.AddFolder(os.path.join(packages[sdk_name].sdk_name, 'addons', MMSPlugin.metadata['name'], 'configs')) + zr_folder = builder.AddFolder(os.path.join(packages[sdk_name].sdk_name, 'addons', MMSPlugin.metadata['name'], 'configs', 'zr')) + ew_maps_folder = builder.AddFolder(os.path.join(packages[sdk_name].sdk_name, 'addons', MMSPlugin.metadata['name'], 'configs', 'entwatch', 'maps')) + cfg_folder = builder.AddFolder(os.path.join(packages[sdk_name].sdk_name, 'cfg', MMSPlugin.metadata['name'])) + mapcfg_folder = builder.AddFolder(os.path.join(packages[sdk_name].sdk_name, 'cfg', MMSPlugin.metadata['name'], 'maps')) + gamedata_folder = builder.AddFolder(os.path.join(packages[sdk_name].sdk_name, 'addons', MMSPlugin.metadata['name'], 'gamedata')) + builder.AddCopy(os.path.join('configs', 'admins.jsonc.example'), configs_folder) + builder.AddCopy(os.path.join('configs', 'discordbots.cfg.example'), configs_folder) + builder.AddCopy(os.path.join('configs', 'maplist.jsonc.example'), configs_folder) + builder.AddCopy(os.path.join('cfg', MMSPlugin.metadata['name'], 'cs2fixes.cfg'), cfg_folder) + builder.AddCopy(os.path.join('cfg', MMSPlugin.metadata['name'], 'maps', 'de_somemap.cfg'), mapcfg_folder) + builder.AddCopy(os.path.join('configs', 'zr', 'playerclass.jsonc.example'), zr_folder) + builder.AddCopy(os.path.join('configs', 'zr', 'weapons.cfg.example'), zr_folder) + builder.AddCopy(os.path.join('configs', 'zr', 'hitgroups.cfg.example'), zr_folder) + builder.AddCopy(os.path.join('configs', 'entwatch', 'maps', 'example_config.jsonc'), ew_maps_folder) + builder.AddCopy(os.path.join('gamedata', 'cs2fixes.games.txt'), gamedata_folder) - if task.target.arch == 'x86_64': - builder.AddCopy(task.binary, bin64_folder) - if cxx.target.platform == 'windows' and task.debug: - builder.AddCopy(task.debug, bin64_folder) - else: - builder.AddCopy(task.binary, bin_folder) + particles_cs2f_folder = builder.AddFolder(os.path.join(packages[sdk_name].sdk_name, 'particles', MMSPlugin.metadata['name'])) + builder.AddCopy(os.path.join('assets', 'particles', MMSPlugin.metadata['name'], 'admin_beacon.vpcf_c'), particles_cs2f_folder) + builder.AddCopy(os.path.join('assets', 'particles', MMSPlugin.metadata['name'], 'admin_beacon_inner.vpcf_c'), particles_cs2f_folder) + builder.AddCopy(os.path.join('assets', 'particles', MMSPlugin.metadata['name'], 'leader_defend_mark.vpcf_c'), particles_cs2f_folder) + builder.AddCopy(os.path.join('assets', 'particles', MMSPlugin.metadata['name'], 'leader_defend_mark_ground.vpcf_c'), particles_cs2f_folder) + builder.AddCopy(os.path.join('assets', 'particles', MMSPlugin.metadata['name'], 'leader_tracer.vpcf_c'), particles_cs2f_folder) + builder.AddCopy(os.path.join('assets', 'particles', MMSPlugin.metadata['name'], 'napalm_fire.vpcf_c'), particles_cs2f_folder) + builder.AddCopy(os.path.join('assets', 'particles', MMSPlugin.metadata['name'], 'simple_overlay.vpcf_c'), particles_cs2f_folder) + + materials_cs2f_folder = builder.AddFolder(os.path.join(packages[sdk_name].sdk_name, 'materials', MMSPlugin.metadata['name'])) + builder.AddCopy(os.path.join('assets', 'materials', MMSPlugin.metadata['name'], 'leader_defend_mark.vtex_c'), materials_cs2f_folder) + + soundevents_folder = builder.AddFolder(os.path.join(packages[sdk_name].sdk_name, 'soundevents')) + builder.AddCopy(os.path.join('assets', 'soundevents', 'soundevents_zr.vsndevts_c'), soundevents_folder) + + sounds_zr_folder = builder.AddFolder(os.path.join(packages[sdk_name].sdk_name, 'sounds', 'zr')) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'fz_scream1.vsnd_c'), sounds_zr_folder) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'zombie_die1.vsnd_c'), sounds_zr_folder) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'zombie_die2.vsnd_c'), sounds_zr_folder) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'zombie_die3.vsnd_c'), sounds_zr_folder) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'zombie_pain1.vsnd_c'), sounds_zr_folder) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'zombie_pain2.vsnd_c'), sounds_zr_folder) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'zombie_pain3.vsnd_c'), sounds_zr_folder) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'zombie_pain4.vsnd_c'), sounds_zr_folder) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'zombie_pain5.vsnd_c'), sounds_zr_folder) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'zombie_pain6.vsnd_c'), sounds_zr_folder) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'zombie_voice_idle1.vsnd_c'), sounds_zr_folder) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'zombie_voice_idle2.vsnd_c'), sounds_zr_folder) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'zombie_voice_idle3.vsnd_c'), sounds_zr_folder) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'zombie_voice_idle4.vsnd_c'), sounds_zr_folder) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'zombie_voice_idle5.vsnd_c'), sounds_zr_folder) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'zombie_voice_idle6.vsnd_c'), sounds_zr_folder) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'zombie_voice_idle7.vsnd_c'), sounds_zr_folder) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'zombie_voice_idle8.vsnd_c'), sounds_zr_folder) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'zombie_voice_idle9.vsnd_c'), sounds_zr_folder) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'zombie_voice_idle10.vsnd_c'), sounds_zr_folder) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'zombie_voice_idle11.vsnd_c'), sounds_zr_folder) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'zombie_voice_idle12.vsnd_c'), sounds_zr_folder) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'zombie_voice_idle13.vsnd_c'), sounds_zr_folder) + builder.AddCopy(os.path.join('assets', 'sounds', 'zr', 'zombie_voice_idle14.vsnd_c'), sounds_zr_folder) if task.debug: pdb_list.append(task.debug) -builder.AddCopy(os.path.join(builder.buildPath, MMSPlugin.plugin_name + '.vdf'), metamod_folder) - # Generate PDB info. with open(os.path.join(builder.buildPath, 'pdblog.txt'), 'wt') as fp: for line in pdb_list: - fp.write(line.path + '\n') - -# Add CS2Fixes-specific files -builder.AddFolder(os.path.join('addons', MMSPlugin.plugin_name, 'data')) -configs_folder = builder.AddFolder(os.path.join('addons', MMSPlugin.plugin_name, 'configs')) -zr_folder = builder.AddFolder(os.path.join('addons', MMSPlugin.plugin_name, 'configs', 'zr')) -ew_maps_folder = builder.AddFolder(os.path.join('addons', MMSPlugin.plugin_name, 'configs', 'entwatch', 'maps')) -cfg_folder = builder.AddFolder(os.path.join('cfg', MMSPlugin.plugin_name)) -mapcfg_folder = builder.AddFolder(os.path.join('cfg', MMSPlugin.plugin_name, 'maps')) -gamedata_folder = builder.AddFolder(os.path.join('addons', MMSPlugin.plugin_name, 'gamedata')) -builder.AddCopy(os.path.join(builder.sourcePath, 'configs', 'admins.cfg.example'), configs_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'configs', 'discordbots.cfg.example'), configs_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'configs', 'maplist.jsonc.example'), configs_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'cfg', MMSPlugin.plugin_name, 'cs2fixes.cfg'), cfg_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'cfg', MMSPlugin.plugin_name, 'maps', 'de_somemap.cfg'), mapcfg_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'configs', 'zr', 'playerclass.jsonc.example'), zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'configs', 'zr', 'weapons.cfg.example'), zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'configs', 'zr', 'hitgroups.cfg.example'), zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'configs', 'entwatch', 'maps', 'example_config.jsonc'), ew_maps_folder) -builder.AddCopy(os.path.join('gamedata', 'cs2fixes.games.txt'), gamedata_folder) - -# Add CS2Fixes-specific compiled asset files -particles_cs2f_folder = builder.AddFolder(os.path.join('particles', MMSPlugin.plugin_name)) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'particles', MMSPlugin.plugin_name, 'player_beacon.vpcf_c'), particles_cs2f_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'particles', MMSPlugin.plugin_name, 'player_beacon_tint.vpcf_c'), particles_cs2f_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'particles', MMSPlugin.plugin_name, 'leader_defend_mark.vpcf_c'), particles_cs2f_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'particles', MMSPlugin.plugin_name, 'leader_defend_mark_ground.vpcf_c'), particles_cs2f_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'particles', MMSPlugin.plugin_name, 'leader_tracer.vpcf_c'), particles_cs2f_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'particles', MMSPlugin.plugin_name, 'napalm_fire.vpcf_c'), particles_cs2f_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'particles', MMSPlugin.plugin_name, 'simple_overlay.vpcf_c'), particles_cs2f_folder) - -materials_cs2f_folder = builder.AddFolder(os.path.join('materials', MMSPlugin.plugin_name)) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'materials', MMSPlugin.plugin_name, 'leader_defend_mark.vtex_c'), materials_cs2f_folder) - -soundevents_folder = builder.AddFolder(os.path.join('soundevents')) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'soundevents', 'soundevents_zr.vsndevts_c'), soundevents_folder) - -sounds_zr_folder = builder.AddFolder(os.path.join('sounds', 'zr')) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'fz_scream1.vsnd_c'), sounds_zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'zombie_die1.vsnd_c'), sounds_zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'zombie_die2.vsnd_c'), sounds_zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'zombie_die3.vsnd_c'), sounds_zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'zombie_pain1.vsnd_c'), sounds_zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'zombie_pain2.vsnd_c'), sounds_zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'zombie_pain3.vsnd_c'), sounds_zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'zombie_pain4.vsnd_c'), sounds_zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'zombie_pain5.vsnd_c'), sounds_zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'zombie_pain6.vsnd_c'), sounds_zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'zombie_voice_idle1.vsnd_c'), sounds_zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'zombie_voice_idle2.vsnd_c'), sounds_zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'zombie_voice_idle3.vsnd_c'), sounds_zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'zombie_voice_idle4.vsnd_c'), sounds_zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'zombie_voice_idle5.vsnd_c'), sounds_zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'zombie_voice_idle6.vsnd_c'), sounds_zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'zombie_voice_idle7.vsnd_c'), sounds_zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'zombie_voice_idle8.vsnd_c'), sounds_zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'zombie_voice_idle9.vsnd_c'), sounds_zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'zombie_voice_idle10.vsnd_c'), sounds_zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'zombie_voice_idle11.vsnd_c'), sounds_zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'zombie_voice_idle12.vsnd_c'), sounds_zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'zombie_voice_idle13.vsnd_c'), sounds_zr_folder) -builder.AddCopy(os.path.join(builder.sourcePath, 'assets', 'sounds', 'zr', 'zombie_voice_idle14.vsnd_c'), sounds_zr_folder) + fp.write(line.path + '\n') \ No newline at end of file diff --git a/README.md b/README.md index 7642bfacd..2a6089ccc 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Requires Docker to be installed. Produces Linux builds only. docker compose up ``` -Copy the contents of `dockerbuild/package/` to your server's `game/csgo/` directory. +Copy the contents of `dockerbuild/package/cs2/` to your server's `game/csgo/` directory. ### Manual @@ -43,7 +43,7 @@ Copy the contents of `dockerbuild/package/` to your server's `game/csgo/` direct #### Linux ```bash -export MMSOURCE112=/path/to/metamod +export MMSOURCE_DEV=/path/to/metamod export HL2SDKCS2=/path/to/sdk/submodule mkdir build && cd build @@ -56,7 +56,7 @@ ambuild Make sure to run in "x64 Native Tools Command Prompt for VS" ```bash -set MMSOURCE112=\path\to\metamod +set MMSOURCE_DEV=\path\to\metamod set HL2SDKCS2=\path\to\sdk\submodule mkdir build && cd build @@ -64,4 +64,4 @@ py ../configure.py --enable-optimize --sdks cs2 ambuild ``` -Copy the contents of `build/package/` to your server's `game/csgo/` directory. +Copy the contents of `build/package/cs2/` to your server's `game/csgo/` directory. diff --git a/assets/particles/cs2fixes/admin_beacon.vpcf_c b/assets/particles/cs2fixes/admin_beacon.vpcf_c new file mode 100644 index 000000000..9399a6ad1 Binary files /dev/null and b/assets/particles/cs2fixes/admin_beacon.vpcf_c differ diff --git a/assets/particles/cs2fixes/admin_beacon_inner.vpcf_c b/assets/particles/cs2fixes/admin_beacon_inner.vpcf_c new file mode 100644 index 000000000..db4494aae Binary files /dev/null and b/assets/particles/cs2fixes/admin_beacon_inner.vpcf_c differ diff --git a/assets/particles/cs2fixes/player_beacon.vpcf_c b/assets/particles/cs2fixes/player_beacon.vpcf_c deleted file mode 100644 index d3dcf6191..000000000 Binary files a/assets/particles/cs2fixes/player_beacon.vpcf_c and /dev/null differ diff --git a/assets/particles/cs2fixes/player_beacon_tint.vpcf_c b/assets/particles/cs2fixes/player_beacon_tint.vpcf_c deleted file mode 100644 index 1fae25d7b..000000000 Binary files a/assets/particles/cs2fixes/player_beacon_tint.vpcf_c and /dev/null differ diff --git a/assets_source/particles/cs2fixes/player_beacon_tint.vpcf b/assets_source/particles/cs2fixes/admin_beacon.vpcf similarity index 65% rename from assets_source/particles/cs2fixes/player_beacon_tint.vpcf rename to assets_source/particles/cs2fixes/admin_beacon.vpcf index e34f57866..4c09e2cdb 100644 --- a/assets_source/particles/cs2fixes/player_beacon_tint.vpcf +++ b/assets_source/particles/cs2fixes/admin_beacon.vpcf @@ -1,8 +1,8 @@ - + { _class = "CParticleSystemDefinition" m_nBehaviorVersion = 12 - m_nMaxParticles = 32 + m_nMaxParticles = 16 m_controlPointConfigurations = [ { @@ -133,7 +133,7 @@ { m_nType = "PF_TYPE_LITERAL" m_nMapType = "PF_MAP_TYPE_DIRECT" - m_flLiteralValue = 8.0 + m_flLiteralValue = 15.0 m_NamedValue = "" m_nControlPoint = 0 m_nScalarAttribute = 3 @@ -188,7 +188,7 @@ { m_nType = "PF_TYPE_LITERAL" m_nMapType = "PF_MAP_TYPE_DIRECT" - m_flLiteralValue = 230.0 + m_flLiteralValue = 250.0 m_NamedValue = "" m_nControlPoint = 0 m_nScalarAttribute = 3 @@ -243,7 +243,7 @@ { m_nType = "PF_TYPE_LITERAL" m_nMapType = "PF_MAP_TYPE_DIRECT" - m_flLiteralValue = 230.0 + m_flLiteralValue = 250.0 m_NamedValue = "" m_nControlPoint = 0 m_nScalarAttribute = 3 @@ -308,8 +308,8 @@ m_nScalarAttribute = 3 m_nVectorAttribute = 6 m_nVectorComponent = 0 - m_flRandomMin = 1.0 - m_flRandomMax = 1.0 + m_flRandomMin = 0.45 + m_flRandomMax = 0.45 m_bHasRandomSignFlip = false m_nRandomSeed = -1 m_nRandomMode = "PF_RANDOM_MODE_CONSTANT" @@ -367,17 +367,6 @@ { _class = "C_OP_PositionLock" }, - { - _class = "C_OP_FadeAndKill" - m_flEndFadeInTime = 0.2 - m_flStartFadeOutTime = 0.35 - m_flEndFadeOutTime = 0.75 - m_flEndAlpha = -0.3 - }, - { - _class = "C_OP_DampenToCP" - m_flScale = 0.02 - }, ] m_Renderers = [ @@ -388,7 +377,7 @@ { m_nType = "PF_TYPE_LITERAL" m_nMapType = "PF_MAP_TYPE_DIRECT" - m_flLiteralValue = 1.4 + m_flLiteralValue = 1.3 m_NamedValue = "" m_nControlPoint = 0 m_nScalarAttribute = 3 @@ -439,250 +428,60 @@ m_vDomainMaxs = [ 0.0, 0.0 ] } } - m_vecColorScale = + m_flAlphaScale = { - m_nType = "PVEC_TYPE_CP_VALUE" - m_vLiteralValue = [ 0.0, 0.0, 0.0 ] - m_LiteralColor = [ 255, 255, 255 ] + m_nType = "PF_TYPE_LITERAL" + m_nMapType = "PF_MAP_TYPE_DIRECT" + m_flLiteralValue = 0.8 m_NamedValue = "" - m_bFollowNamedValue = false + m_nControlPoint = 0 + m_nScalarAttribute = 3 m_nVectorAttribute = 6 - m_vVectorAttributeScale = [ 1.0, 1.0, 1.0 ] - m_nControlPoint = 1 - m_nDeltaControlPoint = 0 - m_vCPValueScale = [ 0.0039, 0.0039, 0.0039 ] - m_vCPRelativePosition = [ 0.0, 0.0, 0.0 ] - m_vCPRelativeDir = [ 1.0, 0.0, 0.0 ] - m_FloatComponentX = - { - m_nType = "PF_TYPE_LITERAL" - m_nMapType = "PF_MAP_TYPE_DIRECT" - m_flLiteralValue = 0.0 - m_NamedValue = "" - m_nControlPoint = 0 - m_nScalarAttribute = 3 - m_nVectorAttribute = 6 - m_nVectorComponent = 0 - m_flRandomMin = 0.0 - m_flRandomMax = 1.0 - m_bHasRandomSignFlip = false - m_nRandomSeed = -1 - m_nRandomMode = "PF_RANDOM_MODE_CONSTANT" - m_flLOD0 = 0.0 - m_flLOD1 = 0.0 - m_flLOD2 = 0.0 - m_flLOD3 = 0.0 - m_nNoiseInputVectorAttribute = 0 - m_flNoiseOutputMin = 0.0 - m_flNoiseOutputMax = 1.0 - m_flNoiseScale = 0.1 - m_vecNoiseOffsetRate = [ 0.0, 0.0, 0.0 ] - m_flNoiseOffset = 0.0 - m_nNoiseOctaves = 1 - m_nNoiseTurbulence = "PF_NOISE_TURB_NONE" - m_nNoiseType = "PF_NOISE_TYPE_PERLIN" - m_nNoiseModifier = "PF_NOISE_MODIFIER_NONE" - m_flNoiseTurbulenceScale = 1.0 - m_flNoiseTurbulenceMix = 0.5 - m_flNoiseImgPreviewScale = 1.0 - m_bNoiseImgPreviewLive = true - m_flNoCameraFallback = 0.0 - m_bUseBoundsCenter = false - m_nInputMode = "PF_INPUT_MODE_CLAMPED" - m_flMultFactor = 1.0 - m_flInput0 = 0.0 - m_flInput1 = 1.0 - m_flOutput0 = 0.0 - m_flOutput1 = 1.0 - m_flNotchedRangeMin = 0.0 - m_flNotchedRangeMax = 1.0 - m_flNotchedOutputOutside = 0.0 - m_flNotchedOutputInside = 1.0 - m_nBiasType = "PF_BIAS_TYPE_STANDARD" - m_flBiasParameter = 0.0 - m_Curve = - { - m_spline = [ ] - m_tangents = [ ] - m_vDomainMins = [ 0.0, 0.0 ] - m_vDomainMaxs = [ 0.0, 0.0 ] - } - } - m_FloatComponentY = - { - m_nType = "PF_TYPE_LITERAL" - m_nMapType = "PF_MAP_TYPE_DIRECT" - m_flLiteralValue = 0.0 - m_NamedValue = "" - m_nControlPoint = 0 - m_nScalarAttribute = 3 - m_nVectorAttribute = 6 - m_nVectorComponent = 0 - m_flRandomMin = 0.0 - m_flRandomMax = 1.0 - m_bHasRandomSignFlip = false - m_nRandomSeed = -1 - m_nRandomMode = "PF_RANDOM_MODE_CONSTANT" - m_flLOD0 = 0.0 - m_flLOD1 = 0.0 - m_flLOD2 = 0.0 - m_flLOD3 = 0.0 - m_nNoiseInputVectorAttribute = 0 - m_flNoiseOutputMin = 0.0 - m_flNoiseOutputMax = 1.0 - m_flNoiseScale = 0.1 - m_vecNoiseOffsetRate = [ 0.0, 0.0, 0.0 ] - m_flNoiseOffset = 0.0 - m_nNoiseOctaves = 1 - m_nNoiseTurbulence = "PF_NOISE_TURB_NONE" - m_nNoiseType = "PF_NOISE_TYPE_PERLIN" - m_nNoiseModifier = "PF_NOISE_MODIFIER_NONE" - m_flNoiseTurbulenceScale = 1.0 - m_flNoiseTurbulenceMix = 0.5 - m_flNoiseImgPreviewScale = 1.0 - m_bNoiseImgPreviewLive = true - m_flNoCameraFallback = 0.0 - m_bUseBoundsCenter = false - m_nInputMode = "PF_INPUT_MODE_CLAMPED" - m_flMultFactor = 1.0 - m_flInput0 = 0.0 - m_flInput1 = 1.0 - m_flOutput0 = 0.0 - m_flOutput1 = 1.0 - m_flNotchedRangeMin = 0.0 - m_flNotchedRangeMax = 1.0 - m_flNotchedOutputOutside = 0.0 - m_flNotchedOutputInside = 1.0 - m_nBiasType = "PF_BIAS_TYPE_STANDARD" - m_flBiasParameter = 0.0 - m_Curve = - { - m_spline = [ ] - m_tangents = [ ] - m_vDomainMins = [ 0.0, 0.0 ] - m_vDomainMaxs = [ 0.0, 0.0 ] - } - } - m_FloatComponentZ = - { - m_nType = "PF_TYPE_LITERAL" - m_nMapType = "PF_MAP_TYPE_DIRECT" - m_flLiteralValue = 0.0 - m_NamedValue = "" - m_nControlPoint = 0 - m_nScalarAttribute = 3 - m_nVectorAttribute = 6 - m_nVectorComponent = 0 - m_flRandomMin = 0.0 - m_flRandomMax = 1.0 - m_bHasRandomSignFlip = false - m_nRandomSeed = -1 - m_nRandomMode = "PF_RANDOM_MODE_CONSTANT" - m_flLOD0 = 0.0 - m_flLOD1 = 0.0 - m_flLOD2 = 0.0 - m_flLOD3 = 0.0 - m_nNoiseInputVectorAttribute = 0 - m_flNoiseOutputMin = 0.0 - m_flNoiseOutputMax = 1.0 - m_flNoiseScale = 0.1 - m_vecNoiseOffsetRate = [ 0.0, 0.0, 0.0 ] - m_flNoiseOffset = 0.0 - m_nNoiseOctaves = 1 - m_nNoiseTurbulence = "PF_NOISE_TURB_NONE" - m_nNoiseType = "PF_NOISE_TYPE_PERLIN" - m_nNoiseModifier = "PF_NOISE_MODIFIER_NONE" - m_flNoiseTurbulenceScale = 1.0 - m_flNoiseTurbulenceMix = 0.5 - m_flNoiseImgPreviewScale = 1.0 - m_bNoiseImgPreviewLive = true - m_flNoCameraFallback = 0.0 - m_bUseBoundsCenter = false - m_nInputMode = "PF_INPUT_MODE_CLAMPED" - m_flMultFactor = 1.0 - m_flInput0 = 0.0 - m_flInput1 = 1.0 - m_flOutput0 = 0.0 - m_flOutput1 = 1.0 - m_flNotchedRangeMin = 0.0 - m_flNotchedRangeMax = 1.0 - m_flNotchedOutputOutside = 0.0 - m_flNotchedOutputInside = 1.0 - m_nBiasType = "PF_BIAS_TYPE_STANDARD" - m_flBiasParameter = 0.0 - m_Curve = - { - m_spline = [ ] - m_tangents = [ ] - m_vDomainMins = [ 0.0, 0.0 ] - m_vDomainMaxs = [ 0.0, 0.0 ] - } - } - m_FloatInterp = - { - m_nType = "PF_TYPE_LITERAL" - m_nMapType = "PF_MAP_TYPE_DIRECT" - m_flLiteralValue = 0.0 - m_NamedValue = "" - m_nControlPoint = 0 - m_nScalarAttribute = 3 - m_nVectorAttribute = 6 - m_nVectorComponent = 0 - m_flRandomMin = 0.0 - m_flRandomMax = 1.0 - m_bHasRandomSignFlip = false - m_nRandomSeed = -1 - m_nRandomMode = "PF_RANDOM_MODE_CONSTANT" - m_flLOD0 = 0.0 - m_flLOD1 = 0.0 - m_flLOD2 = 0.0 - m_flLOD3 = 0.0 - m_nNoiseInputVectorAttribute = 0 - m_flNoiseOutputMin = 0.0 - m_flNoiseOutputMax = 1.0 - m_flNoiseScale = 0.1 - m_vecNoiseOffsetRate = [ 0.0, 0.0, 0.0 ] - m_flNoiseOffset = 0.0 - m_nNoiseOctaves = 1 - m_nNoiseTurbulence = "PF_NOISE_TURB_NONE" - m_nNoiseType = "PF_NOISE_TYPE_PERLIN" - m_nNoiseModifier = "PF_NOISE_MODIFIER_NONE" - m_flNoiseTurbulenceScale = 1.0 - m_flNoiseTurbulenceMix = 0.5 - m_flNoiseImgPreviewScale = 1.0 - m_bNoiseImgPreviewLive = true - m_flNoCameraFallback = 0.0 - m_bUseBoundsCenter = false - m_nInputMode = "PF_INPUT_MODE_CLAMPED" - m_flMultFactor = 1.0 - m_flInput0 = 0.0 - m_flInput1 = 1.0 - m_flOutput0 = 0.0 - m_flOutput1 = 1.0 - m_flNotchedRangeMin = 0.0 - m_flNotchedRangeMax = 1.0 - m_flNotchedOutputOutside = 0.0 - m_flNotchedOutputInside = 1.0 - m_nBiasType = "PF_BIAS_TYPE_STANDARD" - m_flBiasParameter = 0.0 - m_Curve = - { - m_spline = [ ] - m_tangents = [ ] - m_vDomainMins = [ 0.0, 0.0 ] - m_vDomainMaxs = [ 0.0, 0.0 ] - } - } - m_flInterpInput0 = 0.0 - m_flInterpInput1 = 1.0 - m_vInterpOutput0 = [ 0.0, 0.0, 0.0 ] - m_vInterpOutput1 = [ 1.0, 1.0, 1.0 ] - m_Gradient = + m_nVectorComponent = 0 + m_flRandomMin = 0.0 + m_flRandomMax = 1.0 + m_bHasRandomSignFlip = false + m_nRandomSeed = -1 + m_nRandomMode = "PF_RANDOM_MODE_CONSTANT" + m_flLOD0 = 0.0 + m_flLOD1 = 0.0 + m_flLOD2 = 0.0 + m_flLOD3 = 0.0 + m_nNoiseInputVectorAttribute = 0 + m_flNoiseOutputMin = 0.0 + m_flNoiseOutputMax = 1.0 + m_flNoiseScale = 0.1 + m_vecNoiseOffsetRate = [ 0.0, 0.0, 0.0 ] + m_flNoiseOffset = 0.0 + m_nNoiseOctaves = 1 + m_nNoiseTurbulence = "PF_NOISE_TURB_NONE" + m_nNoiseType = "PF_NOISE_TYPE_PERLIN" + m_nNoiseModifier = "PF_NOISE_MODIFIER_NONE" + m_flNoiseTurbulenceScale = 1.0 + m_flNoiseTurbulenceMix = 0.5 + m_flNoiseImgPreviewScale = 1.0 + m_bNoiseImgPreviewLive = true + m_flNoCameraFallback = 0.0 + m_bUseBoundsCenter = false + m_nInputMode = "PF_INPUT_MODE_CLAMPED" + m_flMultFactor = 1.0 + m_flInput0 = 0.0 + m_flInput1 = 1.0 + m_flOutput0 = 0.0 + m_flOutput1 = 1.0 + m_flNotchedRangeMin = 0.0 + m_flNotchedRangeMax = 1.0 + m_flNotchedOutputOutside = 0.0 + m_flNotchedOutputInside = 1.0 + m_nBiasType = "PF_BIAS_TYPE_STANDARD" + m_flBiasParameter = 0.0 + m_Curve = { - m_Stops = [ ] + m_spline = [ ] + m_tangents = [ ] + m_vDomainMins = [ 0.0, 0.0 ] + m_vDomainMaxs = [ 0.0, 0.0 ] } - m_vRandomMin = [ 0.0, 0.0, 0.0 ] - m_vRandomMax = [ 0.0, 0.0, 0.0 ] } m_vecTexturesInput = [ @@ -694,7 +493,7 @@ { m_nType = "PF_TYPE_LITERAL" m_nMapType = "PF_MAP_TYPE_DIRECT" - m_flLiteralValue = 4.0 + m_flLiteralValue = 2.0 m_NamedValue = "" m_nControlPoint = 0 m_nScalarAttribute = 3 @@ -801,7 +600,12 @@ m_vDomainMaxs = [ 0.0, 0.0 ] } } - m_nColorBlendType = "PARTICLE_COLOR_BLEND_REPLACE" + }, + ] + m_Children = + [ + { + m_ChildRef = resource:"particles/cs2fixes/admin_beacon_inner.vpcf" }, ] } \ No newline at end of file diff --git a/assets_source/particles/cs2fixes/player_beacon.vpcf b/assets_source/particles/cs2fixes/admin_beacon_inner.vpcf similarity index 96% rename from assets_source/particles/cs2fixes/player_beacon.vpcf rename to assets_source/particles/cs2fixes/admin_beacon_inner.vpcf index fd8940901..ccf2e4a4a 100644 --- a/assets_source/particles/cs2fixes/player_beacon.vpcf +++ b/assets_source/particles/cs2fixes/admin_beacon_inner.vpcf @@ -1,8 +1,8 @@ - + { _class = "CParticleSystemDefinition" m_nBehaviorVersion = 12 - m_nMaxParticles = 32 + m_nMaxParticles = 16 m_controlPointConfigurations = [ { @@ -133,7 +133,7 @@ { m_nType = "PF_TYPE_LITERAL" m_nMapType = "PF_MAP_TYPE_DIRECT" - m_flLiteralValue = 15.0 + m_flLiteralValue = 8.0 m_NamedValue = "" m_nControlPoint = 0 m_nScalarAttribute = 3 @@ -188,7 +188,7 @@ { m_nType = "PF_TYPE_LITERAL" m_nMapType = "PF_MAP_TYPE_DIRECT" - m_flLiteralValue = 200.0 + m_flLiteralValue = 250.0 m_NamedValue = "" m_nControlPoint = 0 m_nScalarAttribute = 3 @@ -243,7 +243,7 @@ { m_nType = "PF_TYPE_LITERAL" m_nMapType = "PF_MAP_TYPE_DIRECT" - m_flLiteralValue = 200.0 + m_flLiteralValue = 250.0 m_NamedValue = "" m_nControlPoint = 0 m_nScalarAttribute = 3 @@ -308,8 +308,8 @@ m_nScalarAttribute = 3 m_nVectorAttribute = 6 m_nVectorComponent = 0 - m_flRandomMin = 1.0 - m_flRandomMax = 1.0 + m_flRandomMin = 0.45 + m_flRandomMax = 0.45 m_bHasRandomSignFlip = false m_nRandomSeed = -1 m_nRandomMode = "PF_RANDOM_MODE_CONSTANT" @@ -358,29 +358,14 @@ ] m_Operators = [ - { - _class = "C_OP_RestartAfterDuration" - m_flDurationMin = 1.0 - }, { _class = "C_OP_Decay" }, - { - _class = "C_OP_FadeAndKill" - m_flEndFadeInTime = 0.15 - m_flStartFadeOutTime = 0.25 - m_flEndFadeOutTime = 0.6 - m_flEndAlpha = -0.1 - }, - { - _class = "C_OP_PositionLock" - }, { _class = "C_OP_BasicMovement" }, { - _class = "C_OP_DampenToCP" - m_flScale = 0.06 + _class = "C_OP_PositionLock" }, ] m_Renderers = @@ -392,124 +377,7 @@ { m_nType = "PF_TYPE_LITERAL" m_nMapType = "PF_MAP_TYPE_DIRECT" - m_flLiteralValue = 1.6 - m_NamedValue = "" - m_nControlPoint = 0 - m_nScalarAttribute = 3 - m_nVectorAttribute = 6 - m_nVectorComponent = 0 - m_flRandomMin = 0.0 - m_flRandomMax = 1.0 - m_bHasRandomSignFlip = false - m_nRandomSeed = -1 - m_nRandomMode = "PF_RANDOM_MODE_CONSTANT" - m_flLOD0 = 0.0 - m_flLOD1 = 0.0 - m_flLOD2 = 0.0 - m_flLOD3 = 0.0 - m_nNoiseInputVectorAttribute = 0 - m_flNoiseOutputMin = 0.0 - m_flNoiseOutputMax = 1.0 - m_flNoiseScale = 0.1 - m_vecNoiseOffsetRate = [ 0.0, 0.0, 0.0 ] - m_flNoiseOffset = 0.0 - m_nNoiseOctaves = 1 - m_nNoiseTurbulence = "PF_NOISE_TURB_NONE" - m_nNoiseType = "PF_NOISE_TYPE_PERLIN" - m_nNoiseModifier = "PF_NOISE_MODIFIER_NONE" - m_flNoiseTurbulenceScale = 1.0 - m_flNoiseTurbulenceMix = 0.5 - m_flNoiseImgPreviewScale = 1.0 - m_bNoiseImgPreviewLive = true - m_flNoCameraFallback = 0.0 - m_bUseBoundsCenter = false - m_nInputMode = "PF_INPUT_MODE_CLAMPED" - m_flMultFactor = 1.0 - m_flInput0 = 0.0 - m_flInput1 = 1.0 - m_flOutput0 = 0.0 - m_flOutput1 = 1.0 - m_flNotchedRangeMin = 0.0 - m_flNotchedRangeMax = 1.0 - m_flNotchedOutputOutside = 0.0 - m_flNotchedOutputInside = 1.0 - m_nBiasType = "PF_BIAS_TYPE_STANDARD" - m_flBiasParameter = 0.0 - m_Curve = - { - m_spline = [ ] - m_tangents = [ ] - m_vDomainMins = [ 0.0, 0.0 ] - m_vDomainMaxs = [ 0.0, 0.0 ] - } - } - m_vecTexturesInput = - [ - { - m_hTexture = resource:"materials/particle/base_rope.vtex" - }, - ] - m_flSelfIllumAmount = - { - m_nType = "PF_TYPE_LITERAL" - m_nMapType = "PF_MAP_TYPE_DIRECT" - m_flLiteralValue = 4.0 - m_NamedValue = "" - m_nControlPoint = 0 - m_nScalarAttribute = 3 - m_nVectorAttribute = 6 - m_nVectorComponent = 0 - m_flRandomMin = 0.0 - m_flRandomMax = 1.0 - m_bHasRandomSignFlip = false - m_nRandomSeed = -1 - m_nRandomMode = "PF_RANDOM_MODE_CONSTANT" - m_flLOD0 = 0.0 - m_flLOD1 = 0.0 - m_flLOD2 = 0.0 - m_flLOD3 = 0.0 - m_nNoiseInputVectorAttribute = 0 - m_flNoiseOutputMin = 0.0 - m_flNoiseOutputMax = 1.0 - m_flNoiseScale = 0.1 - m_vecNoiseOffsetRate = [ 0.0, 0.0, 0.0 ] - m_flNoiseOffset = 0.0 - m_nNoiseOctaves = 1 - m_nNoiseTurbulence = "PF_NOISE_TURB_NONE" - m_nNoiseType = "PF_NOISE_TYPE_PERLIN" - m_nNoiseModifier = "PF_NOISE_MODIFIER_NONE" - m_flNoiseTurbulenceScale = 1.0 - m_flNoiseTurbulenceMix = 0.5 - m_flNoiseImgPreviewScale = 1.0 - m_bNoiseImgPreviewLive = true - m_flNoCameraFallback = 0.0 - m_bUseBoundsCenter = false - m_nInputMode = "PF_INPUT_MODE_CLAMPED" - m_flMultFactor = 1.0 - m_flInput0 = 0.0 - m_flInput1 = 1.0 - m_flOutput0 = 0.0 - m_flOutput1 = 1.0 - m_flNotchedRangeMin = 0.0 - m_flNotchedRangeMax = 1.0 - m_flNotchedOutputOutside = 0.0 - m_flNotchedOutputInside = 1.0 - m_nBiasType = "PF_BIAS_TYPE_STANDARD" - m_flBiasParameter = 0.0 - m_Curve = - { - m_spline = [ ] - m_tangents = [ ] - m_vDomainMins = [ 0.0, 0.0 ] - m_vDomainMaxs = [ 0.0, 0.0 ] - } - } - m_bOnlyRenderInEffecsGameOverlay = true - m_flDiffuseAmount = - { - m_nType = "PF_TYPE_LITERAL" - m_nMapType = "PF_MAP_TYPE_DIRECT" - m_flLiteralValue = 0.5 + m_flLiteralValue = 2.0 m_NamedValue = "" m_nControlPoint = 0 m_nScalarAttribute = 3 @@ -564,7 +432,7 @@ { m_nType = "PF_TYPE_LITERAL" m_nMapType = "PF_MAP_TYPE_DIRECT" - m_flLiteralValue = 0.8 + m_flLiteralValue = 0.7 m_NamedValue = "" m_nControlPoint = 0 m_nScalarAttribute = 3 @@ -617,16 +485,16 @@ } m_vecColorScale = { - m_nType = "PVEC_TYPE_LITERAL_COLOR" + m_nType = "PVEC_TYPE_CP_VALUE" m_vLiteralValue = [ 0.0, 0.0, 0.0 ] - m_LiteralColor = [ 200, 200, 200 ] + m_LiteralColor = [ 255, 255, 255 ] m_NamedValue = "" m_bFollowNamedValue = false m_nVectorAttribute = 6 m_vVectorAttributeScale = [ 1.0, 1.0, 1.0 ] - m_nControlPoint = 0 + m_nControlPoint = 1 m_nDeltaControlPoint = 0 - m_vCPValueScale = [ 1.0, 1.0, 1.0 ] + m_vCPValueScale = [ 0.0039, 0.0039, 0.0039 ] m_vCPRelativePosition = [ 0.0, 0.0, 0.0 ] m_vCPRelativeDir = [ 1.0, 0.0, 0.0 ] m_FloatComponentX = @@ -860,13 +728,123 @@ m_vRandomMin = [ 0.0, 0.0, 0.0 ] m_vRandomMax = [ 0.0, 0.0, 0.0 ] } - m_nVectorFieldForOrientation = 0 - }, - ] - m_Children = - [ - { - m_ChildRef = resource:"particles/cs2fixes/player_beacon_tint.vpcf" + m_vecTexturesInput = + [ + { + m_hTexture = resource:"materials/particle/base_rope.vtex" + }, + ] + m_flSelfIllumAmount = + { + m_nType = "PF_TYPE_LITERAL" + m_nMapType = "PF_MAP_TYPE_DIRECT" + m_flLiteralValue = 2.0 + m_NamedValue = "" + m_nControlPoint = 0 + m_nScalarAttribute = 3 + m_nVectorAttribute = 6 + m_nVectorComponent = 0 + m_flRandomMin = 0.0 + m_flRandomMax = 1.0 + m_bHasRandomSignFlip = false + m_nRandomSeed = -1 + m_nRandomMode = "PF_RANDOM_MODE_CONSTANT" + m_flLOD0 = 0.0 + m_flLOD1 = 0.0 + m_flLOD2 = 0.0 + m_flLOD3 = 0.0 + m_nNoiseInputVectorAttribute = 0 + m_flNoiseOutputMin = 0.0 + m_flNoiseOutputMax = 1.0 + m_flNoiseScale = 0.1 + m_vecNoiseOffsetRate = [ 0.0, 0.0, 0.0 ] + m_flNoiseOffset = 0.0 + m_nNoiseOctaves = 1 + m_nNoiseTurbulence = "PF_NOISE_TURB_NONE" + m_nNoiseType = "PF_NOISE_TYPE_PERLIN" + m_nNoiseModifier = "PF_NOISE_MODIFIER_NONE" + m_flNoiseTurbulenceScale = 1.0 + m_flNoiseTurbulenceMix = 0.5 + m_flNoiseImgPreviewScale = 1.0 + m_bNoiseImgPreviewLive = true + m_flNoCameraFallback = 0.0 + m_bUseBoundsCenter = false + m_nInputMode = "PF_INPUT_MODE_CLAMPED" + m_flMultFactor = 1.0 + m_flInput0 = 0.0 + m_flInput1 = 1.0 + m_flOutput0 = 0.0 + m_flOutput1 = 1.0 + m_flNotchedRangeMin = 0.0 + m_flNotchedRangeMax = 1.0 + m_flNotchedOutputOutside = 0.0 + m_flNotchedOutputInside = 1.0 + m_nBiasType = "PF_BIAS_TYPE_STANDARD" + m_flBiasParameter = 0.0 + m_Curve = + { + m_spline = [ ] + m_tangents = [ ] + m_vDomainMins = [ 0.0, 0.0 ] + m_vDomainMaxs = [ 0.0, 0.0 ] + } + } + m_bOnlyRenderInEffecsGameOverlay = true + m_flDiffuseAmount = + { + m_nType = "PF_TYPE_LITERAL" + m_nMapType = "PF_MAP_TYPE_DIRECT" + m_flLiteralValue = 0.5 + m_NamedValue = "" + m_nControlPoint = 0 + m_nScalarAttribute = 3 + m_nVectorAttribute = 6 + m_nVectorComponent = 0 + m_flRandomMin = 0.0 + m_flRandomMax = 1.0 + m_bHasRandomSignFlip = false + m_nRandomSeed = -1 + m_nRandomMode = "PF_RANDOM_MODE_CONSTANT" + m_flLOD0 = 0.0 + m_flLOD1 = 0.0 + m_flLOD2 = 0.0 + m_flLOD3 = 0.0 + m_nNoiseInputVectorAttribute = 0 + m_flNoiseOutputMin = 0.0 + m_flNoiseOutputMax = 1.0 + m_flNoiseScale = 0.1 + m_vecNoiseOffsetRate = [ 0.0, 0.0, 0.0 ] + m_flNoiseOffset = 0.0 + m_nNoiseOctaves = 1 + m_nNoiseTurbulence = "PF_NOISE_TURB_NONE" + m_nNoiseType = "PF_NOISE_TYPE_PERLIN" + m_nNoiseModifier = "PF_NOISE_MODIFIER_NONE" + m_flNoiseTurbulenceScale = 1.0 + m_flNoiseTurbulenceMix = 0.5 + m_flNoiseImgPreviewScale = 1.0 + m_bNoiseImgPreviewLive = true + m_flNoCameraFallback = 0.0 + m_bUseBoundsCenter = false + m_nInputMode = "PF_INPUT_MODE_CLAMPED" + m_flMultFactor = 1.0 + m_flInput0 = 0.0 + m_flInput1 = 1.0 + m_flOutput0 = 0.0 + m_flOutput1 = 1.0 + m_flNotchedRangeMin = 0.0 + m_flNotchedRangeMax = 1.0 + m_flNotchedOutputOutside = 0.0 + m_flNotchedOutputInside = 1.0 + m_nBiasType = "PF_BIAS_TYPE_STANDARD" + m_flBiasParameter = 0.0 + m_Curve = + { + m_spline = [ ] + m_tangents = [ ] + m_vDomainMins = [ 0.0, 0.0 ] + m_vDomainMaxs = [ 0.0, 0.0 ] + } + } }, ] } \ No newline at end of file diff --git a/cfg/cs2fixes/cs2fixes.cfg b/cfg/cs2fixes/cs2fixes.cfg index f52628e12..63d9eaf49 100644 --- a/cfg/cs2fixes/cs2fixes.cfg +++ b/cfg/cs2fixes/cs2fixes.cfg @@ -25,8 +25,10 @@ cs2f_shuffle_player_physics_sim 0 // Whether to enable shuffle player list in p cs2f_prevent_using_players 0 // Whether to prevent +use from hitting players (0=can use players, 1=cannot use players) cs2f_map_steamids_enable 0 // Whether to make Steam ID's available to maps cs2f_fix_game_bans 0 // Whether to fix CS2 game bans spreading to all new joining players +cs2f_free_armor 0 // Whether kevlar (1+) and/or helmet (2) are given automatically +cs2f_fix_hud_flashing 0 // Whether to fix hud flashing using a workaround, this BREAKS warmup so pick one or the other -cs2f_beacon_particle "particles/cs2fixes/player_beacon.vpcf" // .vpcf file to be precached and used for player beacon +cs2f_beacon_particle "particles/cs2fixes/admin_beacon.vpcf" // .vpcf file to be precached and used for player beacon cs2f_delay_auth_fail_kick 0 // How long in seconds to delay kicking players when their Steam authentication fails, use with sv_steamauth_enforce 0 @@ -93,6 +95,7 @@ zr_knockback_scale 5.0 // Global knockback scale zr_ztele_max_distance 150.0 // Maximum distance players are allowed to move after starting ztele zr_ztele_allow_humans 0 // Whether to allow humans to use ztele zr_infect_spawn_type 1 // Type of Mother Zombies Spawn [0 = MZ spawn where they stand, 1 = MZ get teleported back to spawn on being picked] +zr_infect_spawn_warning 1 // Whether to warn players of zombies spawning between humans zr_infect_spawn_time_min 15 // Minimum time in which Mother Zombies should be picked, after round start zr_infect_spawn_time_max 15 // Maximum time in which Mother Zombies should be picked, after round start zr_infect_spawn_mz_ratio 7 // Ratio of all Players to Mother Zombies to be spawned at round start diff --git a/configs/admins.cfg.example b/configs/admins.cfg.example deleted file mode 100644 index daa256e75..000000000 --- a/configs/admins.cfg.example +++ /dev/null @@ -1,37 +0,0 @@ -//--------------------- -// Permission flags -//--------------------- -// a : Reserved slots -// b : Generic admin, required for admins -// c : Kick other players -// d : Banning other players -// e : Removing bans -// f : Slaying other players -// g : Changing the map -// h : Changing cvars -// i : Changing configs -// j : Special chat privileges -// k : Voting -// l : Password the server -// m : Remote console -// n : Change sv_cheats and related commands -// z : Root. It grants ALL flags so use with caution! - -// Flags 'o' to 'y' are custom flags (currently unused) - -// cs2f_admin_immunity 'convar' controls how targetting works with "immunity" KV -// 0 - Commands using immunity targetting can only target players with immunities LOWER than the user's -// 1 - Commands using immunity targetting can only target players with immunities EQUAL TO OR LOWER than the user's -// 2 - Commands ignore immunity levels - -Admins -{ - // Admin entries should follow this format - - //"name" // Unused, can be anything - //{ - // "steamid" "1234567890" // SteamID64 - // "flags" "abcdefg" // Permission flags as described above - // "immunity" "1" // Non-negative value. - //} -} \ No newline at end of file diff --git a/configs/admins.jsonc.example b/configs/admins.jsonc.example new file mode 100644 index 000000000..3b4ba8235 --- /dev/null +++ b/configs/admins.jsonc.example @@ -0,0 +1,57 @@ +//--------------------- +// Permission flags +//--------------------- +// a : Reserved slots +// b : Generic admin, required for admins +// c : Kick other players +// d : Banning other players +// e : Removing bans +// f : Slaying other players +// g : Changing the map +// h : Changing cvars +// i : Changing configs +// j : Special chat privileges +// k : Voting +// l : Password the server +// m : Remote console +// n : Change sv_cheats and related commands +// z : Root. It grants ALL flags so use with caution! + +// Flags 'o' to 'y' are custom flags (currently unused) + +// cs2f_admin_immunity 'convar' controls how targetting works with "immunity" KV +// 0 - Commands using immunity targetting can only target players with immunities LOWER than the user's +// 1 - Commands using immunity targetting can only target players with immunities EQUAL TO OR LOWER than the user's +// 2 - Commands ignore immunity levels +{ + "Groups": + { + // Group entries should follow this format + //"Moderator": // Group name. Used in "Admins" entries + //{ + // // These fields are optional and can be excluded if unset + // "flags": "abcdefg", // Permission flags as described above + // "immunity": 1 // Non-negative value. + //} + }, + + "Admins": + { + // Admin entries should follow this format + //"76561197960287930": // Steam64 ID + //{ + // "name": "Gabe Newell", // Admin name, can be anything. + // + // // Groups array is optional; you can instead manually define individual admin settings with later fields + // "groups": + // [ + // // Groups this admin inherits permissions from + // "Moderator" // Case sensitive + // ], + // + // // If at least 1 group is added above, these are all optional fields. + // "flags": "abcdefg", // Permission flags. Combines with any flags defined in user's groups + // "immunity": "1" // Non-negative value. Takes highest value from here or any of user's groups + //} + } +} \ No newline at end of file diff --git a/configs/maplist.jsonc.example b/configs/maplist.jsonc.example index 9786c30b2..51940db3e 100644 --- a/configs/maplist.jsonc.example +++ b/configs/maplist.jsonc.example @@ -44,6 +44,7 @@ "ze_my_third_ze_map": { "enabled": true, + "display_name": "ze_third_map", // A custom display name that will be used in the map vote UI "workshop_id": 789, "max_players": 20, "groups": [ "MyFirstGroup", "MySecondGroup" ] // A map can be in multiple groups diff --git a/configs/zr/weapons.cfg.example b/configs/zr/weapons.cfg.example index 5de243015..0d6d36f1b 100644 --- a/configs/zr/weapons.cfg.example +++ b/configs/zr/weapons.cfg.example @@ -33,6 +33,18 @@ { "enabled" "0" } + "kevlar" + { + "enabled" "1" + } + "assaultsuit" + { + "enabled" "0" + } + "defuser" + { + "enabled" "0" + } // Grenade "decoy" diff --git a/configure.py b/configure.py index b9c7013a9..fd72162b0 100644 --- a/configure.py +++ b/configure.py @@ -19,13 +19,9 @@ sys.exit(1) parser = run.BuildParser(sourcePath=sys.path[0], api='2.2') -parser.options.add_argument('-n', '--plugin-name', type=str, dest='plugin_name', default=None, - help='Plugin name') -parser.options.add_argument('-a', '--plugin-alias', type=str, dest='plugin_alias', default=None, - help='Plugin alias') parser.options.add_argument('--hl2sdk-root', type=str, dest='hl2sdk_root', default=None, help='Root search folder for HL2SDKs') -parser.options.add_argument('--hl2sdk-manifests', type=str, dest='hl2sdk_manifests', default='hl2sdk-manifests/', +parser.options.add_argument('--hl2sdk-manifests', type=str, dest='hl2sdk_manifests', default=None, help='HL2SDK manifests source tree folder') parser.options.add_argument('--mms_path', type=str, dest='mms_path', default=None, help='Metamod:Source source tree folder') diff --git a/gamedata/cs2fixes.games.txt b/gamedata/cs2fixes.games.txt index 19c6896d6..a689d8fb0 100644 --- a/gamedata/cs2fixes.games.txt +++ b/gamedata/cs2fixes.games.txt @@ -391,6 +391,21 @@ "windows" "\x48\x8D\x0D\x2A\x2A\x2A\x2A\xE8\x2A\x2A\x2A\x2A\x48\x63\xD8\x48\x6B\xD3" "linux" "\x48\x8D\x3D\x2A\x2A\x2A\x2A\xE8\x2A\x2A\x2A\x2A\x48\x8B\x05\x2A\x2A\x2A\x2A\x83\x05" } + // Called right before "%d spawn groups:\n" + "GetSpawnGroups" + { + "library" "server" + "windows" "\x40\x56\x48\x83\xEC\x2A\x48\x89\x5C\x24\x2A\x48\x8D\xB1" + "linux" "\x55\x48\x89\xE5\x41\x57\x41\x56\x4C\x8D\xB7\x2A\x2A\x2A\x2A\x41\x55\x49\x89\xF5\xBE" + } + // Only has "weapon_incgrenade" and "weapon_incgrenade" strings + // May need to look into custom way of acquiring this function address, normal sig sees frequent breakage in the CS# project + "CCSPlayer_ItemServices_CanAcquire" + { + "library" "server" + "windows" "\x44\x89\x44\x24\x2A\x48\x89\x54\x24\x2A\x48\x89\x4C\x24\x2A\x55\x56\x57\x41\x54\x41\x55\x41\x56\x41\x57\x48\x8B\xEC" + "linux" "\x55\x48\x89\xE5\x41\x57\x41\x56\x48\x8D\x45\x2A\x41\x55\x41\x54\x53\x48\x89\xCB" + } } "Offsets" { diff --git a/plugin-metadata.json b/plugin-metadata.json new file mode 100644 index 000000000..d0e244265 --- /dev/null +++ b/plugin-metadata.json @@ -0,0 +1,11 @@ +{ + "name": "cs2fixes", + "alias": "cs2fixes", + "display_name": "CS2Fixes", + "description": "A Metamod plugin with fixes and features aimed but not limited to zombie escape", + "author": "xen, Poggu, Vauff, Ice, lonefang, Kxnrl, tilgep, EasterLee and various contributors", + "url": "https://github.com/Source2ZE/CS2Fixes", + "log_tag": "CS2Fixes", + "license": "GPL v3 License", + "version": "{{parsed-version}}" +} \ No newline at end of file diff --git a/public/ics2fixes.h b/public/ics2fixes.h new file mode 100644 index 000000000..d4fa81a08 --- /dev/null +++ b/public/ics2fixes.h @@ -0,0 +1,52 @@ +/** + * ============================================================================= + * CS2Fixes + * Copyright (C) 2023-2025 Source2ZE + * ============================================================================= + * + * 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 . + */ + +#pragma once + +#include + +#define CS2FIXES_INTERFACE "CS2Fixes001" + +class ICS2Fixes +{ +public: + // Returns a bit flag of admin permissions. 0 if admin system is not initialized or user has no permissions. + // Valid flags can be found in cs2fixes/src/adminsystem.h + // What permission each flag correlates to is described in cs2fixes/configs/admins.jsonc.example + virtual std::uint64_t GetAdminFlags(std::uint64_t iSteam64ID) const = 0; + + // Sets a player's admin permissions bit flag. This will be overwritten if the plugin is reloaded + // or an admin uses c_reload_admins as it does not alter the config file. + // Returns false if unable to modify the admin (internal admin system is not set up yet) + virtual bool SetAdminFlags(std::uint64_t iSteam64ID, std::uint64_t iFlags) = 0; + + // Returns an integer for admin immunity level. 0 if admin system is not initialized or if user has no immunity. + // For behavior related to immunity, fetch the value for the "cs2f_admin_immunity" ConVar. + // Immunity targetting should work in the following ways for each value of cs2f_admin_immunity: + // 0 - Commands using immunity targetting can only target players with immunities LOWER than the user's + // 1 - Commands using immunity targetting can only target players with immunities EQUAL TO OR LOWER than the user's + // 2 - Commands ignore immunity levels + virtual int GetAdminImmunity(std::uint64_t iSteam64ID) const = 0; + + // Sets a player's immunity level. This will be overwritten if the plugin is reloaded + // or an admin uses c_reload_admins as it does not alter the config file. + // iImmunity's max value is INT_MAX and will be defaulted to INT_MAX if higher + // Returns false if unable to modify the admin (internal admin system is not set up yet) + virtual bool SetAdminImmunity(std::uint64_t iSteam64ID, std::uint32_t iImmunity) = 0; +}; \ No newline at end of file diff --git a/sdk b/sdk index 74ea93437..decc3d05c 160000 --- a/sdk +++ b/sdk @@ -1 +1 @@ -Subproject commit 74ea9343746475b148f5f53679516dc9cd2b336b +Subproject commit decc3d05c99c25e892a492a79d364e3fa3d8a7ea diff --git a/src/addresses.cpp b/src/addresses.cpp index 4391b5963..1ba79a5bc 100644 --- a/src/addresses.cpp +++ b/src/addresses.cpp @@ -73,6 +73,7 @@ bool addresses::Initialize(CGameConfig* g_GameConfig) RESOLVE_SIG(g_GameConfig, "CTakeDamageInfo", addresses::CTakeDamageInfo_Constructor); RESOLVE_SIG(g_GameConfig, "CNetworkStringTable_DeleteAllStrings", addresses::CNetworkStringTable_DeleteAllStrings); RESOLVE_SIG(g_GameConfig, "CCSPlayer_WeaponServices_EquipWeapon", addresses::CCSPlayer_WeaponServices_EquipWeapon); + RESOLVE_SIG(g_GameConfig, "GetSpawnGroups", addresses::GetSpawnGroups); return InitializeBanMap(g_GameConfig); } diff --git a/src/addresses.h b/src/addresses.h index d20ccd5f3..31c4a72e3 100644 --- a/src/addresses.h +++ b/src/addresses.h @@ -57,6 +57,7 @@ class CTakeDamageInfo; class INetworkStringTable; class CCSPlayer_WeaponServices; class CBasePlayerWeapon; +class CSpawnGroupMgrGameSystem; struct EmitSound_t; struct SndOpEventGuid_t; @@ -108,4 +109,5 @@ namespace addresses const Vector* vecDamageForce, const Vector* vecDamagePosition, float flDamage, int bitsDamageType, int iCustomDamage, void* a10); inline void(FASTCALL* CNetworkStringTable_DeleteAllStrings)(INetworkStringTable* pThis); inline void(FASTCALL* CCSPlayer_WeaponServices_EquipWeapon)(CCSPlayer_WeaponServices* pWeaponServices, CBasePlayerWeapon* pPlayerWeapon); + inline void(FASTCALL* GetSpawnGroups)(CSpawnGroupMgrGameSystem* pSpawnGroupMgr, CUtlVector* pList); } // namespace addresses \ No newline at end of file diff --git a/src/adminsystem.cpp b/src/adminsystem.cpp index 5715f0b95..faf00a9c1 100644 --- a/src/adminsystem.cpp +++ b/src/adminsystem.cpp @@ -29,18 +29,24 @@ #include "entwatch.h" #include "filesystem.h" #include "gamesystem.h" +#include "hud_manager.h" #include "icvar.h" #include "interfaces/interfaces.h" #include "map_votes.h" #include "playermanager.h" #include "utils/entity.h" #include "votemanager.h" +#include #include +#undef snprintf +#include "vendor/nlohmann/json.hpp" + extern IVEngineServer2* g_pEngineServer2; extern CGameEntitySystem* g_pEntitySystem; extern CGlobalVars* GetGlobals(); extern CCSGameRules* g_pGameRules; +extern CPlayerManager* g_playerManager; CAdminSystem* g_pAdminSystem = nullptr; @@ -395,53 +401,6 @@ CON_COMMAND_CHAT_FLAGS(bring, " - Bring a player", ADMFLAG_SLAY) PrintMultiAdminAction(nType, player->GetPlayerName(), "brought"); } -CON_COMMAND_CHAT_FLAGS(setteam, " - Set a player's team", ADMFLAG_SLAY) -{ - if (args.ArgC() < 3) - { - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Usage: !setteam "); - return; - } - - int iTeam = V_StringToInt32(args[2], -1); - - if (iTeam < CS_TEAM_NONE || iTeam > CS_TEAM_CT) - { - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Invalid team specified, range is 0-3."); - return; - } - - int iNumClients = 0; - int pSlots[MAXPLAYERS]; - ETargetType nType; - - if (!g_playerManager->CanTargetPlayers(player, args[1], iNumClients, pSlots, NO_TARGET_BLOCKS, nType)) - return; - - const char* pszCommandPlayerName = player ? player->GetPlayerName() : CONSOLE_NAME; - - constexpr const char* teams[] = {"none", "spectators", "terrorists", "counter-terrorists"}; - - char szAction[64]; - V_snprintf(szAction, sizeof(szAction), " to %s", teams[iTeam]); - - for (int i = 0; i < iNumClients; i++) - { - CCSPlayerController* pTarget = CCSPlayerController::FromSlot(pSlots[i]); - - if (!pTarget) - continue; - - pTarget->SwitchTeam(iTeam); - - if (iNumClients == 1) - PrintSingleAdminAction(pszCommandPlayerName, pTarget->GetPlayerName(), "moved", szAction); - } - - if (iNumClients > 1) - PrintMultiAdminAction(nType, pszCommandPlayerName, "moved", szAction); -} - CON_COMMAND_CHAT_FLAGS(noclip, "[name] - Toggle noclip on a player", ADMFLAG_CHEATS) { int iNumClients = 0; @@ -602,7 +561,9 @@ CON_COMMAND_CHAT_FLAGS(hsay, " - Say something as a hud hint", ADMFLAG_ return; } - ClientPrintAll(HUD_PRINTCENTER, "%s", args.ArgS()); + SendHudMessageAll( + 10, EHudPriority::AdminHSay, "ADMIN: %s", + EscapeHTMLSpecialCharacters(args.ArgS()).c_str()); } CON_COMMAND_CHAT_FLAGS(rcon, " - Send a command to server console", ADMFLAG_RCON) @@ -1075,6 +1036,131 @@ CON_COMMAND_CHAT_FLAGS(setpos, " - Set your origin", ADMFLAG_CHEATS) PrintSingleAdminAction(player->GetPlayerName(), szOrigin, "teleported to"); } +CON_COMMAND_CHAT_FLAGS(strip, " - Strip all the weapons/items of a player", ADMFLAG_CHEATS) +{ + if (args.ArgC() < 2) + { + ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Usage: !strip "); + return; + } + + int iNumClients = 0; + int pSlots[MAXPLAYERS]; + ETargetType nType; + + if (!g_playerManager->CanTargetPlayers(player, args[1], iNumClients, pSlots, NO_DEAD | NO_SPECTATOR, nType)) + return; + + const char* pszCommandPlayerName = player ? player->GetPlayerName() : CONSOLE_NAME; + + for (int i = 0; i < iNumClients; i++) + { + CCSPlayerController* pTarget = CCSPlayerController::FromSlot(pSlots[i]); + + if (!pTarget) + continue; + + CCSPlayerPawn* pPawn = pTarget->GetPlayerPawn(); + + if (!pPawn) + continue; + + CCSPlayer_ItemServices* pItemServices = pPawn->m_pItemServices(); + + if (!pItemServices) + return; + + pItemServices->StripPlayerWeapons(true); + + if (iNumClients == 1) + PrintSingleAdminAction(pszCommandPlayerName, pTarget->GetPlayerName(), "stripped", ""); + } + + if (iNumClients > 1) + PrintMultiAdminAction(nType, pszCommandPlayerName, "stripped", ""); +} + +CON_COMMAND_CHAT_FLAGS(give, " - Give a weapon/item to a player", ADMFLAG_CHEATS) +{ + if (args.ArgC() < 3) + { + ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Usage: !give "); + return; + } + + const WeaponInfo_t* pWeaponInfo = FindWeaponInfoByClassCaseInsensitive(args[2]); + + if (!pWeaponInfo) + { + ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "%s is not a valid weapon class.", args[2]); + return; + } + + int iNumClients = 0; + int pSlots[MAXPLAYERS]; + ETargetType nType; + + if (!g_playerManager->CanTargetPlayers(player, args[1], iNumClients, pSlots, NO_DEAD | NO_SPECTATOR, nType)) + return; + + const char* pszCommandPlayerName = player ? player->GetPlayerName() : CONSOLE_NAME; + + char szAction[64]; + V_snprintf(szAction, sizeof(szAction), "given %s to", args[2]); + + for (int i = 0; i < iNumClients; i++) + { + CCSPlayerController* pTarget = CCSPlayerController::FromSlot(pSlots[i]); + + if (!pTarget) + continue; + + CCSPlayerPawn* pPawn = pTarget->GetPlayerPawn(); + + if (!pPawn) + continue; + + CCSPlayer_ItemServices* pItemServices = pPawn->m_pItemServices; + CCSPlayer_WeaponServices* pWeaponServices = pPawn->m_pWeaponServices; + + if (!pItemServices || !pWeaponServices) + return; + + if (pWeaponInfo->m_eSlot == GEAR_SLOT_RIFLE || pWeaponInfo->m_eSlot == GEAR_SLOT_PISTOL) + { + CUtlVector>* weapons = pWeaponServices->m_hMyWeapons(); + + FOR_EACH_VEC(*weapons, i) + { + CBasePlayerWeapon* weapon = (*weapons)[i].Get(); + + if (!weapon) + continue; + + if (weapon->GetWeaponVData()->m_GearSlot() == pWeaponInfo->m_eSlot) + { + pWeaponServices->DropWeapon(weapon); + break; + } + } + } + + CBasePlayerWeapon* pWeapon = pItemServices->GiveNamedItemAws(pWeaponInfo->m_pClass); + + if (!pWeapon) + { + ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Failed to give %s, something went wrong.", pWeaponInfo->m_pClass); + return; + } + + if (iNumClients == 1) + PrintSingleAdminAction(pszCommandPlayerName, pTarget->GetPlayerName(), szAction, ""); + } + + if (iNumClients > 1) + PrintMultiAdminAction(nType, pszCommandPlayerName, szAction, ""); +} + #ifdef _DEBUG CON_COMMAND_CHAT_FLAGS(add_dc, " - Adds a fake player to disconnected player list for testing", ADMFLAG_GENERIC) { @@ -1095,8 +1181,85 @@ CON_COMMAND_CHAT_FLAGS(add_dc, " - Adds a fake p g_pAdminSystem->AddDisconnectedPlayer(args[1], iSteamID, args[3]); } + +CON_COMMAND_CHAT_FLAGS(setteam, " - Set a player's team", ADMFLAG_CHEATS) +{ + if (args.ArgC() < 3) + { + ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Usage: !setteam "); + return; + } + + int iTeam = V_StringToInt32(args[2], -1); + + if (iTeam < CS_TEAM_NONE || iTeam > CS_TEAM_CT) + { + ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Invalid team specified, range is 0-3."); + return; + } + + int iNumClients = 0; + int pSlots[MAXPLAYERS]; + ETargetType nType; + + if (!g_playerManager->CanTargetPlayers(player, args[1], iNumClients, pSlots, NO_TARGET_BLOCKS, nType)) + return; + + const char* pszCommandPlayerName = player ? player->GetPlayerName() : CONSOLE_NAME; + + constexpr const char* teams[] = {"none", "spectators", "terrorists", "counter-terrorists"}; + + char szAction[64]; + V_snprintf(szAction, sizeof(szAction), " to %s", teams[iTeam]); + + for (int i = 0; i < iNumClients; i++) + { + CCSPlayerController* pTarget = CCSPlayerController::FromSlot(pSlots[i]); + + if (!pTarget) + continue; + + pTarget->SwitchTeam(iTeam); + + if (iNumClients == 1) + PrintSingleAdminAction(pszCommandPlayerName, pTarget->GetPlayerName(), "moved", szAction); + } + + if (iNumClients > 1) + PrintMultiAdminAction(nType, pszCommandPlayerName, "moved", szAction); +} #endif +void CAdmin::SetFlags(uint64 iFlags) +{ + CAdminBase::SetFlags(iFlags); + + if (!GetGlobals()) + return; + + ZEPlayer* zpAdmin = g_playerManager->GetPlayerFromSteamId(m_iSteamID); + if (!zpAdmin) // Authentication is checked in GetPlayerFromSteamId, so dont need to redo it here + return; + + zpAdmin->SetAdminFlags(iFlags); +} + +void CAdmin::SetImmunity(std::uint32_t iAdminImmunity) +{ + if (iAdminImmunity > INT_MAX) + iAdminImmunity = INT_MAX; + CAdminBase::SetImmunity(iAdminImmunity); + + if (!GetGlobals()) + return; + + ZEPlayer* zpAdmin = g_playerManager->GetPlayerFromSteamId(m_iSteamID); + if (!zpAdmin) // Authentication is checked in GetPlayerFromSteamId, so dont need to redo it here + return; + + zpAdmin->SetAdminImmunity(static_cast(iAdminImmunity)); // should be safe to cast due to earlier check +} + CAdminSystem::CAdminSystem() { LoadAdmins(); @@ -1108,9 +1271,9 @@ CAdminSystem::CAdminSystem() m_iDCPlyIndex = 0; } -bool CAdminSystem::LoadAdmins() +// TODO: Remove this once servers have been given a few months to update cs2fixes +bool CAdminSystem::ConvertAdminsKVToJSON() { - m_vecAdmins.Purge(); KeyValues* pKV = new KeyValues("admins"); KeyValues::AutoDelete autoDelete(pKV); @@ -1121,45 +1284,182 @@ bool CAdminSystem::LoadAdmins() Warning("Failed to load %s\n", pszPath); return false; } + + ordered_json jAdmins; + + jAdmins["Admins"] = ordered_json(ordered_json::value_t::object); + for (KeyValues* pKey = pKV->GetFirstSubKey(); pKey; pKey = pKey->GetNextKey()) { - const char* pszName = pKey->GetName(); - const char* pszSteamID = pKey->GetString("steamid", nullptr); - const char* pszFlags = pKey->GetString("flags", nullptr); - int iImmunityLevel = pKey->GetInt("immunity", -1); + ordered_json jAdmin; - if (!pszSteamID) + if (!pKey->FindKey("steamid")) { - Warning("Admin entry %s is missing 'steam' key\n", pszName); + Warning("Admin entry %s is missing 'steam' key\n", pKey->GetName()); return false; } - if (!pszFlags) + jAdmin["name"] = pKey->GetName(); + + if (pKey->FindKey("flags")) + jAdmin["flags"] = pKey->GetString("flags", nullptr); + + if (pKey->FindKey("immunity")) + jAdmin["immunity"] = pKey->GetInt("immunity", 0); + + jAdmins["Admins"][pKey->GetString("steamid", "")] = jAdmin; + } + + const char* pszJsonPath = "addons/cs2fixes/configs/admins.jsonc"; + const char* pszKVConfigRenamePath = "addons/cs2fixes/configs/admins_old.cfg"; + char szPath[MAX_PATH]; + V_snprintf(szPath, sizeof(szPath), "%s%s%s", Plat_GetGameDirectory(), "/csgo/", pszJsonPath); + std::ofstream jsonFile(szPath); + + if (!jsonFile.is_open()) + { + Panic("Failed to open %s\n", pszJsonPath); + return false; + } + + jsonFile << std::setfill('\t') << std::setw(1) << jAdmins << std::endl; + + char szKVRenamePath[MAX_PATH]; + V_snprintf(szPath, sizeof(szPath), "%s%s%s", Plat_GetGameDirectory(), "/csgo/", pszPath); + V_snprintf(szKVRenamePath, sizeof(szPath), "%s%s%s", Plat_GetGameDirectory(), "/csgo/", pszKVConfigRenamePath); + + std::rename(szPath, szKVRenamePath); + + // remove old cfg example if it exists + const char* pszKVExamplePath = "addons/cs2fixes/configs/admins.cfg.example"; + V_snprintf(szPath, sizeof(szPath), "%s%s%s", Plat_GetGameDirectory(), "/csgo/", pszKVExamplePath); + std::remove(szPath); + + Message("Successfully converted KV1 admins.cfg to JSON format at %s\n", pszJsonPath); + return true; +} + +bool CAdminSystem::LoadAdmins() +{ + m_mapAdmins.clear(); + m_mapAdminGroups.clear(); + + const char* pszJsonPath = "addons/cs2fixes/configs/admins.jsonc"; + char szPath[MAX_PATH]; + V_snprintf(szPath, sizeof(szPath), "%s%s%s", Plat_GetGameDirectory(), "/csgo/", pszJsonPath); + std::ifstream jsonFile(szPath); + + if (!jsonFile.is_open()) + { + if (!ConvertAdminsKVToJSON()) { - Warning("Admin entry %s is missing 'flags' key\n", pszName); + Panic("Failed to open %s and convert KV1 admins.cfg to JSON format, admins are not loaded!\n", pszJsonPath); return false; } - if (iImmunityLevel < 0) + jsonFile.open(szPath); + } + + ordered_json jAdminConfig = ordered_json::parse(jsonFile, nullptr, false, true); + + if (jAdminConfig.is_discarded()) + { + Panic("Failed parsing JSON from %s, admins are not loaded!\n", pszJsonPath); + return false; + } + + ordered_json jAdmins = jAdminConfig.value("Admins", ordered_json()); + + if (jAdmins.empty()) + { + Panic("Failed parsing JSON from %s, admins are not loaded!\n", pszJsonPath); + return false; + } + + ordered_json jGroups = jAdminConfig.value("Groups", ordered_json()); + for (auto it = jGroups.cbegin(); it != jGroups.cend(); ++it) + { + const json& jGroup = it.value(); + + if (jGroup.contains("immunity") && !jGroup["immunity"].is_number()) { - Warning("Admin entry %s is missing 'immunity' key\n", pszName); - iImmunityLevel = 0; // Zero is default immunity, so set that if not given + Panic("Group '%s' has non-numeric 'immunity' field\n", it.key().c_str()); + return false; } - ConMsg("Loaded admin %s\n", pszName); - ConMsg(" - Steam ID: %5s\n", pszSteamID); - ConMsg(" - Flags: %5s\n", pszFlags); - ConMsg(" - Immunity: %i\n", iImmunityLevel); + CAdminBase group = CAdminBase(ParseFlags(jGroup.value("flags", "")), jGroup.value("immunity", 0)); + m_mapAdminGroups.emplace(it.key(), group); - uint64 iFlags = ParseFlags(pszFlags); - - // Let's just use steamID64 for now - m_vecAdmins.AddToTail(CAdmin(pszName, atoll(pszSteamID), iFlags, iImmunityLevel)); + ConMsg("Loaded group %s\n", it.key().c_str()); + ConMsg(" - Flags: %s\n", StringifyFlags(group.GetFlags()).c_str()); + ConMsg(" - Immunity: %i\n", group.GetImmunity()); } + for (auto it = jAdmins.cbegin(); it != jAdmins.cend(); ++it) + { + const json& jAdmin = it.value(); + + if (jAdmin.contains("immunity") && !jAdmin["immunity"].is_number()) + { + Panic("Admin '%s' has non-numeric 'immunity' field\n", it.key().c_str()); + return false; + } + + uint64 iFlags = ParseFlags(jAdmin.value("flags", "")); + int iImmunity = jAdmin.value("immunity", 0); + + ordered_json jInheritedGroups = jAdmin.value("groups", ordered_json()); + for (const auto& groupName : jInheritedGroups) + { + if (!groupName.is_string()) + { + Panic("Admin '%s' has invalid group name in 'groups' array\n", it.key().c_str()); + return false; + } + + const std::string& name = groupName.get(); + + auto jt = m_mapAdminGroups.find(name); + if (jt == m_mapAdminGroups.end()) + { + Panic("Admin '%s' has invalid group name '%s'\n", it.key().c_str(), name.c_str()); + return false; + } + + const CAdminBase& group = jt->second; + + iFlags |= group.GetFlags(); + iImmunity = std::max(iImmunity, group.GetImmunity()); + } + + uint64 iSteamID = atoll(it.key().c_str()); + CAdmin admin = CAdmin(jAdmin.value("name", ""), iFlags, iImmunity, iSteamID); + m_mapAdmins.emplace(iSteamID, admin); + + ConMsg("Loaded admin %s\n", it.key().c_str()); + ConMsg(" - Name: %s\n", admin.GetName().c_str()); + ConMsg(" - Flags: %s\n", StringifyFlags(admin.GetFlags()).c_str()); + ConMsg(" - Immunity: %i\n", admin.GetImmunity()); + } return true; } +// If an admin with this iSteamID already exists, just update Flags and Immunity. +void CAdminSystem::AddOrUpdateAdmin(uint64 iSteamID, uint64 iFlags, int iAdminImmunity) +{ + CAdmin* admin = FindAdmin(iSteamID); + if (!admin) + { + m_mapAdmins.emplace(iSteamID, CAdmin("< blank >", iFlags, iAdminImmunity, iSteamID)); + admin = FindAdmin(iSteamID); + } + + // Set these even if we created a new admin with the flags, as these have + // extra logic to apply new values to the player if they are currently online + admin->SetFlags(iFlags); + admin->SetImmunity(iAdminImmunity); +} + bool CAdminSystem::LoadInfractions() { m_vecInfractions.PurgeAndDeleteElements(); @@ -1322,23 +1622,20 @@ bool CAdminSystem::FindAndRemoveInfractionSteamId64(uint64 steamid64, CInfractio CAdmin* CAdminSystem::FindAdmin(uint64 iSteamID) { - FOR_EACH_VEC(m_vecAdmins, i) - { - if (m_vecAdmins[i].GetSteamID() == iSteamID) - return &m_vecAdmins[i]; - } + auto it = m_mapAdmins.find(iSteamID); + if (it == m_mapAdmins.end()) + return nullptr; - return nullptr; + return &it->second; } -uint64 CAdminSystem::ParseFlags(const char* pszFlags) +uint64 CAdminSystem::ParseFlags(std::string strFlags) { uint64 flags = 0; - size_t length = V_strlen(pszFlags); - for (size_t i = 0; i < length; i++) + for (size_t i = 0; i < strFlags.length(); i++) { - char c = tolower(pszFlags[i]); + char c = tolower(strFlags[i]); if (c < 'a' || c > 'z') continue; @@ -1351,6 +1648,20 @@ uint64 CAdminSystem::ParseFlags(const char* pszFlags) return flags; } +std::string CAdminSystem::StringifyFlags(uint64 iFlags) +{ + if (iFlags == static_cast(-1)) + return "z"; // root / all permissions + + std::string strFlags; + + for (int i = 0; i < 25; ++i) // 'a' to 'y' + if (iFlags & (static_cast(1) << i)) + strFlags += static_cast('a' + i); + + return strFlags; +} + void CAdminSystem::AddDisconnectedPlayer(const char* pszName, uint64 xuid, const char* pszIP) { auto plyInfo = std::make_tuple(pszName, xuid, pszIP); @@ -1498,7 +1809,10 @@ std::string GetReason(const CCommand& args, int iArgsBefore, bool bStripUnicode) { if (args.ArgC() <= iArgsBefore + 1) return ""; - std::string strReason = args.ArgS(); + std::string strTemp = args.ArgS(); + std::string strReason = ""; + // Remove all double quotes + std::copy_if(strTemp.cbegin(), strTemp.cend(), std::back_inserter(strReason), [](unsigned char c) { return c != '\"'; }); for (size_t i = 1; i <= iArgsBefore; i++) { diff --git a/src/adminsystem.h b/src/adminsystem.h index 3320e7218..914fa58a5 100644 --- a/src/adminsystem.h +++ b/src/adminsystem.h @@ -139,30 +139,45 @@ class CEbanInfraction : public CInfractionBase void UndoInfraction(ZEPlayer*) override; }; -class CAdmin +class CAdminBase { public: - CAdmin(const char* pszName, uint64 iSteamID, uint64 iFlags, int iAdminImmunity) : - m_pszName(pszName), m_iSteamID(iSteamID), m_iFlags(iFlags), m_iAdminImmunity(iAdminImmunity) + CAdminBase(uint64 iFlags, int iAdminImmunity) : + m_iFlags(iFlags), m_iAdminImmunity(iAdminImmunity) {} - const char* GetName() { return m_pszName; } - uint64 GetSteamID() { return m_iSteamID; } - uint64 GetFlags() { return m_iFlags; } - int GetImmunity() { return m_iAdminImmunity; } + void SetFlags(uint64 iFlags) { m_iFlags = iFlags; }; + uint64 GetFlags() const { return m_iFlags; } + void SetImmunity(std::uint32_t iAdminImmunity) { m_iAdminImmunity = static_cast(iAdminImmunity); } + int GetImmunity() const { return m_iAdminImmunity; } private: - const char* m_pszName; - uint64 m_iSteamID; uint64 m_iFlags; int m_iAdminImmunity; }; +class CAdmin : public CAdminBase +{ +public: + CAdmin(std::string strName, uint64 iFlags, int iAdminImmunity, uint64 iSteamID) : + CAdminBase(iFlags, iAdminImmunity), m_strName(strName), m_iSteamID(iSteamID) + {} + + std::string GetName() { return m_strName; } + void SetFlags(uint64 iFlags); + void SetImmunity(std::uint32_t iAdminImmunity); + +private: + std::string m_strName; + uint64 m_iSteamID; +}; + class CAdminSystem { public: CAdminSystem(); bool LoadAdmins(); + void AddOrUpdateAdmin(uint64 iSteamID, uint64 iFlags = 0, int iAdminImmunity = 0); bool LoadInfractions(); void AddInfraction(CInfractionBase*); void SaveInfractions(); @@ -170,12 +185,17 @@ class CAdminSystem bool FindAndRemoveInfraction(ZEPlayer* player, CInfractionBase::EInfractionType type); bool FindAndRemoveInfractionSteamId64(uint64 steamid64, CInfractionBase::EInfractionType type); CAdmin* FindAdmin(uint64 iSteamID); - uint64 ParseFlags(const char* pszFlags); + uint64 ParseFlags(std::string strFlags); + std::string StringifyFlags(uint64 iFlags); void AddDisconnectedPlayer(const char* pszName, uint64 xuid, const char* pszIP); void ShowDisconnectedPlayers(CCSPlayerController* const pAdmin); + // TODO: Remove this once servers have been given a few months to update cs2fixes + bool ConvertAdminsKVToJSON(); + private: - CUtlVector m_vecAdmins; + std::map m_mapAdminGroups; + std::map m_mapAdmins; CUtlVector m_vecInfractions; // Implemented as a circular buffer. diff --git a/src/buttonwatch.cpp b/src/buttonwatch.cpp index 0a68d7644..99636cf1a 100644 --- a/src/buttonwatch.cpp +++ b/src/buttonwatch.cpp @@ -32,16 +32,17 @@ #include "entity/clogiccase.h" #include "entity/cpointviewcontrol.h" -CConVar g_cvarEnableButtonWatch("cs2f_enable_button_watch", FCVAR_NONE, "INCOMPATIBLE WITH CS#. Whether to enable button watch or not.", false, - [](CConVar* cvar, CSplitScreenSlot slot, const bool* new_val, const bool* old_val) { - if (!(*new_val) || !SetupFireOutputInternalDetour()) - { - mapIOFunctions.erase("buttonwatch"); - cvar->Set(false); - } - else if (!IsButtonWatchEnabled()) - mapIOFunctions["buttonwatch"] = ButtonWatch; - }); +CConVar g_cvarEnableButtonWatch( + "cs2f_enable_button_watch", FCVAR_NONE, "INCOMPATIBLE WITH CS#. Whether to enable button watch or not.", false, + [](CConVar* cvar, CSplitScreenSlot slot, const bool* new_val, const bool* old_val) { + if (!(*new_val) || !SetupFireOutputInternalDetour()) + { + mapIOFunctions.erase("buttonwatch"); + cvar->Set(false); + } + else if (!IsButtonWatchEnabled()) + mapIOFunctions["buttonwatch"] = ButtonWatch; + }); CON_COMMAND_CHAT_FLAGS(bw, "- Toggle button watch display", ADMFLAG_GENERIC) { diff --git a/src/commands.cpp b/src/commands.cpp index 5fc28926f..6ecdd77b6 100644 --- a/src/commands.cpp +++ b/src/commands.cpp @@ -52,6 +52,7 @@ extern IGameEventSystem* g_gameEventSystem; extern CGameEntitySystem* g_pEntitySystem; extern IVEngineServer2* g_pEngineServer2; extern ISteamHTTP* g_http; +extern CConVar g_cvarFlashLightAttachment; CConVar g_cvarEnableCommands("cs2f_commands_enable", FCVAR_NONE, "Whether to enable chat commands", false); CConVar g_cvarEnableAdminCommands("cs2f_admin_commands_enable", FCVAR_NONE, "Whether to enable admin chat commands", false); @@ -135,13 +136,14 @@ void ParseWeaponCommand(const CCommand& args, CCSPlayerController* player) return; } - if (pWeaponInfo->m_eSlot == GEAR_SLOT_GRENADES) - { - static ConVarRefAbstract ammo_grenade_limit_default("ammo_grenade_limit_default"), ammo_grenade_limit_total("ammo_grenade_limit_total"); + static ConVarRefAbstract ammo_grenade_limit_default("ammo_grenade_limit_default"), ammo_grenade_limit_total("ammo_grenade_limit_total"), mp_weapons_allow_typecount("mp_weapons_allow_typecount"); - int iGrenadeLimitDefault = ammo_grenade_limit_default.GetInt(); - int iGrenadeLimitTotal = ammo_grenade_limit_total.GetInt(); + int iGrenadeLimitDefault = ammo_grenade_limit_default.GetInt(); + int iGrenadeLimitTotal = ammo_grenade_limit_total.GetInt(); + int iWeaponLimit = mp_weapons_allow_typecount.GetInt(); + if (pWeaponInfo->m_eSlot == GEAR_SLOT_GRENADES) + { int iMatchingGrenades = GetGrenadeAmmo(pWeaponServices, pWeaponInfo); int iTotalGrenades = GetGrenadeAmmoTotal(pWeaponServices); @@ -158,35 +160,42 @@ void ParseWeaponCommand(const CCommand& args, CCSPlayerController* player) } } + int maxAmount; + if (pWeaponInfo->m_nMaxAmount) + maxAmount = pWeaponInfo->m_nMaxAmount; + else if (pWeaponInfo->m_eSlot == GEAR_SLOT_GRENADES) + maxAmount = iGrenadeLimitDefault; + else + maxAmount = iWeaponLimit == -1 ? 9999 : iWeaponLimit; + + CUtlVector* weaponPurchases = pPawn->m_pActionTrackingServices->m_weaponPurchasesThisRound().m_weaponPurchases; + bool found = false; + FOR_EACH_VEC(*weaponPurchases, i) { - CUtlVector* weaponPurchases = pPawn->m_pActionTrackingServices->m_weaponPurchasesThisRound().m_weaponPurchases; - bool found = false; - FOR_EACH_VEC(*weaponPurchases, i) + WeaponPurchaseCount_t& purchase = (*weaponPurchases)[i]; + if (purchase.m_nItemDefIndex == pWeaponInfo->m_iItemDefinitionIndex) { - WeaponPurchaseCount_t& purchase = (*weaponPurchases)[i]; - if (purchase.m_nItemDefIndex == pWeaponInfo->m_iItemDefinitionIndex) + // Note ammo_grenade_limit_total is not followed here, only for checking inventory space + if (purchase.m_nCount >= maxAmount) { - if (purchase.m_nCount >= pWeaponInfo->m_nMaxAmount) - { - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "You cannot buy any more %s (Max %i)", pWeaponInfo->m_pName, pWeaponInfo->m_nMaxAmount); - return; - } - purchase.m_nCount += 1; - found = true; - break; + ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "You cannot buy any more %s (Max %i)", pWeaponInfo->m_pName, maxAmount); + return; } + purchase.m_nCount += 1; + found = true; + break; } + } - if (!found) - { - WeaponPurchaseCount_t purchase = {}; + if (!found) + { + WeaponPurchaseCount_t purchase = {}; - purchase.m_nCount = 1; - purchase.m_nItemDefIndex = pWeaponInfo->m_iItemDefinitionIndex; + purchase.m_nCount = 1; + purchase.m_nItemDefIndex = pWeaponInfo->m_iItemDefinitionIndex; - weaponPurchases->AddToTail(purchase); - } + weaponPurchases->AddToTail(purchase); } if (pWeaponInfo->m_eSlot == GEAR_SLOT_RIFLE || pWeaponInfo->m_eSlot == GEAR_SLOT_PISTOL) @@ -210,16 +219,12 @@ void ParseWeaponCommand(const CCommand& args, CCSPlayerController* player) CBasePlayerWeapon* pWeapon = pItemServices->GiveNamedItemAws(pWeaponInfo->m_pClass); - // Normally shouldn't be possible, but avoid crashes in some edge cases + // Normally shouldn't be possible, but avoid issues in some edge cases if (!pWeapon) return; player->m_pInGameMoneyServices->m_iAccount = money - pWeaponInfo->m_nPrice; - // If the weapon spawn goes through AWS, it needs to be manually selected because it spawns dropped in-world due to ZR enforcing mp_weapons_allow_* cvars against T's - if (pWeaponInfo->m_eSlot == GEAR_SLOT_RIFLE || pWeaponInfo->m_eSlot == GEAR_SLOT_PISTOL) - pWeaponServices->SelectItem(pWeapon); - ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "You have purchased %s for $%i", pWeaponInfo->m_pName, pWeaponInfo->m_nPrice); } @@ -800,10 +805,8 @@ CON_COMMAND_CHAT(fl, "- Flashlight") pLight->DispatchSpawn(pKeyValues); - variant_t val("!player"); - pLight->AcceptInput("SetParent", &val); - variant_t val2("clip_limit"); - pLight->AcceptInput("SetParentAttachmentMaintainOffset", &val2); + pLight->SetParent(pPawn); + pLight->AcceptInput("SetParentAttachmentMaintainOffset", g_cvarFlashLightAttachment.Get().String()); } CON_COMMAND_CHAT(say, " - Say something using console") diff --git a/src/cs2_sdk/entity/cbaseentity.h b/src/cs2_sdk/entity/cbaseentity.h index 67e403561..04c918e6d 100644 --- a/src/cs2_sdk/entity/cbaseentity.h +++ b/src/cs2_sdk/entity/cbaseentity.h @@ -154,6 +154,7 @@ class CBaseEntity : public CEntityInstance SCHEMA_FIELD(CUtlString, m_sUniqueHammerID); SCHEMA_FIELD(CUtlSymbolLarge, m_target); SCHEMA_FIELD(CUtlSymbolLarge, m_iGlobalname); + SCHEMA_FIELD(CHandle, m_hOwnerEntity) int entindex() { return m_pEntity->m_EHandle.GetEntryIndex(); } diff --git a/src/cs2_sdk/entity/ccsplayerpawn.cpp b/src/cs2_sdk/entity/ccsplayerpawn.cpp index 15c3641b9..9750afad3 100644 --- a/src/cs2_sdk/entity/ccsplayerpawn.cpp +++ b/src/cs2_sdk/entity/ccsplayerpawn.cpp @@ -21,16 +21,19 @@ #include "../ctimer.h" // Silly workaround for an animation bug that's been happening since 2024-11-06 CS2 update -// Clients need to see the new playermodel with zero velocity for at least one tick to properly render animations +// Clients need to see the new playermodel with zero velocity for at least (two?) ticks to properly render animations void CCSPlayerPawn::FixPlayerModelAnimations() { + if (m_nActualMoveType() < MOVETYPE_WALK) + return; + CHandle hPawn = GetHandle(); Vector originalVelocity = m_vecAbsVelocity; Teleport(nullptr, nullptr, &vec3_origin); SetMoveType(MOVETYPE_OBSOLETE); - new CTimer(0.01f, false, false, [hPawn, originalVelocity]() { + new CTimer(0.02f, false, false, [hPawn, originalVelocity]() { CCSPlayerPawn* pPawn = hPawn.Get(); if (!pPawn || !pPawn->IsAlive()) diff --git a/src/cs2_sdk/entity/ccsplayerpawn.h b/src/cs2_sdk/entity/ccsplayerpawn.h index c6b96c818..db0fcf3ec 100644 --- a/src/cs2_sdk/entity/ccsplayerpawn.h +++ b/src/cs2_sdk/entity/ccsplayerpawn.h @@ -43,6 +43,7 @@ class CCSPlayerPawnBase : public CBasePlayerPawn SCHEMA_FIELD(CSPlayerState, m_iPlayerState) SCHEMA_FIELD(CHandle, m_hOriginalController) SCHEMA_FIELD(CCSPlayer_ViewModelServices*, m_pViewModelServices) + SCHEMA_FIELD(CCSPlayer_PingServices*, m_pPingServices) CCSPlayerController* GetOriginalController() { diff --git a/src/cs2_sdk/entity/cgamerules.h b/src/cs2_sdk/entity/cgamerules.h index 127ba020c..7e04474e1 100644 --- a/src/cs2_sdk/entity/cgamerules.h +++ b/src/cs2_sdk/entity/cgamerules.h @@ -71,6 +71,10 @@ class CCSGameRules : public CGameRules SCHEMA_FIELD(bool, m_bFreezePeriod) SCHEMA_FIELD_POINTER(CUtlVector, m_CTSpawnPoints) SCHEMA_FIELD_POINTER(CUtlVector, m_TerroristSpawnPoints) + SCHEMA_FIELD(int, m_iMaxNumTerrorists) + SCHEMA_FIELD(int, m_iMaxNumCTs) + SCHEMA_FIELD(bool, m_bGameRestart) + SCHEMA_FIELD(bool, m_bWarmupPeriod) void TerminateRound(float flDelay, CSRoundEndReason reason) { diff --git a/src/cs2_sdk/entity/services.cpp b/src/cs2_sdk/entity/services.cpp index 58e0cad3d..d7cc470aa 100644 --- a/src/cs2_sdk/entity/services.cpp +++ b/src/cs2_sdk/entity/services.cpp @@ -53,10 +53,8 @@ CBasePlayerWeapon* CCSPlayer_ItemServices::GiveNamedItemAws(const char* item) no const auto team = pPawn->m_iTeamNum(); g_bAwsChangingTeam = true; pPawn->m_iTeamNum(pInfo->m_iTeamNum); - const auto pWeapon = GiveNamedItem(item); - // Forcibly equip the weapon, because it spawns dropped in-world due to ZR enforcing mp_weapons_allow_* cvars against T's, which meant other players could pick up the weapon instead - pWeaponServices->EquipWeapon(pWeapon); + const auto pWeapon = GiveNamedItem(item); pPawn->m_iTeamNum(team); g_bAwsChangingTeam = false; diff --git a/src/cs2_sdk/entity/services.h b/src/cs2_sdk/entity/services.h index 15fe5b400..3fd7b1a9a 100644 --- a/src/cs2_sdk/entity/services.h +++ b/src/cs2_sdk/entity/services.h @@ -327,3 +327,14 @@ class CCSPlayer_ViewModelServices : public CPlayer_ViewModelServices pViewModel->m_nViewModelIndex = iIndex; } }; + +class CCSPlayer_PingServices : public CPlayerPawnComponent +{ + virtual ~CCSPlayer_PingServices() = 0; + +public: + DECLARE_SCHEMA_CLASS(CCSPlayer_PingServices); + + SCHEMA_FIELD_POINTER(GameTime_t, m_flPlayerPingTokens) + SCHEMA_FIELD(CHandle, m_hPlayerPing) +}; diff --git a/src/cs2fixes.cpp b/src/cs2fixes.cpp index 1b4ae95ca..d9637d0bb 100644 --- a/src/cs2fixes.cpp +++ b/src/cs2fixes.cpp @@ -41,6 +41,7 @@ #include "gameevents.pb.h" #include "gamesystem.h" #include "httpmanager.h" +#include "hud_manager.h" #include "icvar.h" #include "idlemanager.h" #include "interface.h" @@ -123,6 +124,7 @@ SH_DECL_HOOK2(IGameEventManager2, LoadEventsFromFile, SH_NOATTRIB, 0, int, const SH_DECL_MANUALHOOK1_void(GoToIntermission, 0, 0, 0, bool); SH_DECL_MANUALHOOK2_void(PhysicsTouchShuffle, 0, 0, 0, CUtlVector*, bool); SH_DECL_MANUALHOOK3_void(DropWeapon, 0, 0, 0, CBasePlayerWeapon*, Vector*, Vector*); +SH_DECL_HOOK1_void(IServer, SetGameSpawnGroupMgr, SH_NOATTRIB, 0, IGameSpawnGroupMgr*); CS2Fixes g_CS2Fixes; @@ -135,7 +137,8 @@ IVEngineServer2* g_pEngineServer2 = nullptr; CGameConfig* g_GameConfig = nullptr; ISteamHTTP* g_http = nullptr; CSteamGameServerAPIContext g_steamAPI; -CCSGameRules* g_pGameRules = nullptr; // Will be null between map end & new map startup, null check if necessary! +CCSGameRules* g_pGameRules = nullptr; // Will be null between map end & new map startup, null check if necessary! +CSpawnGroupMgrGameSystem* g_pSpawnGroupMgr = nullptr; // Will be null between map end & new map startup, null check if necessary! int g_iCGamePlayerEquipUseId = -1; int g_iCGamePlayerEquipPrecacheId = -1; int g_iCreateWorkshopMapGroupId = -1; @@ -145,6 +148,7 @@ int g_iLoadEventsFromFileId = -1; int g_iGoToIntermissionId = -1; int g_iPhysicsTouchShuffle = -1; int g_iWeaponServiceDropWeaponId = -1; +int g_iSetGameSpawnGroupMgrId = -1; CGameEntitySystem* GameEntitySystem() { @@ -436,6 +440,9 @@ bool CS2Fixes::Unload(char* error, size_t maxlen) SH_REMOVE_HOOK_ID(g_iGoToIntermissionId); SH_REMOVE_HOOK_ID(g_iCGamePlayerEquipUseId); + if (g_iSetGameSpawnGroupMgrId != -1) + SH_REMOVE_HOOK_ID(g_iSetGameSpawnGroupMgrId); + if (g_iCGamePlayerEquipPrecacheId != -1) SH_REMOVE_HOOK_ID(g_iCGamePlayerEquipPrecacheId); @@ -601,6 +608,9 @@ void CS2Fixes::Hook_StartupServer(const GameSessionConfiguration_t& config, ISou g_pEntitySystem = GameEntitySystem(); g_pEntitySystem->AddListenerEntity(g_pEntityListener); + if (g_pNetworkServerService->GetIGameServer()) + g_iSetGameSpawnGroupMgrId = SH_ADD_HOOK(IServer, SetGameSpawnGroupMgr, g_pNetworkServerService->GetIGameServer(), SH_MEMBER(this, &CS2Fixes::Hook_SetGameSpawnGroupMgr), false); + Message("Hook_StartupServer: %s\n", pszMapName); RegisterEventListeners(); @@ -649,6 +659,11 @@ void CS2Fixes::Hook_GameServerSteamAPIDeactivated() RETURN_META(MRES_IGNORED); } +uint32 GetSoundEventHash(const char* pszSoundEventName) +{ + return MurmurHash2LowerCase(pszSoundEventName, 0x53524332); +} + void CS2Fixes::Hook_PostEvent(CSplitScreenSlot nSlot, bool bLocalOnly, int nClientCount, const uint64* clients, INetworkMessageInternal* pEvent, const CNetMessage* pData, unsigned long nSize, NetChannelBufType_t bufType) { @@ -712,14 +727,67 @@ void CS2Fixes::Hook_PostEvent(CSplitScreenSlot nSlot, bool bLocalOnly, int nClie } else if (g_cvarEnableStopSound.Get() && info->m_MessageId == GE_SosStartSoundEvent) { + static std::set soundEventHashes; auto msg = const_cast(pData)->ToPB(); - if (msg->soundevent_hash() == MurmurHash2LowerCase("Weapon_Revolver.Prepare", 0x53524332)) + ExecuteOnce( + soundEventHashes.insert(GetSoundEventHash("Weapon_Knife.HitWall")); + soundEventHashes.insert(GetSoundEventHash("Weapon_Knife.Slash")); + soundEventHashes.insert(GetSoundEventHash("Weapon_Knife.Hit")); + soundEventHashes.insert(GetSoundEventHash("Weapon_Knife.Stab")); + soundEventHashes.insert(GetSoundEventHash("Weapon_sg556.ZoomIn")); + soundEventHashes.insert(GetSoundEventHash("Weapon_sg556.ZoomOut")); + soundEventHashes.insert(GetSoundEventHash("Weapon_AUG.ZoomIn")); + soundEventHashes.insert(GetSoundEventHash("Weapon_AUG.ZoomOut")); + soundEventHashes.insert(GetSoundEventHash("Weapon_SSG08.Zoom")); + soundEventHashes.insert(GetSoundEventHash("Weapon_SSG08.ZoomOut")); + soundEventHashes.insert(GetSoundEventHash("Weapon_SCAR20.Zoom")); + soundEventHashes.insert(GetSoundEventHash("Weapon_SCAR20.ZoomOut")); + soundEventHashes.insert(GetSoundEventHash("Weapon_G3SG1.Zoom")); + soundEventHashes.insert(GetSoundEventHash("Weapon_G3SG1.ZoomOut")); + soundEventHashes.insert(GetSoundEventHash("Weapon_AWP.Zoom")); + soundEventHashes.insert(GetSoundEventHash("Weapon_AWP.ZoomOut")); + soundEventHashes.insert(GetSoundEventHash("Weapon_Revolver.Prepare")); + soundEventHashes.insert(GetSoundEventHash("Weapon.AutoSemiAutoSwitch"));); + + if (!soundEventHashes.contains(msg->soundevent_hash())) + return; + + uint64 stopSoundMask = g_playerManager->GetStopSoundMask(); + uint64 silenceSoundMask = g_playerManager->GetSilenceSoundMask(); + + if (!msg->has_source_entity_index()) + return; + + CBaseEntity* pSourceEntity = (CBaseEntity*)g_pEntitySystem->GetEntityInstance(CEntityIndex(msg->source_entity_index())); + int playerSlot = -1; + + if (!pSourceEntity) + return; + + if (!V_strcasecmp(pSourceEntity->GetClassname(), "player")) { - // Filter out people using stop/silence sound from hearing R8 windup - *(uint64*)clients &= ~g_playerManager->GetStopSoundMask(); - *(uint64*)clients &= ~g_playerManager->GetSilenceSoundMask(); + playerSlot = ((CCSPlayerPawn*)pSourceEntity)->GetController()->GetPlayerSlot(); } + else if (!V_strncasecmp(pSourceEntity->GetClassname(), "weapon_", 7)) + { + CCSPlayerPawn* pPawn = (CCSPlayerPawn*)pSourceEntity->m_hOwnerEntity().Get(); + + if (pPawn && pPawn->IsPawn()) + playerSlot = pPawn->GetController()->GetPlayerSlot(); + } + + // Remove player who triggered this sound from masks + // Because some of these sounds never get played locally (Zoom's, Knife Hit/Stab) + if (playerSlot != -1 && g_playerManager->IsPlayerUsingStopSound(playerSlot)) + stopSoundMask &= ~((uint64)1 << playerSlot); + + if (playerSlot != -1 && g_playerManager->IsPlayerUsingSilenceSound(playerSlot)) + silenceSoundMask &= ~((uint64)1 << playerSlot); + + // Filter out people using stop/silence sound from hearing this sound from other players + *(uint64*)clients &= ~stopSoundMask; + *(uint64*)clients &= ~silenceSoundMask; } } @@ -1152,6 +1220,72 @@ int CS2Fixes::Hook_LoadEventsFromFile(const char* filename, bool bSearchAll) RETURN_META_VALUE(MRES_IGNORED, 0); } +void CS2Fixes::Hook_SetGameSpawnGroupMgr(IGameSpawnGroupMgr* pSpawnGroupMgr) +{ + // This also resets our stored pointer on deletion, since null gets passed into this function, nice! + g_pSpawnGroupMgr = (CSpawnGroupMgrGameSystem*)pSpawnGroupMgr; +} + +void* CS2Fixes::OnMetamodQuery(const char* iface, int* ret) +{ + if (V_strcmp(iface, CS2FIXES_INTERFACE)) + { + if (ret) + *ret = META_IFACE_FAILED; + + return nullptr; + } + + if (ret) + *ret = META_IFACE_OK; + + return static_cast(&g_CS2Fixes); +} + +std::uint64_t CS2Fixes::GetAdminFlags(std::uint64_t iSteam64ID) const +{ + if (!g_pAdminSystem) + return 0; + + const CAdmin* admin = g_pAdminSystem->FindAdmin(static_cast(iSteam64ID)); + if (!admin) + return 0; + + return admin->GetFlags(); +} + +bool CS2Fixes::SetAdminFlags(std::uint64_t iSteam64ID, std::uint64_t iFlags) +{ + if (!g_pAdminSystem) + return false; + + CAdmin* admin = g_pAdminSystem->FindAdmin(static_cast(iSteam64ID)); + g_pAdminSystem->AddOrUpdateAdmin(static_cast(iSteam64ID), iFlags, admin ? admin->GetImmunity() : 0); + return true; +} + +int CS2Fixes::GetAdminImmunity(std::uint64_t iSteam64ID) const +{ + if (!g_pAdminSystem) + return 0; + + const CAdmin* admin = g_pAdminSystem->FindAdmin(static_cast(iSteam64ID)); + if (!admin) + return 0; + + return admin->GetImmunity(); +} + +bool CS2Fixes::SetAdminImmunity(std::uint64_t iSteam64ID, std::uint32_t iImmunity) +{ + if (!g_pAdminSystem) + return false; + + CAdmin* admin = g_pAdminSystem->FindAdmin(static_cast(iSteam64ID)); + g_pAdminSystem->AddOrUpdateAdmin(static_cast(iSteam64ID), admin ? admin->GetFlags() : 0, iImmunity); + return true; +} + void CS2Fixes::OnLevelInit(char const* pMapName, char const* pMapEntities, char const* pOldLevel, @@ -1182,6 +1316,8 @@ void CS2Fixes::OnLevelInit(char const* pMapName, if (g_cvarEnableEntWatch.Get()) EW_OnLevelInit(pMapName); + + StartFlashingFixTimer(); } void CS2Fixes::OnLevelShutdown() @@ -1200,48 +1336,4 @@ bool CS2Fixes::Pause(char* error, size_t maxlen) bool CS2Fixes::Unpause(char* error, size_t maxlen) { return true; -} - -const char* CS2Fixes::GetLicense() -{ - return "GPL v3 License"; -} - -const char* CS2Fixes::GetVersion() -{ -#ifndef CS2FIXES_VERSION - #define CS2FIXES_VERSION "1.7-dev" -#endif - - return CS2FIXES_VERSION; // defined by the build script -} - -const char* CS2Fixes::GetDate() -{ - return __DATE__; -} - -const char* CS2Fixes::GetLogTag() -{ - return "CS2Fixes"; -} - -const char* CS2Fixes::GetAuthor() -{ - return "xen, Poggu, and the Source2ZE community"; -} - -const char* CS2Fixes::GetDescription() -{ - return "A bunch of experiments thrown together into one big mess of a plugin."; -} - -const char* CS2Fixes::GetName() -{ - return "CS2Fixes"; -} - -const char* CS2Fixes::GetURL() -{ - return "https://github.com/Source2ZE/CS2Fixes"; } \ No newline at end of file diff --git a/src/cs2fixes.h b/src/cs2fixes.h index e334bed01..a1f32f7cc 100644 --- a/src/cs2fixes.h +++ b/src/cs2fixes.h @@ -19,13 +19,21 @@ #pragma once +#include "gamesystems/spawngroup_manager.h" #include "igameevents.h" #include "networksystem/inetworkserializer.h" +#include "public/ics2fixes.h" #include #include #include #include +#ifdef AMBUILD + #include "version_gen.h" +#else + #include "version_gen_placeholder.h" +#endif + struct CTakeDamageInfoContainer; class CCSPlayer_MovementServices; class CServerSideClient; @@ -33,7 +41,7 @@ struct TouchLinked_t; class CCSPlayer_WeaponServices; class CBasePlayerWeapon; -class CS2Fixes : public ISmmPlugin, public IMetamodListener +class CS2Fixes : public ISmmPlugin, public IMetamodListener, public ICS2Fixes { public: bool Load(PluginId id, ISmmAPI* ismm, char* error, size_t maxlen, bool late); @@ -83,16 +91,24 @@ class CS2Fixes : public ISmmPlugin, public IMetamodListener void Hook_CheckMovingGround(double frametime); void Hook_DropWeaponPost(CBasePlayerWeapon* pWeapon, Vector* pVecTarget, Vector* pVelocity); int Hook_LoadEventsFromFile(const char* filename, bool bSearchAll); + void Hook_SetGameSpawnGroupMgr(IGameSpawnGroupMgr* pSpawnGroupMgr); + +public: // MetaMod API + void* OnMetamodQuery(const char* iface, int* ret); + std::uint64_t GetAdminFlags(std::uint64_t iSteam64ID) const override; + bool SetAdminFlags(std::uint64_t iSteam64ID, std::uint64_t iFlags) override; + int GetAdminImmunity(std::uint64_t iSteam64ID) const override; + bool SetAdminImmunity(std::uint64_t iSteam64ID, std::uint32_t iImmunity) override; public: - const char* GetAuthor(); - const char* GetName(); - const char* GetDescription(); - const char* GetURL(); - const char* GetLicense(); - const char* GetVersion(); - const char* GetDate(); - const char* GetLogTag(); + const char* GetAuthor() { return PLUGIN_AUTHOR; } + const char* GetName() { return PLUGIN_DISPLAY_NAME; } + const char* GetDescription() { return PLUGIN_DESCRIPTION; } + const char* GetURL() { return PLUGIN_URL; } + const char* GetLicense() { return PLUGIN_LICENSE; } + const char* GetVersion() { return PLUGIN_FULL_VERSION; } + const char* GetDate() { return __DATE__; } + const char* GetLogTag() { return PLUGIN_LOGTAG; } }; extern CS2Fixes g_CS2Fixes; diff --git a/src/detours.cpp b/src/detours.cpp index 6a1178fed..238f8e2ff 100644 --- a/src/detours.cpp +++ b/src/detours.cpp @@ -85,6 +85,7 @@ DECLARE_DETOUR(CBasePlayerPawn_GetEyePosition, Detour_CBasePlayerPawn_GetEyePosi DECLARE_DETOUR(CBasePlayerPawn_GetEyeAngles, Detour_CBasePlayerPawn_GetEyeAngles); DECLARE_DETOUR(CBaseFilter_InputTestActivator, Detour_CBaseFilter_InputTestActivator); DECLARE_DETOUR(GameSystem_Think_CheckSteamBan, Detour_GameSystem_Think_CheckSteamBan); +DECLARE_DETOUR(CCSPlayer_ItemServices_CanAcquire, Detour_CCSPlayer_ItemServices_CanAcquire); CConVar g_cvarBlockMolotovSelfDmg("cs2f_block_molotov_self_dmg", FCVAR_NONE, "Whether to block self-damage from molotovs", false); CConVar g_cvarBlockAllDamage("cs2f_block_all_dmg", FCVAR_NONE, "Whether to block all damage to players", false); @@ -369,9 +370,6 @@ void FASTCALL Detour_UTIL_SayText2Filter( bool FASTCALL Detour_CCSPlayer_WeaponServices_CanUse(CCSPlayer_WeaponServices* pWeaponServices, CBasePlayerWeapon* pPlayerWeapon) { - if (g_cvarEnableZR.Get() && !ZR_Detour_CCSPlayer_WeaponServices_CanUse(pWeaponServices, pPlayerWeapon)) - return false; - if (g_cvarEnableEntWatch.Get() && !EW_Detour_CCSPlayer_WeaponServices_CanUse(pWeaponServices, pPlayerWeapon)) return false; @@ -412,7 +410,7 @@ bool FASTCALL Detour_CEntityIdentity_AcceptInput(CEntityIdentity* pThis, CUtlSym if ((value->m_type == FIELD_CSTRING || value->m_type == FIELD_STRING) && value->m_pszString) flDuration = V_StringToFloat32(value->m_pszString, 0.f); else - flDuration = value->m_float; + flDuration = value->m_float32; CCSPlayerPawn* pPawn = reinterpret_cast(pThis->m_pInstance); @@ -426,7 +424,7 @@ bool FASTCALL Detour_CEntityIdentity_AcceptInput(CEntityIdentity* pThis, CUtlSym if ((value->m_type == FIELD_CSTRING || value->m_type == FIELD_STRING) && value->m_pszString) iScore = V_StringToInt32(value->m_pszString, 0); else - iScore = value->m_int; + iScore = value->m_int32; CCSPlayerPawn* pPawn = reinterpret_cast(pThis->m_pInstance); @@ -450,7 +448,17 @@ bool FASTCALL Detour_CEntityIdentity_AcceptInput(CEntityIdentity* pThis, CUtlSym if (const auto pModelEntity = reinterpret_cast(pThis->m_pInstance)->AsBaseModelEntity()) { if ((value->m_type == FIELD_CSTRING || value->m_type == FIELD_STRING) && value->m_pszString) + { + // Player color may have been changed by zclass/server customization, so reset it first + // This also means if maps want to change player color, it needs to be done after the SetModel input + if (pModelEntity->IsPawn()) + { + int originalAlpha = pModelEntity->m_clrRender().a(); + pModelEntity->m_clrRender = Color(255, 255, 255, originalAlpha); + } + pModelEntity->SetModel(value->m_pszString); + } return true; } } @@ -737,6 +745,19 @@ void FASTCALL Detour_GameSystem_Think_CheckSteamBan() pMap->RemoveAll(); } +AcquireResult FASTCALL Detour_CCSPlayer_ItemServices_CanAcquire(CCSPlayer_ItemServices* pItemServices, CEconItemView* pEconItem, AcquireMethod iAcquireMethod, uint64_t unk4) +{ + if (g_cvarEnableZR.Get()) + { + AcquireResult zrResult = ZR_Detour_CCSPlayer_ItemServices_CanAcquire(pItemServices, pEconItem); + + if (zrResult != AcquireResult::Allowed) + return zrResult; + } + + return CCSPlayer_ItemServices_CanAcquire(pItemServices, pEconItem, iAcquireMethod, unk4); +} + bool InitDetours(CGameConfig* gameConfig) { bool success = true; diff --git a/src/detours.h b/src/detours.h index 6502123b8..a07a53f0a 100644 --- a/src/detours.h +++ b/src/detours.h @@ -36,6 +36,7 @@ class CGameRules; class CTakeDamageInfo; class CCSPlayer_WeaponServices; class CCSPlayer_MovementServices; +class CCSPlayer_ItemServices; class CBasePlayerWeapon; class INetworkMessageInternal; class IEngineServiceMgr; @@ -50,6 +51,28 @@ class CCSPlayer_UseServices; class CTraceFilter; class Vector; class QAngle; +class CEconItemView; + +enum class AcquireMethod +{ + PickUp, + Buy, +}; + +enum class AcquireResult +{ + Allowed, + InvalidItem, + AlreadyOwned, + AlreadyPurchased, + ReachedGrenadeTypeLimit, + ReachedGrenadeTotalLimit, + NotAllowedByTeam, + NotAllowedByMap, + NotAllowedByMode, + NotAllowedForPurchase, + NotAllowedByProhibition, +}; bool InitDetours(CGameConfig* gameConfig); void FlushAllDetours(); @@ -86,4 +109,5 @@ Vector FASTCALL Detour_CBasePlayerPawn_GetEyePosition(CBasePlayerPawn*); QAngle FASTCALL Detour_CBasePlayerPawn_GetEyeAngles(CBasePlayerPawn*); #endif void FASTCALL Detour_CBaseFilter_InputTestActivator(CBaseEntity* pThis, InputData_t& inputdata); -void FASTCALL Detour_GameSystem_Think_CheckSteamBan(); \ No newline at end of file +void FASTCALL Detour_GameSystem_Think_CheckSteamBan(); +AcquireResult FASTCALL Detour_CCSPlayer_ItemServices_CanAcquire(CCSPlayer_ItemServices* pItemServices, CEconItemView* pEconItem, AcquireMethod iAcquireMethod, uint64_t unk4); \ No newline at end of file diff --git a/src/entities.cpp b/src/entities.cpp index ca184f344..c92d42656 100644 --- a/src/entities.cpp +++ b/src/entities.cpp @@ -1,4 +1,4 @@ -/** +/** * ============================================================================= * CS2Fixes * Copyright (C) 2023-2025 Source2ZE @@ -251,10 +251,10 @@ namespace CGameUIHandler static std::unordered_map s_repository; - inline uint64 GetButtons(CPlayer_MovementServices* pMovement) + inline uint64 GetButtons(CPlayer_MovementServices* pMovement, int key = 0) { const auto buttonStates = pMovement->m_nButtons().m_pButtonStates(); - const auto buttons = buttonStates[0]; + const auto buttons = buttonStates[key]; return buttons; } @@ -266,8 +266,9 @@ namespace CGameUIHandler const auto spawnFlags = pEntity->m_spawnflags(); const auto buttons = GetButtons(pMovement); + const auto scrolls = GetButtons(pMovement, 2); - if ((spawnFlags & CGameUI::SF_GAMEUI_JUMP_DEACTIVATE) != 0 && (buttons & IN_JUMP) != 0) + if (((spawnFlags & CGameUI::SF_GAMEUI_JUMP_DEACTIVATE) != 0) && ((buttons & IN_JUMP) != 0 || (scrolls & IN_JUMP) != 0)) { DelayInput(pEntity, pPlayer, "Deactivate"); return BAD_BUTTONS; diff --git a/src/entwatch.cpp b/src/entwatch.cpp index a65bf6392..0c4044c8e 100644 --- a/src/entwatch.cpp +++ b/src/entwatch.cpp @@ -91,16 +91,12 @@ void ItemGlowDistanceChanged(CConVar* ref, CSplitScreenSlot nSlot, const in continue; if (pItemWeapon->m_Glow().m_bGlowing) - { if (newValue > 0) pItemWeapon->m_Glow().m_nGlowRange = newValue; else item->EndGlow(); - } else if (item->bShouldGlow && newValue > 0) - { item->StartGlow(); - } } } CConVar g_cvarItemDroppedGlow("entwatch_glow", FCVAR_NONE, "Distance that dropped item weapon glow will be visible (0 = glow disabled)", 1000, true, 0, false, 0, ItemGlowDistanceChanged); @@ -981,7 +977,7 @@ void EWItemInstance::StartGlow() Message("Error getting weapon entity while creating item glow.\n"); return; } - + int r = colorGlow.r(); int g = colorGlow.g(); int b = colorGlow.b(); @@ -1027,7 +1023,7 @@ bool EWItemInstance::IsEmpty() // Empty items don't glow when dropped, so have to be careful since some items can recharge // Only return true if ALL handlers that appear in any way (chat/ui) are empty (non-counter max-uses) // If it has no visible handlers, its not empty - // + // // TODO: maybe add an optional config setting for whether a handler is rechargable (depends if people complain) if (vecHandlers.size() < 1) @@ -1043,9 +1039,7 @@ bool EWItemInstance::IsEmpty() if ((vecHandlers[i]->bShowHud || vecHandlers[i]->bShowUse) && vecHandlers[i]->szOutput != "") { if (vecHandlers[i]->IsCounter() || vecHandlers[i]->mode != EWHandlerMode::MaxUses) - { return false; - } // Maxuses handler (can be empty) // Check if it is empty @@ -2343,7 +2337,7 @@ void EW_FireOutput(const CEntityIOOutput* pThis, CEntityInstance* pActivator, CE // Message("Output for item %s (instance:%d) handler:%d outputname:%s\n", g_pEWHandler->vecItems[i]->szItemName, i, j, pThis->m_pDesc->m_pName); if (handler->type == EWHandlerType::CounterDown || handler->type == EWHandlerType::CounterUp) - handler->Use(value->m_float); + handler->Use(value->m_float32); else handler->Use(0.0); } diff --git a/src/entwatch.h b/src/entwatch.h index d7251cf1c..8ee480233 100644 --- a/src/entwatch.h +++ b/src/entwatch.h @@ -175,7 +175,7 @@ struct EWItemInstance : EWItem /* Current instance of defined items */ sClantag(""), bHasThisClantag(false), iTeamNum(CS_TEAM_NONE), - bShouldGlow(false) {}; + bShouldGlow(false){}; bool RegisterHandler(CBaseEntity* pEnt, int iHandlerTemplateNum); bool RemoveHandler(CBaseEntity* pEnt); int FindHandlerByEntIndex(int indexToFind); diff --git a/src/events.cpp b/src/events.cpp index 18026ce7b..6eb6a88dd 100644 --- a/src/events.cpp +++ b/src/events.cpp @@ -26,6 +26,7 @@ #include "entity/cgamerules.h" #include "entwatch.h" #include "eventlistener.h" +#include "hud_manager.h" #include "idlemanager.h" #include "leader.h" #include "map_votes.h" @@ -128,6 +129,7 @@ GAME_EVENT_F(player_team) } CConVar g_cvarNoblock("cs2f_noblock_enable", FCVAR_NONE, "Whether to use player noblock, which sets debris collision on every player", false); +CConVar g_cvarFreeArmor("cs2f_free_armor", FCVAR_NONE, "Whether kevlar (1+) and/or helmet (2) are given automatically", 0, true, 0, true, 2); GAME_EVENT_F(player_spawn) { @@ -174,24 +176,21 @@ GAME_EVENT_F(player_spawn) return -1.0f; }); - // And this needs even more delay..? Don't even know if this is enough, bug can't be reproduced - new CTimer(0.1f, false, false, [hController]() { - CCSPlayerController* pController = hController.Get(); + CCSPlayerPawn* pPawn = (CCSPlayerPawn*)pController->GetPawn(); - if (!pController) - return -1.0f; + if (!pPawn) + return; - CBasePlayerPawn* pPawn = pController->GetPawn(); + CCSPlayer_ItemServices* pItemServices = pPawn->m_pItemServices(); - if (pPawn) - { - // Fix new haunted CS2 bug? https://www.reddit.com/r/cs2/comments/1glvg9s/thank_you_for_choosing_anubis_airlines/ - // We've seen this several times across different maps at this point - pPawn->m_vecAbsVelocity = Vector(0, 0, 0); - } + if (!pItemServices) + return; - return -1.0f; - }); + // Dumb workaround for mp_free_armor breaking kevlar rebuys in buy menu + if (g_cvarFreeArmor.GetInt() == 1) + pItemServices->GiveNamedItem("item_kevlar"); + else if (g_cvarFreeArmor.GetInt() == 2) + pItemServices->GiveNamedItem("item_assaultsuit"); } CConVar g_cvarEnableTopDefender("cs2f_topdefender_enable", FCVAR_NONE, "Whether to use TopDefender", false); @@ -259,6 +258,10 @@ GAME_EVENT_F(round_start) if (g_cvarFullAllTalk.Get()) g_pEngineServer2->ServerCommand("sv_full_alltalk 1"); + // Ensure there's no warmup, because mp_warmup_online_enabled gets randomly ignored for some reason, this is a problem with cs2f_fix_hud_flashing + if (g_cvarFixHudFlashing.Get() && g_pGameRules && g_pGameRules->m_bWarmupPeriod) + g_pEngineServer2->ServerCommand("mp_warmup_end"); + if (!g_cvarEnableTopDefender.Get() || !GetGlobals()) return; @@ -277,6 +280,9 @@ GAME_EVENT_F(round_start) GAME_EVENT_F(round_end) { + if (g_cvarFixHudFlashing.Get() && g_pGameRules) + g_pGameRules->m_bGameRestart = false; + if (!g_cvarEnableTopDefender.Get() || !GetGlobals()) return; diff --git a/src/gamesystem.cpp b/src/gamesystem.cpp index 23a4562ac..80db12c8a 100644 --- a/src/gamesystem.cpp +++ b/src/gamesystem.cpp @@ -35,6 +35,8 @@ extern CGlobalVars* GetGlobals(); extern CGameConfig* g_GameConfig; extern CCSGameRules* g_pGameRules; +extern CSpawnGroupMgrGameSystem* g_pSpawnGroupMgr; +extern CUtlVector* GetClientList(); CBaseGameSystemFactory** CBaseGameSystemFactory::sm_pFirst = nullptr; @@ -186,3 +188,26 @@ GS_EVENT_MEMBER(CGameSystem, GameShutdown) { g_pGameRules = nullptr; } + +GS_EVENT_MEMBER(CGameSystem, PostSpawnGroupLoad) +{ + if (!g_pSpawnGroupMgr) + return; + + CUtlVector vecActualSpawnGroups; + addresses::GetSpawnGroups(g_pSpawnGroupMgr, &vecActualSpawnGroups); + + auto pClients = GetClientList(); + + // Ensure clients have no leaked spawngroups every time a new one loads + // Due to a timing problem for leaked spawngroups with this callback, clients may have one lingering, this is fine since it'll still be taken care of next time a spawngroup loads + FOR_EACH_VEC(*pClients, i) + { + auto pClient = (*pClients)[i]; + + if (!pClient || pClient->m_vecLoadedSpawnGroups.Count() == vecActualSpawnGroups.Count()) + continue; + + pClient->m_vecLoadedSpawnGroups = vecActualSpawnGroups; + } +} \ No newline at end of file diff --git a/src/gamesystem.h b/src/gamesystem.h index 7571d7d35..bf88548d3 100644 --- a/src/gamesystem.h +++ b/src/gamesystem.h @@ -68,6 +68,7 @@ class CGameSystem : public CBaseGameSystem GS_EVENT(ServerPreEntityThink); GS_EVENT(ServerPostEntityThink); GS_EVENT(GameShutdown); + GS_EVENT(PostSpawnGroupLoad); void Shutdown() override { diff --git a/src/httpmanager.cpp b/src/httpmanager.cpp index f4b8f6a43..f1fd55bc0 100644 --- a/src/httpmanager.cpp +++ b/src/httpmanager.cpp @@ -97,7 +97,7 @@ void HTTPManager::TrackedRequest::OnHTTPRequestCompleted(HTTPRequestCompleted_t* // Allow error callback even if invalid json, since error code can provide useful info m_callbackError(arg->m_hRequest, arg->m_eStatusCode, json()); } - else if (!jsonResponse.is_discarded()) + else if (!jsonResponse.is_discarded() && m_callbackCompleted) m_callbackCompleted(arg->m_hRequest, jsonResponse); delete[] response; diff --git a/src/leader.cpp b/src/leader.cpp index 16e27203b..b71c444a8 100644 --- a/src/leader.cpp +++ b/src/leader.cpp @@ -57,8 +57,28 @@ CUtlVector g_vecLeaders; static int g_iMarkerCount = 0; static bool g_bPingWithLeader = true; +static void RemoveLeader(CCSPlayerController* ccsPly); + // CONVARS -CConVar g_cvarEnableLeader("cs2f_leader_enable", FCVAR_NONE, "Whether to enable Leader features", false); +CConVar g_cvarEnableLeader( + "cs2f_leader_enable", FCVAR_NONE, "Whether to enable Leader features", false, + [](CConVar* cvar, CSplitScreenSlot slot, const bool* new_val, const bool* old_val) { + if ((new_val && *new_val) || !GetGlobals()) + return; + + // Remove all active leaders if disabling convar + for (int i = 0; i < GetGlobals()->maxClients; i++) + { + CCSPlayerController* ccsPly = CCSPlayerController::FromSlot(i); + ZEPlayer* pPlayer = g_playerManager->GetPlayer(i); + + if (!ccsPly || !pPlayer || !pPlayer->IsLeader()) + continue; + + RemoveLeader(ccsPly); + } + }); + CConVar g_cvarlLeaderVoteRatio("cs2f_leader_vote_ratio", FCVAR_NONE, "Vote ratio needed for player to become a leader", 0.15f, true, 0.0f, true, 1.0f); CConVar g_cvarLeaderActionsHumanOnly("cs2f_leader_actions_ct_only", FCVAR_NONE, "Whether to allow leader actions (like !beacon) only from human team", true); CConVar g_cvarLeaderMarkerHumanOnly("cs2f_leader_marker_ct_only", FCVAR_NONE, "Whether to have zombie leaders' player_pings spawn in particle markers or not", true); @@ -91,7 +111,7 @@ Color Leader_GetColor(std::string strColor, ZEPlayer* zpUser = nullptr, CCSPlaye } // This also wipes any invalid entries from g_vecLeaders -std::pair GetLeaders() +static std::pair GetLeaders() { int iLeaders = 0; std::string strLeaders = ""; @@ -170,7 +190,7 @@ std::pair GetCount(LeaderVisual iType) return std::make_pair(iCount, strPlayerNames.substr(0, strPlayerNames.length() - 2)); } -bool Leader_SetNewLeader(ZEPlayer* zpLeader, std::string strColor = "") +static bool Leader_SetNewLeader(ZEPlayer* zpLeader, std::string strColor = "") { CCSPlayerController* pLeader = CCSPlayerController::FromSlot(zpLeader->GetPlayerSlot()); CCSPlayerPawn* pawnLeader = (CCSPlayerPawn*)pLeader->GetPawn(); @@ -252,7 +272,7 @@ void Leader_ApplyLeaderVisuals(CCSPlayerPawn* pPawn) zpLeader->SetTracerColor(Color(0, 0, 0, 0)); } -void Leader_RemoveLeaderVisuals(CCSPlayerPawn* pPawn) +static void Leader_RemoveLeaderVisuals(CCSPlayerPawn* pPawn) { g_pZRPlayerClassManager->ApplyPreferredOrDefaultHumanClassVisuals(pPawn); @@ -268,7 +288,37 @@ void Leader_RemoveLeaderVisuals(CCSPlayerPawn* pPawn) zpLeader->EndGlow(); } -bool Leader_CreateDefendMarker(ZEPlayer* pPlayer, Color clrTint, int iDuration) +static void RemoveLeader(CCSPlayerController* ccsLeader) +{ + if (!ccsLeader) + return; + + ZEPlayer* zpLeader = ccsLeader->GetZEPlayer(); + + if (!zpLeader || !zpLeader->IsLeader()) + return; + + ccsLeader->m_iScore() = ccsLeader->m_iScore() - g_cvarLeaderExtraScore.Get(); + zpLeader->SetLeader(false); + zpLeader->SetLeaderColor(Color(0, 0, 0, 0)); + zpLeader->SetTracerColor(Color(0, 0, 0, 0)); + zpLeader->SetBeaconColor(Color(0, 0, 0, 0)); + zpLeader->SetGlowColor(Color(0, 0, 0, 0)); + FOR_EACH_VEC_BACK(g_vecLeaders, i) + { + if (g_vecLeaders[i] == zpLeader) + { + g_vecLeaders.Remove(i); + break; + } + } + + CCSPlayerPawn* pLeader = (ccsLeader->m_iTeamNum != CS_TEAM_CT || !ccsLeader->IsAlive()) ? nullptr : (CCSPlayerPawn*)ccsLeader->GetPawn(); + if (pLeader) + Leader_RemoveLeaderVisuals(pLeader); +} + +static bool Leader_CreateDefendMarker(ZEPlayer* pPlayer, Color clrTint, int iDuration) { CCSPlayerController* pController = CCSPlayerController::FromSlot(pPlayer->GetPlayerSlot()); CCSPlayerPawn* pPawn = (CCSPlayerPawn*)pController->GetPawn(); @@ -357,7 +407,16 @@ void Leader_PostEventAbstract_Source1LegacyGameEvent(const uint64* clients, cons Vector vecOrigin = pEntity->GetAbsOrigin(); vecOrigin.z += 10; - pPlayer->CreateMark(15, vecOrigin); // 6.1 seconds is time of default ping if you want it to match + pPlayer->CreateMark(6.1, vecOrigin); // 6.1 seconds is time of default ping if you want it to match + + CCSPlayerPawn* pPawn = pController->GetPlayerPawn(); + if (pPawn && pPawn->m_pPingServices) + { + // Remove ping cooldown for leaders so they can spam it if needed + // Still prints the cooldown message to the player though + for (int i = 0; i < 5; i++) + pPawn->m_pPingServices->m_flPlayerPingTokens[i] = 0; + } return; } @@ -941,24 +1000,7 @@ CON_COMMAND_CHAT_FLAGS(removeleader, "[name] - Remove leader status from a playe return; } - pTarget->m_iScore() = pTarget->m_iScore() - g_cvarLeaderExtraScore.Get(); - pPlayerTarget->SetLeader(false); - pPlayerTarget->SetLeaderColor(Color(0, 0, 0, 0)); - pPlayerTarget->SetTracerColor(Color(0, 0, 0, 0)); - pPlayerTarget->SetBeaconColor(Color(0, 0, 0, 0)); - pPlayerTarget->SetGlowColor(Color(0, 0, 0, 0)); - FOR_EACH_VEC_BACK(g_vecLeaders, i) - { - if (g_vecLeaders[i] == pPlayerTarget) - { - g_vecLeaders.Remove(i); - break; - } - } - - CCSPlayerPawn* pPawn = (pTarget->m_iTeamNum != CS_TEAM_CT || !pTarget->IsAlive()) ? nullptr : (CCSPlayerPawn*)pTarget->GetPawn(); - if (pPawn) - Leader_RemoveLeaderVisuals(pPawn); + RemoveLeader(pTarget); if (player == pTarget) ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "%s resigned from being a leader.", player->GetPlayerName()); @@ -987,24 +1029,7 @@ CON_COMMAND_CHAT(resign, "- Remove leader status from yourself") return; } - player->m_iScore() = player->m_iScore() - g_cvarLeaderExtraScore.Get(); - pPlayer->SetLeader(false); - pPlayer->SetLeaderColor(Color(0, 0, 0, 0)); - pPlayer->SetTracerColor(Color(0, 0, 0, 0)); - pPlayer->SetBeaconColor(Color(0, 0, 0, 0)); - pPlayer->SetGlowColor(Color(0, 0, 0, 0)); - FOR_EACH_VEC_BACK(g_vecLeaders, i) - { - if (g_vecLeaders[i] == pPlayer) - { - g_vecLeaders.Remove(i); - break; - } - } - - CCSPlayerPawn* pPawn = (player->m_iTeamNum != CS_TEAM_CT || !player->IsAlive()) ? nullptr : (CCSPlayerPawn*)player->GetPawn(); - if (pPawn) - Leader_RemoveLeaderVisuals(pPawn); + RemoveLeader(player); ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "%s resigned from being a leader.", player->GetPlayerName()); } \ No newline at end of file diff --git a/src/map_votes.cpp b/src/map_votes.cpp index 3d94b710e..79bcaffe5 100644 --- a/src/map_votes.cpp +++ b/src/map_votes.cpp @@ -70,14 +70,8 @@ CON_COMMAND_CHAT_FLAGS(reload_map_list, "- Reload map list, also reloads current CALL_VIRTUAL(void, g_GameConfig->GetOffset("IGameTypes_CreateWorkshopMapGroup"), g_pGameTypes, "workshop"); // Updating the mapgroup requires reloading the map for everything to load properly - char sChangeMapCmd[128] = ""; - - if (g_pMapVoteSystem->GetCurrentWorkshopMap() != 0) - V_snprintf(sChangeMapCmd, sizeof(sChangeMapCmd), "host_workshop_map %llu", g_pMapVoteSystem->GetCurrentWorkshopMap()); - else - V_snprintf(sChangeMapCmd, sizeof(sChangeMapCmd), "map %s", g_pMapVoteSystem->GetCurrentMapName()); + g_pMapVoteSystem->ReloadCurrentMap(); - g_pEngineServer2->ServerCommand(sChangeMapCmd); ClientPrint(player, HUD_PRINTTALK, CHAT_PREFIX "Map list reloaded!"); } @@ -273,7 +267,7 @@ void CMapVoteSystem::OnLevelInit(const char* pMapName) void CMapVoteSystem::StartVote() { - if (!g_pGameRules) + if (!g_cvarVoteManagerEnable.Get() || !g_pGameRules) return; m_bIsVoteOngoing = true; @@ -287,15 +281,7 @@ void CMapVoteSystem::StartVote() m_iVoteSize = std::min((int)vecPossibleMaps.size(), g_cvarVoteMaxMaps.Get()); bool bAbort = false; - static ConVarRefAbstract mp_endmatch_votenextmap("mp_endmatch_votenextmap"); - bool bVoteEnabled = mp_endmatch_votenextmap.GetBool(); - - if (!bVoteEnabled) - { - m_bIsVoteOngoing = false; - bAbort = true; - } - else if (m_iForcedNextMap != -1) + if (m_iForcedNextMap != -1) { new CTimer(6.0f, false, true, []() { g_pMapVoteSystem->FinishVote(); @@ -308,9 +294,15 @@ void CMapVoteSystem::StartVote() { ClientPrintAll(HUD_PRINTTALK, CHAT_PREFIX "Not enough maps available for map vote, aborting! Please have an admin loosen map limits."); Message("Not enough maps available for map vote, aborting!\n"); - g_pEngineServer2->ServerCommand("mp_match_end_changelevel 1"); // Allow game to auto-switch map again m_bIsVoteOngoing = false; bAbort = true; + + // Reload the current map as a fallback + // Previously we fell back to game behaviour which could choose a random map in mapgroup, but a crash bug with default map changes was introduced in 2025-05-07 CS2 update + new CTimer(6.0f, false, true, []() { + g_pMapVoteSystem->ReloadCurrentMap(); + return -1.0f; + }); } if (bAbort) @@ -325,10 +317,6 @@ void CMapVoteSystem::StartVote() return; } - // We're checking this later, so we can always disable the map vote if mp_endmatch_votenextmap is disabled - if (!g_cvarVoteManagerEnable.Get()) - return; - // Reset the player vote counts as the vote just started for (int i = 0; i < MAXPLAYERS; i++) m_arrPlayerVotes[i] = -1; @@ -583,7 +571,7 @@ std::vector CMapVoteSystem::GetNominatedMapsForVote() std::vector vecTiedNominations; // Nominations with tied nom counts std::vector vecChosenNominatedMaps; // Final vector of chosen nominations int iMapsToIncludeInNominate = std::min({(int)mapOriginalNominatedMaps.size(), g_cvarVoteMaxNominations.Get(), g_cvarVoteMaxMaps.Get()}); - int iMostNominations; + int iMostNominations = 0; auto rng = std::default_random_engine{std::random_device{}()}; // Select top maps by number of nominations @@ -659,9 +647,9 @@ std::vector CMapVoteSystem::GetMapIndexesFromSubstring(const char* sMapSubs return vecMaps; } -uint64 CMapVoteSystem::HandlePlayerMapLookup(CCSPlayerController* pController, const char* sMapSubstring, bool bAllowWorkshopID) +uint64 CMapVoteSystem::HandlePlayerMapLookup(CCSPlayerController* pController, const char* sMapSubstring, bool bAdmin) { - if (bAllowWorkshopID) + if (bAdmin) { uint64 iWorkshopID = V_StringToUint64(sMapSubstring, 0, NULL, NULL, PARSING_FLAG_SKIP_WARNING); @@ -678,6 +666,17 @@ uint64 CMapVoteSystem::HandlePlayerMapLookup(CCSPlayerController* pController, c std::vector foundIndexes = GetMapIndexesFromSubstring(sMapSubstring); + // Don't list disabled maps in non-admin commands + if (!bAdmin) + { + for (int iIndex : foundIndexes) + { + // Only erase if vector has multiple elements, so we can still give "map disabled" output in single-match scenarios + if (!GetMapEnabledStatus(iIndex) && foundIndexes.size() > 1) + foundIndexes.erase(std::remove(foundIndexes.begin(), foundIndexes.end(), iIndex), foundIndexes.end()); + } + } + if (foundIndexes.size() > 0) { if (foundIndexes.size() > 1) @@ -873,6 +872,9 @@ void CMapVoteSystem::ForceNextMap(CCSPlayerController* pController, const char* uint64 iFoundMap = HandlePlayerMapLookup(pController, sMapSubstring, true); + if (iFoundMap == -1) + return; + if (GetForcedNextMap() == iFoundMap) { ClientPrint(pController, HUD_PRINTTALK, CHAT_PREFIX "\x06%s\x01 is already the next map!", GetForcedNextMapName().c_str()); @@ -997,6 +999,7 @@ bool CMapVoteSystem::LoadMapList() iWorkshopId = jsonEntry["workshop_id"].get(); bool bIsEnabled = jsonEntry.value("enabled", true); + std::string strDisplayName = jsonEntry.value("display_name", ""); int iMinPlayers = jsonEntry.value("min_players", 0); int iMaxPlayers = jsonEntry.value("max_players", 64); float fCooldown = jsonEntry.value("cooldown", 0.0f); @@ -1010,7 +1013,7 @@ bool CMapVoteSystem::LoadMapList() QueueMapDownload(iWorkshopId); // We just append the maps to the map list - m_vecMapList.push_back(std::make_shared(sEntry, iWorkshopId, bIsEnabled, iMinPlayers, iMaxPlayers, fCooldown, vecGroups)); + m_vecMapList.push_back(std::make_shared(sEntry, strDisplayName, iWorkshopId, bIsEnabled, iMinPlayers, iMaxPlayers, fCooldown, vecGroups)); } } } @@ -1066,7 +1069,7 @@ CUtlStringList CMapVoteSystem::CreateWorkshopMapGroup() CUtlStringList mapList; for (int i = 0; i < GetMapListSize(); i++) - mapList.CopyAndAddToTail(GetMapName(i)); + mapList.CopyAndAddToTail(GetMapDisplayName(i)); return mapList; } @@ -1165,7 +1168,8 @@ void CMapVoteSystem::OnLevelShutdown() } } - WriteMapCooldownsToFile(); + if (IsMapListLoaded()) + WriteMapCooldownsToFile(); } std::string CMapVoteSystem::ConvertFloatToString(float fValue, int precision) @@ -1324,6 +1328,18 @@ float CCooldown::GetCurrentCooldown() return fRemainingTime; } +void CMapVoteSystem::ReloadCurrentMap() +{ + char sChangeMapCmd[128] = ""; + + if (GetCurrentWorkshopMap() != 0) + V_snprintf(sChangeMapCmd, sizeof(sChangeMapCmd), "host_workshop_map %llu", GetCurrentWorkshopMap()); + else + V_snprintf(sChangeMapCmd, sizeof(sChangeMapCmd), "map %s", GetCurrentMapName()); + + g_pEngineServer2->ServerCommand(sChangeMapCmd); +} + // TODO: remove this once servers have been given at least a few months to update cs2fixes bool CMapVoteSystem::ConvertMapListKVToJSON() { diff --git a/src/map_votes.h b/src/map_votes.h index a51d7cac6..a4a2f0e03 100644 --- a/src/map_votes.h +++ b/src/map_votes.h @@ -36,9 +36,10 @@ using ordered_json = nlohmann::ordered_json; class CMap { public: - CMap(std::string sName, uint64 iWorkshopId, bool bIsEnabled, int iMinPlayers, int iMaxPlayers, float fCustomCooldown, std::vector vecGroups) + CMap(std::string sName, std::string sDisplayName, uint64 iWorkshopId, bool bIsEnabled, int iMinPlayers, int iMaxPlayers, float fCustomCooldown, std::vector vecGroups) { m_strName = sName; + m_strDisplayName = sDisplayName; m_iWorkshopId = iWorkshopId; m_bIsEnabled = bIsEnabled; m_fCustomCooldown = fCustomCooldown; @@ -48,6 +49,7 @@ class CMap } const char* GetName() { return m_strName.c_str(); }; + const char* GetDisplayName() { return m_strDisplayName.empty() ? m_strName.c_str() : m_strDisplayName.c_str(); }; uint64 GetWorkshopId() const { return m_iWorkshopId; }; bool IsEnabled() { return m_bIsEnabled; }; float GetCustomCooldown() { return m_fCustomCooldown; }; @@ -58,6 +60,7 @@ class CMap private: std::string m_strName; + std::string m_strDisplayName; uint64 m_iWorkshopId; bool m_bIsEnabled; int m_iMinPlayers; @@ -129,7 +132,7 @@ class CMapVoteSystem void FinishVote(); bool RegisterPlayerVote(CPlayerSlot iPlayerSlot, int iVoteOption); std::vector GetMapIndexesFromSubstring(const char* sMapSubstring); - uint64 HandlePlayerMapLookup(CCSPlayerController* pController, const char* sMapSubstring, bool bAllowWorkshopID = false); + uint64 HandlePlayerMapLookup(CCSPlayerController* pController, const char* sMapSubstring, bool bAdmin = false); int GetMapIndexFromString(const char* pszMapString); std::shared_ptr GetGroupFromString(const char* pszName); std::shared_ptr GetMapCooldown(const char* pszMapName); @@ -145,6 +148,7 @@ class CMapVoteSystem void ForceNextMap(CCSPlayerController* pController, const char* sMapSubstring); int GetMapListSize() { return m_vecMapList.size(); }; const char* GetMapName(int iMapIndex) { return m_vecMapList[iMapIndex]->GetName(); }; + const char* GetMapDisplayName(int iMapIndex) { return m_vecMapList[iMapIndex]->GetDisplayName(); }; uint64 GetMapWorkshopId(int iMapIndex) { return m_vecMapList[iMapIndex]->GetWorkshopId(); }; void ClearPlayerInfo(int iSlot); bool IsVoteOngoing() { return m_bIsVoteOngoing; } @@ -175,6 +179,7 @@ class CMapVoteSystem std::string StringToLower(std::string sValue); void SetDisabledCooldowns(bool bValue) { g_bDisableCooldowns = bValue; } // Can be used by custom fork features, e.g. an auto-restart void ProcessGroupCooldowns(); + void ReloadCurrentMap(); private: int WinningMapIndex(); diff --git a/src/playermanager.cpp b/src/playermanager.cpp index a947c4883..f1ed605b3 100644 --- a/src/playermanager.cpp +++ b/src/playermanager.cpp @@ -45,6 +45,7 @@ extern CGameEntitySystem* g_pEntitySystem; extern CGlobalVars* GetGlobals(); extern IGameEventSystem* g_gameEventSystem; extern CUtlVector* GetClientList(); +extern CSpawnGroupMgrGameSystem* g_pSpawnGroupMgr; CConVar g_cvarAdminImmunityTargetting("cs2f_admin_immunity", FCVAR_NONE, "Mode for which admin immunity system targetting allows: 0 - strictly lower, 1 - equal to or lower, 2 - ignore immunity levels", 0, true, 0, true, 2); CConVar g_cvarEnableMapSteamIds("cs2f_map_steamids_enable", FCVAR_NONE, "Whether to make Steam ID's available to maps", false); @@ -224,7 +225,7 @@ void ZEPlayer::SpawnFlashLight() CBaseViewModel* pViewModel = GetOrCreateCustomViewModel(pPawn); if (!pViewModel) return; - + pLight->SetParent(pViewModel); } @@ -252,7 +253,7 @@ void ZEPlayer::ToggleFlashLight() CConVar g_cvarFloodInterval("cs2f_flood_interval", FCVAR_NONE, "Amount of time allowed between chat messages acquiring flood tokens", 0.75f, true, 0.0f, false, 0.0f); CConVar g_cvarMaxFloodTokens("cs2f_max_flood_tokens", FCVAR_NONE, "Maximum number of flood tokens allowed before chat messages are blocked", 3, true, 0, false, 0); CConVar g_cvarFloodCooldown("cs2f_flood_cooldown", FCVAR_NONE, "Amount of time to block messages for when a player floods", 3.0f, true, 0.0f, false, 0.0f); -CConVar g_cvarBeaconParticle("cs2f_beacon_particle", FCVAR_NONE, ".vpcf file to be precached and used for beacon", "particles/cs2fixes/player_beacon.vpcf"); +CConVar g_cvarBeaconParticle("cs2f_beacon_particle", FCVAR_NONE, ".vpcf file to be precached and used for beacon", "particles/cs2fixes/admin_beacon.vpcf"); bool ZEPlayer::IsFlooding() { @@ -306,7 +307,7 @@ void ZEPlayer::StartBeacon(Color color, ZEPlayerHandle hGiver /* = 0*/) pKeyValues->SetString("effect_name", g_cvarBeaconParticle.Get().String()); pKeyValues->SetInt("tint_cp", 1); pKeyValues->SetVector("origin", vecAbsOrigin); - pKeyValues->SetBool("start_active", true); + pKeyValues->SetBool("start_active", false); particle->m_clrTint->SetRawColor(color.GetRawColor()); @@ -324,7 +325,7 @@ void ZEPlayer::StartBeacon(Color color, ZEPlayerHandle hGiver /* = 0*/) if (pGiver && pGiver->IsLeader()) bLeaderBeacon = true; - new CTimer(1.0f, false, false, [hPlayer, hParticle, hGiver, iTeamNum, bLeaderBeacon]() { + new CTimer(0.0f, false, false, [hPlayer, hParticle, hGiver, iTeamNum, bLeaderBeacon]() { CParticleSystem* pParticle = hParticle.Get(); if (!hPlayer.IsValid() || !pParticle) @@ -338,6 +339,16 @@ void ZEPlayer::StartBeacon(Color color, ZEPlayerHandle hGiver /* = 0*/) return -1.0f; } + pParticle->AcceptInput("Start"); + + // delayed DestroyImmediately input so particle effect can be replayed (and default particle doesn't bug out) + new CTimer(0.5f, false, false, [hParticle]() { + CParticleSystem* particle = hParticle.Get(); + if (particle) + particle->AcceptInput("DestroyImmediately"); + return -1.0f; + }); + if (!bLeaderBeacon) return 1.0f; @@ -808,11 +819,11 @@ void CPlayerManager::OnClientDisconnect(CPlayerSlot slot) ResetPlayerFlags(slot.Get()); g_pMapVoteSystem->ClearPlayerInfo(slot.Get()); - g_pMapVoteSystem->ClearInvalidNominations(); // One tick delay, to ensure player count decrements new CTimer(0.01f, false, true, []() { g_pVoteManager->CheckRTVStatus(); + g_pMapVoteSystem->ClearInvalidNominations(); return -1.0f; }); @@ -824,6 +835,17 @@ void CPlayerManager::OnClientPutInServer(CPlayerSlot slot) ZEPlayer* pPlayer = m_vecPlayers[slot.Get()]; pPlayer->SetInGame(true); + + if (!g_pSpawnGroupMgr) + return; + + CUtlVector vecActualSpawnGroups; + addresses::GetSpawnGroups(g_pSpawnGroupMgr, &vecActualSpawnGroups); + + CServerSideClient* pClient = GetClientBySlot(slot); + + if (pClient && pClient->m_vecLoadedSpawnGroups.Count() != vecActualSpawnGroups.Count()) + pClient->m_vecLoadedSpawnGroups = vecActualSpawnGroups; } void CPlayerManager::OnLateLoad() diff --git a/src/serversideclient.h b/src/serversideclient.h index 89872659c..11eb6d6ad 100644 --- a/src/serversideclient.h +++ b/src/serversideclient.h @@ -11,6 +11,10 @@ #include #include // #include // @Wend4r: use instead. +#include "circularbuffer.h" +#include "networksystem/inetworksystem.h" +#include "threadtools.h" +#include "tier1/netadr.h" #include #include #include @@ -25,6 +29,7 @@ class CHLTVServer; class INetMessage; class CNetworkGameServerBase; class CNetworkGameServer; +class CUtlSlot; struct HltvReplayStats_t { @@ -66,11 +71,14 @@ class CNetworkStatTrace int m_nCurBit; }; // sizeof 40 -// class CServerSideClientBase: CUtlSlot, INetworkChannelNotify, INetworkMessageProcessingPreFilter; -class CServerSideClientBase +abstract_class INetworkChannelNotify { - virtual void UnkDestructor() = 0; +public: + virtual void OnShutdownChannel(INetChannel * pChannel) = 0; +}; +class CServerSideClientBase : public INetworkChannelNotify, public INetworkMessageProcessingPreFilter +{ public: virtual ~CServerSideClientBase() = 0; @@ -102,7 +110,7 @@ class CServerSideClientBase virtual void Clear() = 0; - virtual void ExecuteStringCommand(const CNETMsg_StringCmd& msg) = 0; + virtual bool ExecuteStringCommand(const CNETMsg_StringCmd_t& msg) = 0; // "false" trigger an anti spam counter to kick a client. virtual void SendNetMessage(const CNetMessage* pData, NetChannelBufType_t bufType) = 0; #ifdef LINUX @@ -111,48 +119,22 @@ class CServerSideClientBase #endif public: - virtual void ClientPrintf(const char*, ...) = 0; + virtual void ClientPrintf(PRINTF_FORMAT_STRING const char*, ...) = 0; - bool IsConnected() const - { - return m_nSignonState >= SIGNONSTATE_CONNECTED; - } - bool IsSpawned() const - { - return m_nSignonState >= SIGNONSTATE_NEW; - } - bool IsActive() const - { - return m_nSignonState == SIGNONSTATE_FULL; - } - virtual bool IsFakeClient() const - { - return m_bFakePlayer; - } + bool IsConnected() const { return m_nSignonState >= SIGNONSTATE_CONNECTED; } + bool IsInGame() const { return m_nSignonState == SIGNONSTATE_FULL; } + bool IsSpawned() const { return m_nSignonState >= SIGNONSTATE_NEW; } + bool IsActive() const { return m_nSignonState == SIGNONSTATE_FULL; } + virtual bool IsFakeClient() const { return m_bFakePlayer; } virtual bool IsHLTV() = 0; // Is an actual human player or splitscreen player (not a bot and not a HLTV slot) - virtual bool IsHumanPlayer() const - { - return false; - } - virtual bool IsHearingClient(CPlayerSlot nSlot) const - { - return false; - } - virtual bool IsLowViolenceClient() const - { - return m_bLowViolence; - } + virtual bool IsHumanPlayer() const { return false; } + virtual bool IsHearingClient(CPlayerSlot nSlot) const { return false; } + virtual bool IsLowViolenceClient() const { return m_bLowViolence; } - virtual bool IsSplitScreenUser() const - { - return m_bSplitScreenUser; - } - int GetClientPlatform() const - { - return m_ClientPlatform; - } // CrossPlayPlatform_t + virtual bool IsSplitScreenUser() const { return m_bSplitScreenUser; } + int GetClientPlatform() const { return m_ClientPlatform; } // CrossPlayPlatform_t public: // Message Handlers virtual bool ProcessTick(const CNETMsg_Tick_t& msg) = 0; @@ -207,16 +189,15 @@ class CServerSideClientBase virtual bool UpdateAcknowledgedFramecount(int tick) = 0; void ForceFullUpdate() { - UpdateAcknowledgedFramecount(-1); + // This seems to be wrong and crashes linux, plus I can't be bothered to check the vtable + // UpdateAcknowledgedFramecount(-1); + m_nDeltaTick = -1; } virtual bool ShouldSendMessages() = 0; virtual void UpdateSendState() = 0; - virtual const CMsgPlayerInfo& GetPlayerInfo() const - { - return m_playerInfo; - } + virtual const CMsgPlayerInfo& GetPlayerInfo() const { return m_playerInfo; } virtual void UpdateUserSettings() = 0; virtual void ResetUserSettings() = 0; @@ -232,26 +213,14 @@ class CServerSideClientBase virtual void SetName(const char* name) = 0; virtual void SetUserCVar(const char* cvar, const char* value) = 0; - int GetSignonState() const - { - return m_nSignonState; - } + SignonState_t GetSignonState() const { return m_nSignonState; } virtual void FreeBaselines() = 0; - bool IsFullyAuthenticated(void) - { - return m_bFullyAuthenticated; - } - void SetFullyAuthenticated(void) - { - m_bFullyAuthenticated = true; - } + bool IsFullyAuthenticated(void) { return m_bFullyAuthenticated; } + void SetFullyAuthenticated(void) { m_bFullyAuthenticated = true; } - virtual CServerSideClientBase* GetSplitScreenOwner() - { - return m_pAttachedTo; - } + virtual CServerSideClientBase* GetSplitScreenOwner() { return m_pAttachedTo; } virtual int GetNumPlayers() = 0; @@ -279,14 +248,16 @@ class CServerSideClientBase virtual bool ProcessSignonStateMsg(int state) = 0; virtual void PerformDisconnection(ENetworkDisconnectionReason reason) = 0; +public: // INetworkMessageProcessingPreFilter + virtual bool FilterMessage(const CNetMessage* pData, INetChannel* pChannel) = 0; // "Client %d(%s) tried to send a RebroadcastSourceId msg.\n" + public: - [[maybe_unused]] void* m_pVT1; // INetworkMessageProcessingPreFilter CUtlString m_unk16; // 16 [[maybe_unused]] char pad24[0x16]; // 24 #ifdef __linux__ [[maybe_unused]] char pad46[0x10]; // 46 #endif - void (*RebroadcastSource)(int msgID); // 64 + void (*RebroadcastSource)(int msgID); CUtlString m_UserIDString; // 72 CUtlString m_Name; // 80 CPlayerSlot m_nClientSlot; // 88 @@ -295,7 +266,7 @@ class CServerSideClientBase INetChannel* m_NetChannel; // 104 uint8 m_nUnkVariable; // 112 bool m_bMarkedToKick; // 113 - int32 m_nSignonState; // 116 + SignonState_t m_nSignonState; // 116 bool m_bSplitScreenUser; // 120 bool m_bSplitAllowFastDisconnect; // 121 int m_nSplitScreenPlayerSlot; // 124 @@ -306,48 +277,79 @@ class CServerSideClientBase bool m_bFakePlayer; // 176 bool m_bSendingSnapshot; // 177 [[maybe_unused]] char pad6[0x5]; - CPlayerUserId m_UserID; // 184 - bool m_bReceivedPacket; // 186 - CSteamID m_SteamID; // 187 - CSteamID m_UnkSteamID; // 195 - CSteamID m_UnkSteamID2; // 203 from auth ticket - CSteamID m_nFriendsID; // 211 - ns_address m_nAddr; // 220 - ns_address m_nAddr2; // 252 - KeyValues* m_ConVars; // 288 - bool m_bConVarsChanged; // 296 - bool m_bConVarsInited; // 297 - bool m_bIsHLTV; // 298 - bool m_bIsReplay; // 299 - [[maybe_unused]] char pad29[0xA]; - uint32 m_nSendtableCRC; // 312 - int m_ClientPlatform; // 316 - int m_nSignonTick; // 320 - int m_nDeltaTick; // 324 - int m_UnkVariable3; // 328 - int m_nStringTableAckTick; // 332 - int m_UnkVariable4; // 336 - CFrameSnapshot* m_pLastSnapshot; // 344 - CUtlVector m_vecLoadedSpawnGroups; // 352 - CMsgPlayerInfo m_playerInfo; // 376 - CFrameSnapshot* m_pBaseline; // 432 - int m_nBaselineUpdateTick; // 440 - CBitVec m_BaselinesSent; // 444 - int m_nBaselineUsed; // 2492 - int m_nLoadingProgress; // 2496 - int m_nForceWaitForTick; // 2500 - bool m_bLowViolence; // 2504 - bool m_bSomethingWithAddressType; // 2505 - bool m_bFullyAuthenticated; // 2506 - bool m_bUnkBool2507; // 2507 - float m_fNextMessageTime; // 2508 - float m_fSnapshotInterval; // 2512 - float m_fAuthenticatedTime; // 2516 - [[maybe_unused]] char pad168[0x124]; // 2520 - [[maybe_unused]] char pad1658[0x24]; // 2816 something in CServerSideClientBase::ExecuteStringCommand - CNetworkStatTrace m_Trace; // 2848 - int m_spamCommandsCount; // 2888 if the value is greater than 16, the player will be kicked with reason 39 - double m_lastExecutedCommand; // 2896 if command executed more than once per second, ++m_spamCommandCount + CPlayerUserId m_UserID = -1; // 184 + bool m_bReceivedPacket; // true, if client received a packet after the last send packet + CSteamID m_SteamID; // 187 + CSteamID m_UnkSteamID; // 195 + CSteamID m_UnkSteamID2; // 203 from auth ticket + CSteamID m_nFriendsID; // 211 + ns_address m_nAddr; // 220 + ns_address m_nAddr2; // 252 + KeyValues* m_ConVars; // 288 + bool m_bConVarsChanged; // 296 + bool m_bConVarsInited; // 297 + bool m_bIsHLTV; // 298 + bool m_bIsReplay; // 299 + +private: + [[maybe_unused]] char pad29[0x12]; + +public: + uint32 m_nSendtableCRC; // 312 + int m_ClientPlatform; // 316 + int m_nSignonTick; // 320 + int m_nDeltaTick; // 324 + int m_UnkVariable3; // 328 + int m_nStringTableAckTick; // 332 + int m_UnkVariable4; // 336 + CUtlVector m_vecLoadedSpawnGroups; // 352 + CFrameSnapshot* m_pLastSnapshot; // last send snapshot + CMsgPlayerInfo m_playerInfo; // 376 + CFrameSnapshot* m_pBaseline; // 432 + int m_nBaselineUpdateTick; // 440 + CBitVec m_BaselinesSent; // 444 + int m_nBaselineUsed; // 0/1 toggling flag, singaling client what baseline to use + int m_nLoadingProgress; // 0..100 progress, only valid during loading + + // This is used when we send out a nodelta packet to put the client in a state where we wait + // until we get an ack from them on this packet. + // This is for 3 reasons: + // 1. A client requesting a nodelta packet means they're screwed so no point in deluging them with data. + // Better to send the uncompressed data at a slow rate until we hear back from them (if at all). + // 2. Since the nodelta packet deletes all client entities, we can't ever delta from a packet previous to it. + // 3. It can eat up a lot of CPU on the server to keep building nodelta packets while waiting for + // a client to get back on its feet. + int m_nForceWaitForTick = -1; + + CCircularBuffer m_UnkBuffer = {1024}; // 2504 (24 bytes) + bool m_bLowViolence = false; // true if client is in low-violence mode (L4D server needs to know) + bool m_bSomethingWithAddressType = true; // 2529 + bool m_bFullyAuthenticated = false; // 2530 + bool m_bUnk1 = false; // 2531 + int m_nUnk; + + // The datagram is written to after every frame, but only cleared + // when it is sent out to the client. overflow is tolerated. + + // Time when we should send next world state update ( datagram ) + float m_fNextMessageTime = 0.0f; + float m_fAuthenticatedTime = -1.0f; + + // Default time to wait for next message + float m_fSnapshotInterval = 0.0f; + +private: + // CSVCMsg_PacketEntities_t m_packetmsg; // 2552 + [[maybe_unused]] char pad2552[0x138]; // 2552 + +public: + CNetworkStatTrace m_Trace; // 2864 + int m_spamCommandsCount = 0; // 2904 if the value is greater than 16, the player will be kicked with reason 39 + int m_unknown = 0; // 2908 + double m_lastExecutedCommand = 0.0; // 2912 if command executed more than once per second, ++m_spamCommandCount + +private: + [[maybe_unused]] char pad2920[0x20]; // 2920 }; class CServerSideClient : public CServerSideClientBase @@ -356,26 +358,34 @@ class CServerSideClient : public CServerSideClientBase virtual ~CServerSideClient() = 0; public: - CPlayerBitVec m_VoiceStreams; // 2904 - CPlayerBitVec m_VoiceProximity; // 2912 - CCheckTransmitInfo m_PackInfo; // 2920 - CClientFrameManager m_FrameManager; // 3520 - CClientFrame* m_pCurrentFrame; // 3808 - float m_flLastClientCommandQuotaStart; // 3816 - float m_flTimeClientBecameFullyConnected; // 3820 - bool m_bVoiceLoopback; // 3824 - int m_nHltvReplayDelay; // 3828 - CHLTVServer* m_pHltvReplayServer; // 3832 - int m_nHltvReplayStopAt; // 3840 - int m_nHltvReplayStartAt; // 3844 - int m_nHltvReplaySlowdownBeginAt; // 3848 - int m_nHltvReplaySlowdownEndAt; // 3852 - float m_flHltvReplaySlowdownRate; // 3856 - int m_nHltvLastSendTick; // 3860 - float m_flHltvLastReplayRequestTime; // 3864 - CUtlVector m_HltvQueuedMessages; // 3872 - HltvReplayStats_t m_HltvReplayStats; // 3896 - void* m_pLastJob; // 3952 + CPlayerBitVec m_VoiceStreams; // 2952 + CPlayerBitVec m_VoiceProximity; // 2960 + CCheckTransmitInfo m_PackInfo; // 2968 + CClientFrameManager m_FrameManager; // 3568 + +private: + [[maybe_unused]] char pad3856[8]; // 3856 + +public: + float m_flLastClientCommandQuotaStart = 0.0f; // 3864 + float m_flTimeClientBecameFullyConnected = -1.0f; // 3868 + bool m_bVoiceLoopback = false; // 3872 + bool m_bUnk10 = false; // 3873 + int m_nHltvReplayDelay = 0; // 3876 + CHLTVServer* m_pHltvReplayServer; // 3880 + int m_nHltvReplayStopAt; // 3888 + int m_nHltvReplayStartAt; // 3892 + int m_nHltvReplaySlowdownBeginAt; // 3896 + int m_nHltvReplaySlowdownEndAt; // 3900 + float m_flHltvReplaySlowdownRate; // 3904 + int m_nHltvLastSendTick; // 3908 + float m_flHltvLastReplayRequestTime; // 3912 + CUtlVector m_HltvQueuedMessages; // 3920 + HltvReplayStats_t m_HltvReplayStats; // 3944 + void* m_pLastJob; // 4000 + +private: + [[maybe_unused]] char pad3984[8]; // 4008 }; // not full class reversed @@ -385,20 +395,24 @@ class CHLTVClient : public CServerSideClientBase virtual ~CHLTVClient() = 0; public: - CNetworkGameServerBase* m_pHLTV; // 2904 - CUtlString m_szPassword; // 2912 - CUtlString m_szChatGroup; // 2920 - double m_fLastSendTime; // 2928 - double m_flLastChatTime; // 2936 - int m_nLastSendTick; // 2944 - [[maybe_unused]] char pad2948[0x4]; // 2948 - int m_nFullFrameTime; // 2952 - [[maybe_unused]] char pad2956[0x4]; // 2956 - [[maybe_unused]] char pad2960[0x4]; // 2960 - bool m_bNoChat; // 2964 - bool m_bUnkBool; // 2965 - bool m_bUnkBool2; // 2966 - bool m_bUnkBool3; // 2967 -}; // sizeof 3008 + CNetworkGameServerBase* m_pHLTV; // 2960 + CUtlString m_szPassword; // 2968 + CUtlString m_szChatGroup; // 2976 // "all" or "group%d" + double m_fLastSendTime = 0.0; // 2984 + double m_flLastChatTime = 0.0; // 2992 + int m_nLastSendTick = 0; // 2996 + int m_unknown2 = 0; // 3000 + int m_nFullFrameTime = 0; // 3008 + int m_unknown3 = 0; // 3012 + +public: + bool m_bNoChat = false; // 3016 + bool m_bUnkBool = false; // 3017 + bool m_bUnkBool2 = false; // 3018 + bool m_bUnkBool3 = false; // 3019 + +private: + [[maybe_unused]] char pad3976[0x24]; // 3020 +}; #endif // SERVERSIDECLIENT_H diff --git a/src/utils/hud_manager.cpp b/src/utils/hud_manager.cpp new file mode 100644 index 000000000..ab2aef24d --- /dev/null +++ b/src/utils/hud_manager.cpp @@ -0,0 +1,168 @@ +/** + * ============================================================================= + * CS2Fixes + * Copyright (C) 2023-2025 Source2ZE + * ============================================================================= + * + * 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 . + */ + +#include "hud_manager.h" +#include "../cs2fixes.h" +#include "../ctimer.h" +#include "../recipientfilters.h" +#include "engine/igameeventsystem.h" +#include "entity/cgamerules.h" +#include "gameevents.pb.h" +#include "networksystem/inetworkmessages.h" + +extern CCSGameRules* g_pGameRules; +extern IGameEventManager2* g_gameEventManager; +extern IGameEventSystem* g_gameEventSystem; +extern CGlobalVars* GetGlobals(); + +CConVar g_cvarFixHudFlashing("cs2f_fix_hud_flashing", FCVAR_NONE, "Whether to fix hud flashing using a workaround, this BREAKS warmup so pick one or the other", false); +static std::vector> g_vecHudMessages; + +bool ShouldDisplayForPlayer(ZEPlayerHandle hPlayer, EHudPriority ePriority) +{ + for (std::shared_ptr pHudMessage : g_vecHudMessages) + { + // Require higher priority to block, so if priority matches most recent message takes precedence + if (pHudMessage->HasRecipient(hPlayer) && pHudMessage->GetPriority() > ePriority) + return false; + } + + return true; +} + +void CreateHudMessage(std::shared_ptr pHudMessage) +{ + IGameEvent* pEvent = g_gameEventManager->CreateEvent("show_survival_respawn_status"); + + if (!pEvent) + return; + + pEvent->SetString("loc_token", pHudMessage->GetMessage()); + pEvent->SetInt("duration", pHudMessage->GetDuration()); + pEvent->SetInt("userid", -1); + + INetworkMessageInternal* pMsg = g_pNetworkMessages->FindNetworkMessageById(GE_Source1LegacyGameEvent); + + if (!pMsg) + { + g_gameEventManager->FreeEvent(pEvent); + return; + } + + CNetMessagePB* data = pMsg->AllocateMessage()->ToPB(); + CRecipientFilter filter; + + for (ZEPlayerHandle hPlayer : pHudMessage->GetRecipients()) + if (ShouldDisplayForPlayer(hPlayer, pHudMessage->GetPriority())) + filter.AddRecipient(hPlayer.Get()->GetPlayerSlot()); + + // Store the new hud message + g_vecHudMessages.push_back(pHudMessage); + + // Start a timer to remove this hud message after its duration passes + new CTimer(pHudMessage->GetDuration(), true, true, [pHudMessage]() { + g_vecHudMessages.erase(std::remove(g_vecHudMessages.begin(), g_vecHudMessages.end(), pHudMessage), g_vecHudMessages.end()); + + return -1.0f; + }); + + g_gameEventManager->SerializeEvent(pEvent, data); + g_gameEventSystem->PostEventAbstract(-1, false, &filter, pMsg, data, 0); + delete data; + g_gameEventManager->FreeEvent(pEvent); +} + +void SendHudMessage(ZEPlayer* pPlayer, int iDuration, EHudPriority ePriority, const char* pszMessage, ...) +{ + va_list args; + va_start(args, pszMessage); + + char buf[1024]; + V_vsnprintf(buf, sizeof(buf), pszMessage, args); + + va_end(args); + + std::shared_ptr pHudMessage = std::make_shared(buf, iDuration, ePriority); + + pHudMessage->AddRecipient(pPlayer->GetHandle()); + CreateHudMessage(pHudMessage); +} + +void SendHudMessageAll(int iDuration, EHudPriority ePriority, const char* pszMessage, ...) +{ + if (!GetGlobals()) + return; + + va_list args; + va_start(args, pszMessage); + + char buf[1024]; + V_vsnprintf(buf, sizeof(buf), pszMessage, args); + + va_end(args); + + std::shared_ptr pHudMessage = std::make_shared(buf, iDuration, ePriority); + + for (int i = 0; i < GetGlobals()->maxClients; i++) + { + ZEPlayer* pPlayer = g_playerManager->GetPlayer(i); + + if (pPlayer) + pHudMessage->AddRecipient(pPlayer->GetHandle()); + } + + CreateHudMessage(pHudMessage); +} + +void StartFlashingFixTimer() +{ + // Timer that fakes m_bGameRestart enabled, to fix flashing with show_survival_respawn_status + new CTimer(0.5f, false, true, []() { + if (!g_cvarFixHudFlashing.Get() || !g_pGameRules) + return 0.5f; + + // Faking m_bGameRestart as true close to a round ending causes UI to falsely show game is restarting + if (g_pGameRules->m_flRestartRoundTime.Get().GetTime() == 0.0f) + g_pGameRules->m_bGameRestart = true; + else + g_pGameRules->m_bGameRestart = false; + + return 0.5f; + }); +} + +std::string EscapeHTMLSpecialCharacters(std::string strMsg) +{ + // Always replace & first, as it is used in html escaped characters (so dont want to replace inside an escape) + for (size_t iPos = 0; (iPos = strMsg.find('&', iPos)) != std::string::npos; iPos += 5) + strMsg.replace(iPos, 1, "&"); + + std::unordered_map mapReplacements{ + {"<", "<"}, + {">", ">"}, + {"\"", """}, + {"\'", "'"} + }; + + for (const auto& [strBadChar, strEscapedChar] : mapReplacements) + for (size_t iPos = 0; (iPos = strMsg.find(strBadChar, iPos)) != std::string::npos; iPos += strEscapedChar.length()) + strMsg.replace(iPos, strBadChar.length(), strEscapedChar); + + return strMsg; +} \ No newline at end of file diff --git a/src/utils/hud_manager.h b/src/utils/hud_manager.h new file mode 100644 index 000000000..6e4c0ba4c --- /dev/null +++ b/src/utils/hud_manager.h @@ -0,0 +1,64 @@ +/** + * ============================================================================= + * CS2Fixes + * Copyright (C) 2023-2025 Source2ZE + * ============================================================================= + * + * 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 . + */ + +#pragma once + +#include "../playermanager.h" +#include +#include + +enum class EHudPriority +{ + InfectionCountdown = 2, + AdminHSay = 99 +}; + +class CHudMessage +{ +public: + CHudMessage(std::string sMessage, int iDuration, EHudPriority ePriority) + { + m_strMessage = sMessage; + m_iDuration = iDuration; + m_ePriority = ePriority; + } + + const char* GetMessage() { return m_strMessage.c_str(); }; + int GetDuration() { return m_iDuration; }; + EHudPriority GetPriority() { return m_ePriority; }; + std::vector GetRecipients() { return m_vecRecipients; }; + bool HasRecipient(ZEPlayerHandle hPlayer) { return std::find(m_vecRecipients.begin(), m_vecRecipients.end(), hPlayer) != m_vecRecipients.end(); }; + void AddRecipient(ZEPlayerHandle hPlayer) { m_vecRecipients.push_back(hPlayer); }; + +private: + std::string m_strMessage; + int m_iDuration; + EHudPriority m_ePriority; + std::vector m_vecRecipients; +}; + +// When multiple hud messages are active, whichever one has highest priority one will display +// Note this is a basic implementation (TODO: is this worth expanding?), so e.g. a previously sent lower priority message will not display once a higher priority one expires +void SendHudMessage(ZEPlayer* pPlayer, int iDuration, EHudPriority ePriority, const char* pszMessage, ...); +void SendHudMessageAll(int iDuration, EHudPriority ePriority, const char* pszMessage, ...); + +void StartFlashingFixTimer(); +std::string EscapeHTMLSpecialCharacters(std::string strMsg); + +extern CConVar g_cvarFixHudFlashing; \ No newline at end of file diff --git a/src/utils/version_gen_placeholder.h b/src/utils/version_gen_placeholder.h new file mode 100644 index 000000000..7be4c6d1e --- /dev/null +++ b/src/utils/version_gen_placeholder.h @@ -0,0 +1,28 @@ +/** + * ============================================================================= + * CS2Fixes + * Copyright (C) 2023-2025 Source2ZE + * ============================================================================= + * + * 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 . + */ + +#define PLUGIN_NAME "cs2fixes" +#define PLUGIN_ALIAS "cs2fixes" +#define PLUGIN_DISPLAY_NAME "CS2Fixes" +#define PLUGIN_DESCRIPTION "" +#define PLUGIN_AUTHOR "" +#define PLUGIN_URL "" +#define PLUGIN_LOGTAG "CS2Fixes" +#define PLUGIN_LICENSE "" +#define PLUGIN_FULL_VERSION "DEV-untracked"; \ No newline at end of file diff --git a/src/utils/weapon.cpp b/src/utils/weapon.cpp index b6a1f8273..0526e5807 100644 --- a/src/utils/weapon.cpp +++ b/src/utils/weapon.cpp @@ -58,9 +58,9 @@ static std::unordered_map s_WeaponMap = { {"weapon_revolver", {"weapon_revolver", 64, 0, GEAR_SLOT_PISTOL, 600, "R8 Revolver", {"r8revolver", "revolver", "r8"}}}, {"weapon_flashbang", {"weapon_flashbang", 43, 0, GEAR_SLOT_GRENADES} }, - {"weapon_hegrenade", {"weapon_hegrenade", 44, 0, GEAR_SLOT_GRENADES, 300, "HE Grenade", {"hegrenade", "he"}, 1} }, + {"weapon_hegrenade", {"weapon_hegrenade", 44, 0, GEAR_SLOT_GRENADES, 300, "HE Grenade", {"hegrenade", "he"}} }, {"weapon_smokegrenade", {"weapon_smokegrenade", 45, 0, GEAR_SLOT_GRENADES} }, - {"weapon_molotov", {"weapon_molotov", 46, 0, GEAR_SLOT_GRENADES, 400, "Molotov", {"molotov"}, 1} }, + {"weapon_molotov", {"weapon_molotov", 46, 0, GEAR_SLOT_GRENADES, 400, "Molotov", {"molotov"}} }, {"weapon_decoy", {"weapon_decoy", 47, 0, GEAR_SLOT_GRENADES} }, {"weapon_incgrenade", {"weapon_incgrenade", 48, 0, GEAR_SLOT_GRENADES} }, @@ -90,10 +90,10 @@ static std::unordered_map s_WeaponMap = { {"weapon_knife_skeleton", {"weapon_knife", 525, 0, GEAR_SLOT_KNIFE} }, {"weapon_knife_kukri", {"weapon_knife", 526, 0, GEAR_SLOT_KNIFE} }, - {"item_kevlar", {"item_kevlar", 0, 0, GEAR_SLOT_UTILITY, 650, "Kevlar Vest", {"kevlar"}} }, - {"item_assaultsuit", {"item_assaultsuit", 0, 0, GEAR_SLOT_UTILITY} }, - {"item_heavyassaultsuit", {"item_heavyassaultsuit", 0, 0, GEAR_SLOT_UTILITY} }, - {"item_defuser", {"item_defuser", 0, 0, GEAR_SLOT_UTILITY} }, + {"item_kevlar", {"item_kevlar", 50, 0, GEAR_SLOT_UTILITY, 650, "Kevlar Vest", {"kevlar"}} }, + {"item_assaultsuit", {"item_assaultsuit", 51, 0, GEAR_SLOT_UTILITY} }, + {"item_heavyassaultsuit", {"item_heavyassaultsuit", 52, 0, GEAR_SLOT_UTILITY} }, + {"item_defuser", {"item_defuser", 55, 0, GEAR_SLOT_UTILITY} }, {"ammo_50ae", {"ammo_50ae", 0, 0, GEAR_SLOT_UTILITY} }, }; @@ -126,6 +126,15 @@ const WeaponInfo_t* FindWeaponInfoByAlias(const char* pAlias) return nullptr; } +const WeaponInfo_t* FindWeaponInfoByItemDefIndex(int16_t iItemDefinitionIndex) +{ + for (const auto& info : s_WeaponMap | std::views::values) + if (iItemDefinitionIndex == info.m_iItemDefinitionIndex) + return &info; + + return nullptr; +} + std::vector>> GenerateWeaponCommands() { std::vector>> vec; diff --git a/src/utils/weapon.h b/src/utils/weapon.h index 53c54fea6..dc12704bd 100644 --- a/src/utils/weapon.h +++ b/src/utils/weapon.h @@ -42,5 +42,6 @@ struct WeaponInfo_t const WeaponInfo_t* FindWeaponInfoByClass(const char* pClass); const WeaponInfo_t* FindWeaponInfoByClassCaseInsensitive(const char* pClass); const WeaponInfo_t* FindWeaponInfoByAlias(const char* pAlias); +const WeaponInfo_t* FindWeaponInfoByItemDefIndex(int16_t iItemDefinitionIndex); std::vector>> GenerateWeaponCommands(); \ No newline at end of file diff --git a/src/zombiereborn.cpp b/src/zombiereborn.cpp index b3d8cfc98..f43615399 100644 --- a/src/zombiereborn.cpp +++ b/src/zombiereborn.cpp @@ -1,4 +1,4 @@ -/** +/** * ============================================================================= * CS2Fixes * Copyright (C) 2023-2025 Source2ZE @@ -28,6 +28,7 @@ #include "entity/cteam.h" #include "entity/services.h" #include "eventlistener.h" +#include "hud_manager.h" #include "leader.h" #include "networksystem/inetworkmessages.h" #include "playermanager.h" @@ -52,6 +53,7 @@ extern CCSGameRules* g_pGameRules; extern IGameEventManager2* g_gameEventManager; extern IGameEventSystem* g_gameEventSystem; extern double g_flUniversalTime; +extern CConVar g_cvarFreeArmor; void ZR_Infect(CCSPlayerController* pAttackerController, CCSPlayerController* pVictimController, bool bBroadcast); bool ZR_CheckTeamWinConditions(int iTeamNum); @@ -61,7 +63,6 @@ void SetupCTeams(); bool ZR_IsTeamAlive(int iTeamNum); EZRRoundState g_ZRRoundState = EZRRoundState::ROUND_START; -static int g_iInfectionCountDown = 0; static bool g_bRespawnEnabled = true; static CHandle g_hRespawnToggler; static CHandle g_hTeamCT; @@ -76,6 +77,7 @@ CConVar g_cvarMaxZteleDistance("zr_ztele_max_distance", FCVAR_NONE, "Maxi CConVar g_cvarZteleHuman("zr_ztele_allow_humans", FCVAR_NONE, "Whether to allow humans to use ztele", false); CConVar g_cvarKnockbackScale("zr_knockback_scale", FCVAR_NONE, "Global knockback scale", 5.0f); CConVar g_cvarInfectSpawnType("zr_infect_spawn_type", FCVAR_NONE, "Type of Mother Zombies Spawn [0 = MZ spawn where they stand, 1 = MZ get teleported back to spawn on being picked]", (int)EZRSpawnType::RESPAWN, true, 0, true, 1); +CConVar g_cvarInfectSpawnWarning("zr_infect_spawn_warning", FCVAR_NONE, "Whether to warn players of zombies spawning between humans", true); CConVar g_cvarInfectSpawnTimeMin("zr_infect_spawn_time_min", FCVAR_NONE, "Minimum time in which Mother Zombies should be picked, after round start", 15, true, 0, false, 0); CConVar g_cvarInfectSpawnTimeMax("zr_infect_spawn_time_max", FCVAR_NONE, "Maximum time in which Mother Zombies should be picked, after round start", 15, true, 1, false, 0); CConVar g_cvarInfectSpawnMZRatio("zr_infect_spawn_mz_ratio", FCVAR_NONE, "Ratio of all Players to Mother Zombies to be spawned at round start", 7, true, 1, true, 64); @@ -276,13 +278,13 @@ void ZRClass::Override(ordered_json jsonKeys, std::string szClassname) } ZRHumanClass::ZRHumanClass(ordered_json jsonKeys, std::string szClassname) : - ZRClass(jsonKeys, szClassname, CS_TEAM_CT) {}; + ZRClass(jsonKeys, szClassname, CS_TEAM_CT){}; ZRZombieClass::ZRZombieClass(ordered_json jsonKeys, std::string szClassname) : ZRClass(jsonKeys, szClassname, CS_TEAM_T), iHealthRegenCount(jsonKeys.value("health_regen_count", 0)), flHealthRegenInterval(jsonKeys.value("health_regen_interval", 0)), - flKnockback(jsonKeys.value("knockback", 1.0)) {}; + flKnockback(jsonKeys.value("knockback", 1.0)){}; void ZRZombieClass::Override(ordered_json jsonKeys, std::string szClassname) { @@ -510,15 +512,8 @@ void split(const std::string& s, char delim, Out result) void CZRPlayerClassManager::ApplyBaseClass(std::shared_ptr pClass, CCSPlayerPawn* pPawn) { - std::shared_ptr pModelEntry = pClass->GetRandomModelEntry(); - Color clrRender; - V_StringToColor(pModelEntry->szColor.c_str(), clrRender); - pPawn->m_iMaxHealth = pClass->iHealth; pPawn->m_iHealth = pClass->iHealth; - pPawn->SetModel(pModelEntry->szModelPath.c_str()); - pPawn->m_clrRender = clrRender; - pPawn->AcceptInput("Skin", pModelEntry->GetRandomSkin()); pPawn->m_flGravityScale = pClass->flGravity; // I don't know why, I don't want to know why, @@ -527,14 +522,9 @@ void CZRPlayerClassManager::ApplyBaseClass(std::shared_ptr pClass, CCSP // pPawn->m_flVelocityModifier = pClass->flSpeed; const auto pController = reinterpret_cast(pPawn->GetController()); if (const auto pPlayer = pController != nullptr ? pController->GetZEPlayer() : nullptr) - { pPlayer->SetMaxSpeed(pClass->flSpeed); - pPlayer->SetActiveZRClass(pClass); - pPlayer->SetActiveZRModel(pModelEntry); - } - // This has to be done a bit later - UTIL_AddEntityIOEvent(pPawn, "SetScale", nullptr, nullptr, pClass->flScale); + ApplyBaseClassVisuals(pClass, pPawn); } // only changes that should not (directly) affect gameplay @@ -808,11 +798,6 @@ void ZR_OnLevelInit() // Necessary to fix bots kicked/joining infinitely when forced to CT https://github.com/Source2ZE/ZombieReborn/issues/64 g_pEngineServer2->ServerCommand("bot_quota_mode fill"); g_pEngineServer2->ServerCommand("mp_autoteambalance 0"); - // These disable most of the buy menu for zombies - g_pEngineServer2->ServerCommand("mp_weapons_allow_pistols 3"); - g_pEngineServer2->ServerCommand("mp_weapons_allow_smgs 3"); - g_pEngineServer2->ServerCommand("mp_weapons_allow_heavy 3"); - g_pEngineServer2->ServerCommand("mp_weapons_allow_rifles 3"); return -1.0f; }); @@ -855,6 +840,11 @@ void ZRWeaponConfig::LoadWeaponConfig() std::shared_ptr ZRWeaponConfig::FindWeapon(const char* pszWeaponName) { + if (V_strlen(pszWeaponName) > 7 && !V_strncasecmp(pszWeaponName, "weapon_", 7)) + pszWeaponName = pszWeaponName + 7; + else if (V_strlen(pszWeaponName) > 5 && !V_strncasecmp(pszWeaponName, "item_", 5)) + pszWeaponName = pszWeaponName + 5; + uint16 index = m_WeaponMap.Find(hash_32_fnv1a_const(pszWeaponName)); if (m_WeaponMap.IsValidIndex(index)) return m_WeaponMap[index]; @@ -963,6 +953,13 @@ void ToggleRespawn(bool force = false, bool value = false) void ZR_OnRoundPrestart(IGameEvent* pEvent) { + // Gamerules may not be available earlier, so easiest to just enforce this here + if (g_pGameRules) + { + g_pGameRules->m_iMaxNumCTs = 64; + g_pGameRules->m_iMaxNumTerrorists = 64; + } + g_ZRRoundState = EZRRoundState::ROUND_START; ToggleRespawn(true, true); @@ -1013,6 +1010,8 @@ void SetupCTeams() void ZR_OnRoundStart(IGameEvent* pEvent) { ClientPrintAll(HUD_PRINTTALK, ZR_PREFIX "The game is \x05Humans vs. Zombies\x01, the goal for zombies is to infect all humans by knifing them."); + if (g_cvarInfectSpawnWarning.Get() && g_cvarInfectSpawnType.Get() == (int)EZRSpawnType::IN_PLACE) + ClientPrintAll(HUD_PRINTTALK, ZR_PREFIX "Classic spawn is enabled! Zombies will be \x07spawning between humans\x01!"); SetupRespawnToggler(); CZRRegenTimer::RemoveAllTimers(); @@ -1066,10 +1065,7 @@ void ZR_OnPlayerSpawn(CCSPlayerController* pController) void ZR_ApplyKnockback(CCSPlayerPawn* pHuman, CCSPlayerPawn* pVictim, int iDamage, const char* szWeapon, int hitgroup, float classknockback) { - if (V_strlen(szWeapon) <= 7) - return; - - std::shared_ptr pWeapon = g_pZRWeaponConfig->FindWeapon(szWeapon + 7); + std::shared_ptr pWeapon = g_pZRWeaponConfig->FindWeapon(szWeapon); std::shared_ptr pHitgroup = g_pZRHitgroupConfig->FindHitgroupIndex(hitgroup); // player shouldn't be able to pick up that weapon in the first place, but just in case if (!pWeapon) @@ -1146,8 +1142,11 @@ void ZR_StripAndGiveKnife(CCSPlayerPawn* pPawn) pItemServices->GiveNamedItem("weapon_knife"); ConVarRefAbstract mp_free_armor("mp_free_armor"); - if (mp_free_armor.GetBool()) + + if (mp_free_armor.GetInt() == 1 || g_cvarFreeArmor.GetInt() == 1) pItemServices->GiveNamedItem("item_kevlar"); + else if (mp_free_armor.GetInt() == 2 || g_cvarFreeArmor.GetInt() == 2) + pItemServices->GiveNamedItem("item_assaultsuit"); } CUtlVector>* weapons = pWeaponServices->m_hMyWeapons(); @@ -1197,11 +1196,11 @@ float ZR_MoanTimer(ZEPlayerHandle hPlayer) // This guy is dead but still infected, and corpses are quiet if (!pPawn->IsAlive()) - return g_cvarMoanInterval.Get() + (rand() % 5); + return g_cvarMoanInterval.Get(); pPawn->EmitSound("zr.amb.zombie_voice_idle"); - return g_cvarMoanInterval.Get() + (rand() % 5); + return g_cvarMoanInterval.Get(); } void ZR_InfectShake(CCSPlayerController* pController) @@ -1281,7 +1280,7 @@ void ZR_Infect(CCSPlayerController* pAttackerController, CCSPlayerController* pV pZEPlayer->SetInfectState(true); ZEPlayerHandle hPlayer = pZEPlayer->GetHandle(); - new CTimer(g_cvarMoanInterval.Get() + (rand() % 5), false, false, [hPlayer]() { return ZR_MoanTimer(hPlayer); }); + new CTimer(rand() % (int)g_cvarMoanInterval.Get(), false, false, [hPlayer]() { return ZR_MoanTimer(hPlayer); }); } } @@ -1322,7 +1321,7 @@ void ZR_InfectMotherZombie(CCSPlayerController* pVictimController, std::vectorSetInfectState(true); ZEPlayerHandle hPlayer = pZEPlayer->GetHandle(); - new CTimer(g_cvarMoanInterval.Get() + (rand() % 5), false, false, [hPlayer]() { return ZR_MoanTimer(hPlayer); }); + new CTimer(rand() % (int)g_cvarMoanInterval.Get(), false, false, [hPlayer]() { return ZR_MoanTimer(hPlayer); }); } // make players who've been picked as MZ recently less likely to be picked again @@ -1436,7 +1435,7 @@ void ZR_InitialInfection() if (g_cvarRespawnDelay.Get() < 0.0f) g_bRespawnEnabled = false; - ClientPrintAll(HUD_PRINTCENTER, "First infection has started!"); + SendHudMessageAll(4, EHudPriority::InfectionCountdown, "First infection has started!"); ClientPrintAll(HUD_PRINTTALK, ZR_PREFIX "First infection has started! Good luck, survivors!"); g_ZRRoundState = EZRRoundState::POST_INFECTION; } @@ -1446,10 +1445,15 @@ void ZR_StartInitialCountdown() if (g_cvarInfectSpawnTimeMin.Get() > g_cvarInfectSpawnTimeMax.Get()) g_cvarInfectSpawnTimeMin.Set(g_cvarInfectSpawnTimeMax.Get()); - g_iInfectionCountDown = g_cvarInfectSpawnTimeMin.Get() + (rand() % (g_cvarInfectSpawnTimeMax.Get() - g_cvarInfectSpawnTimeMin.Get() + 1)); - new CTimer(0.0f, false, false, []() { + int iRand = rand(); + auto iSecondsElapsed = std::make_shared(0); + new CTimer(0.0f, false, false, [iRand, iSecondsElapsed]() { if (g_ZRRoundState != EZRRoundState::ROUND_START) return -1.0f; + + int g_iInfectionCountDown = g_cvarInfectSpawnTimeMin.Get() + (iRand % (g_cvarInfectSpawnTimeMax.Get() - g_cvarInfectSpawnTimeMin.Get() + 1)); + g_iInfectionCountDown -= *iSecondsElapsed; + if (g_iInfectionCountDown <= 0) { ZR_InitialInfection(); @@ -1458,14 +1462,19 @@ void ZR_StartInitialCountdown() if (g_iInfectionCountDown <= 60) { - char message[256]; - V_snprintf(message, sizeof(message), "First infection in \7%i %s\1!", g_iInfectionCountDown, g_iInfectionCountDown == 1 ? "second" : "seconds"); + char classicSpawnMsg[256]; + + if (g_cvarInfectSpawnWarning.Get() && g_cvarInfectSpawnType.Get() == (int)EZRSpawnType::IN_PLACE) + V_snprintf(classicSpawnMsg, sizeof(classicSpawnMsg), "WARNING: Zombies will spawn between humans!
\u00A0
"); + else + V_snprintf(classicSpawnMsg, sizeof(classicSpawnMsg), ""); + + SendHudMessageAll(2, EHudPriority::InfectionCountdown, "%sFirst infection in %i %s!", classicSpawnMsg, g_iInfectionCountDown, g_iInfectionCountDown == 1 ? "second" : "seconds"); - ClientPrintAll(HUD_PRINTCENTER, message); if (g_iInfectionCountDown % 5 == 0) - ClientPrintAll(HUD_PRINTTALK, "%s%s", ZR_PREFIX, message); + ClientPrintAll(HUD_PRINTTALK, "%sFirst infection in \7%i %s\1!", ZR_PREFIX, g_iInfectionCountDown, g_iInfectionCountDown == 1 ? "second" : "seconds"); } - g_iInfectionCountDown--; + (*iSecondsElapsed)++; return 1.0f; }); @@ -1516,19 +1525,26 @@ bool ZR_Hook_OnTakeDamage_Alive(CTakeDamageInfo* pInfo, CCSPlayerPawn* pVictimPa return false; } -// return false to prevent player from picking it up -bool ZR_Detour_CCSPlayer_WeaponServices_CanUse(CCSPlayer_WeaponServices* pWeaponServices, CBasePlayerWeapon* pPlayerWeapon) +// can prevent purchasing and picking it up +AcquireResult ZR_Detour_CCSPlayer_ItemServices_CanAcquire(CCSPlayer_ItemServices* pItemServices, CEconItemView* pEconItem) { - CCSPlayerPawn* pPawn = pWeaponServices->__m_pChainEntity(); + CCSPlayerPawn* pPawn = pItemServices->__m_pChainEntity(); + if (!pPawn) - return false; - const char* pszWeaponClassname = pPlayerWeapon->GetWeaponClassname(); - if (pPawn->m_iTeamNum() == CS_TEAM_T && !CCSPlayer_ItemServices::IsAwsProcessing() && V_strncmp(pszWeaponClassname, "weapon_knife", 12) && V_strncmp(pszWeaponClassname, "weapon_c4", 9)) - return false; - if (pPawn->m_iTeamNum() == CS_TEAM_CT && V_strlen(pszWeaponClassname) > 7 && !g_pZRWeaponConfig->FindWeapon(pszWeaponClassname + 7)) - return false; - // doesn't guarantee the player will pick the weapon up, it just allows the original function to run - return true; + return AcquireResult::Allowed; + + const WeaponInfo_t* pWeaponInfo = FindWeaponInfoByItemDefIndex(pEconItem->m_iItemDefinitionIndex); + + if (!pWeaponInfo) + return AcquireResult::Allowed; + + if (pPawn->m_iTeamNum() == CS_TEAM_T && !CCSPlayer_ItemServices::IsAwsProcessing() && V_strncmp(pWeaponInfo->m_pClass, "weapon_knife", 12) && V_strncmp(pWeaponInfo->m_pClass, "weapon_c4", 9)) + return AcquireResult::NotAllowedByTeam; + if (pPawn->m_iTeamNum() == CS_TEAM_CT && !g_pZRWeaponConfig->FindWeapon(pWeaponInfo->m_pClass)) + return AcquireResult::NotAllowedByProhibition; + + // doesn't guarantee the player will acquire the weapon, it just allows the original function to run + return AcquireResult::Allowed; } void ZR_Detour_CEntityIdentity_AcceptInput(CEntityIdentity* pThis, CUtlSymbolLarge* pInputName, CEntityInstance* pActivator, CEntityInstance* pCaller, variant_t* value, int nOutputID) @@ -1607,8 +1623,8 @@ void ZR_Hook_ClientCommand_JoinTeam(CPlayerSlot slot, const CCommand& args) void ZR_OnPlayerTakeDamage(CCSPlayerPawn* pVictimPawn, const CTakeDamageInfo* pInfo, const int32 damage) { - // bullet only - if ((pInfo->m_bitsDamageType & DMG_BULLET) == 0 || !pInfo->m_pTrace || !pInfo->m_pTrace->m_pHitbox) + // bullet & knife only + if ((!(pInfo->m_bitsDamageType & DMG_BULLET) && !(pInfo->m_bitsDamageType & DMG_SLASH)) || !pInfo->m_pTrace || !pInfo->m_pTrace->m_pHitbox) return; const auto pVictimController = reinterpret_cast(pVictimPawn->GetController()); diff --git a/src/zombiereborn.h b/src/zombiereborn.h index a4e3447c5..4de0d6e40 100644 --- a/src/zombiereborn.h +++ b/src/zombiereborn.h @@ -136,7 +136,7 @@ struct ZRClass struct ZRHumanClass : ZRClass { ZRHumanClass(std::shared_ptr pClass) : - ZRClass(pClass, CS_TEAM_CT) {}; + ZRClass(pClass, CS_TEAM_CT){}; ZRHumanClass(ordered_json jsonKeys, std::string szClassname); }; @@ -149,7 +149,7 @@ struct ZRZombieClass : ZRClass ZRClass(pClass, CS_TEAM_T), iHealthRegenCount(pClass->iHealthRegenCount), flHealthRegenInterval(pClass->flHealthRegenInterval), - flKnockback(pClass->flKnockback) {}; + flKnockback(pClass->flKnockback){}; ZRZombieClass(ordered_json jsonKeys, std::string szClassname); void PrintInfo() { @@ -226,7 +226,7 @@ class CZRRegenTimer : public CTimerBase { public: CZRRegenTimer(float flRegenInterval, int iRegenAmount, CHandle hPawnHandle) : - CTimerBase(flRegenInterval, false, false), m_iRegenAmount(iRegenAmount), m_hPawnHandle(hPawnHandle) {}; + CTimerBase(flRegenInterval, false, false), m_iRegenAmount(iRegenAmount), m_hPawnHandle(hPawnHandle){}; bool Execute(); static void StartRegen(float flRegenInterval, int iRegenAmount, CCSPlayerController* pController); @@ -296,7 +296,7 @@ void ZR_OnPlayerDeath(IGameEvent* pEvent); void ZR_OnRoundFreezeEnd(IGameEvent* pEvent); void ZR_OnRoundTimeWarning(IGameEvent* pEvent); bool ZR_Hook_OnTakeDamage_Alive(CTakeDamageInfo* pInfo, CCSPlayerPawn* pVictimPawn); -bool ZR_Detour_CCSPlayer_WeaponServices_CanUse(CCSPlayer_WeaponServices* pWeaponServices, CBasePlayerWeapon* pPlayerWeapon); +AcquireResult ZR_Detour_CCSPlayer_ItemServices_CanAcquire(CCSPlayer_ItemServices* pItemServices, CEconItemView* pEconItem); void ZR_Detour_CEntityIdentity_AcceptInput(CEntityIdentity* pThis, CUtlSymbolLarge* pInputName, CEntityInstance* pActivator, CEntityInstance* pCaller, variant_t* value, int nOutputID); void ZR_Hook_ClientPutInServer(CPlayerSlot slot, char const* pszName, int type, uint64 xuid); void ZR_Hook_ClientCommand_JoinTeam(CPlayerSlot slot, const CCommand& args);