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);