Was a bit busy lately so I couldn't make this sooner, hope you still need this.
First things first, the fire rate part of the plugin works great in both CSS and CSGO, but the no spread / no recoil part only works flawlessly in CSS. In CSS, no matter what the player is doing while shooting (jumping, walking, running, flying, swimming, noclipping, and so on), their bullets will go to where their crosshair is. In CSGO however, it will only work if the player is standing still or crouch walking. Maybe someone else knows why this is happening in CSGO?
Anyway, here's the plugin:
Syntax: Select all
# ../wcs_fire/wcs_fire.py
# Source.Python
from commands.typed import TypedServerCommand
from core import GAME_NAME, echo_console
from cvars import ConVar
from engines.server import global_vars
from entities.helpers import index_from_pointer
from events import Event
from events.hooks import PreEvent
from listeners import OnPlayerRunCommand
from mathlib import NULL_VECTOR
from memory import make_object
from players import UserCmd
from players.dictionary import PlayerDictionary
from players.entity import Player
from weapons.dictionary import WeaponDictionary
recoil_cvars_default = {}
recoil_cvars_modified = {
'weapon_accuracy_nospread': 1,
'weapon_air_spread_scale': 0,
'weapon_recoil_cooldown': 0,
'weapon_recoil_decay1_exp': 99999,
'weapon_recoil_decay2_exp': 99999,
'weapon_recoil_decay2_lin': 99999,
'weapon_recoil_decay_coefficient': 0,
'weapon_recoil_scale': 0,
'weapon_recoil_scale_motion_controller': 0,
'weapon_recoil_suppression_factor': 0,
'weapon_recoil_suppression_shots': 500,
'weapon_recoil_variance': 0,
'weapon_recoil_vel_decay': 0,
'weapon_recoil_view_punch_extra': 0
}
if GAME_NAME == 'csgo':
# Go through all the convars in the 'recoil_cvars_modified' dictionary, get
# their default values, and save them in the 'recoil_cvars_default'
# dictionary.
for cvar in recoil_cvars_modified:
recoil_cvars_default[cvar] = ConVar(cvar).default
# CSGO properties for controlling recoil and spread.
weapon_fire_post_properties = (
# This is used to remove the spray pattern / spread.
'localdata.m_Local.m_aimPunchAngleVel',
# And these two are used for reducing the aimpunch / viewpunch after
# firing a weapon.
'localdata.m_Local.m_aimPunchAngle',
'localdata.m_Local.m_viewPunchAngle'
)
else:
# Since CSS doesn't have the same convars as CSGO, clear the dictionary so
# that the players don't get their console spammed with these errors:
# ConVarRef weapon_recoil_scale doesn't point to an existing ConVar
# SetConVar: No such cvar ( weapon_recoil_scale set to 0), skipping
recoil_cvars_modified.clear()
# CSS (and possibly other Source games) properties for controlling recoil.
weapon_fire_post_properties = (
# Both of these are used for reducing the aimpunch / viewpunch after
# firing a weapon.
'localdata.m_Local.m_vecPunchAngle',
'localdata.m_Local.m_vecPunchAngleVel'
)
class PlayerModified(Player):
def __init__(self, index):
super().__init__(index)
self._fire_rate = 1.0
self.fire_rate_changed = False
self.fired_weapon = None
self.no_recoil = False
def disable_recoil(self):
self.no_recoil = True
# Modify the player's convars to reduce their recoil.
for cvar in recoil_cvars_modified:
self.send_convar_value(cvar, recoil_cvars_modified[cvar])
def enable_recoil(self):
self.no_recoil = False
# Restore the player's recoil convars back to normal.
for cvar in recoil_cvars_default:
self.send_convar_value(cvar, recoil_cvars_default[cvar])
def get_fire_rate(self):
return self._fire_rate
def set_fire_rate(self, new_fire_rate):
# If the given value is 0, set the 'fire_rate' back to default.
if new_fire_rate == 0.0:
new_fire_rate = 1.0
# Are we setting the 'fire_rate' back to default?
if new_fire_rate == 1.0:
self.fire_rate_changed = False
else:
self.fire_rate_changed = True
# Get the current time.
cur_time = global_vars.current_time
# In order to avoid the player not being able to fire their weapon
# after their 'fire_rate' has been changed, go through all of their
# weapons and reset the 'm_flNextPrimaryAttack' property.
for index in self.weapon_indexes():
weapon_instances[index].set_network_property_float(
'LocalActiveWeaponData.m_flNextPrimaryAttack', cur_time
)
# Change the 'fire_rate'.
self._fire_rate = new_fire_rate
fire_rate = property(get_fire_rate, set_fire_rate)
player_instances = PlayerDictionary(PlayerModified)
weapon_instances = WeaponDictionary()
@OnPlayerRunCommand
def on_player_run_command(player, user_cmd):
# Is the player dead?
if player.get_datamap_property_bool('pl.deadflag'):
return
# Get the PlayerModified instance.
player = player_instances[player.index]
# Does the player have their recoil enabled?
if not player.no_recoil:
return
# Remove the randomness of the spread. This will also make sure there's no
# spread while the player is moving and shooting.
# NOTE: This doesn't seem to work in CSGO.
user_cmd.random_seed = 0
@PreEvent('weapon_fire')
def weapon_fire_pre(event):
player = player_instances.from_userid(event['userid'])
# Don't go further if both the fire rate and the recoil are unchanged.
if not player.fire_rate_changed and not player.no_recoil:
return
# If either the fire rate or the recoil have been changed, add a new key
# called '_modified' with the value True to the 'weapon_fire' event.
event.set_bool('_modified', True)
# Get the weapon the player is firing.
weapon = weapon_instances.from_inthandle(player.active_weapon_handle)
# Store the weapon instance for later use (in weapon_fire_post()).
player.fired_weapon = weapon
if not player.no_recoil:
return
# Remove the recoil by making the game think this is still the first shot.
player.set_network_property_uchar('cslocaldata.m_iShotsFired', 0)
# Remove the accuracy penalty for jumping / falling.
# NOTE: Again, this doesn't seem to work in CSGO.
weapon.set_network_property_float('m_fAccuracyPenalty', 0.0)
@Event('weapon_fire')
def weapon_fire(event):
# Is this not a '_modified' event?
if not event.get_bool('_modified'):
return
player = player_instances.from_userid(event['userid'])
# The next couple of changes need to happen after the 'weapon_fire' event,
# so we delay them by a single frame.
player.delay(0, weapon_fire_post, (player,))
def weapon_fire_post(player):
if player.no_recoil:
# Try to reduce the recoil and remove the spray pattern.
for prop in weapon_fire_post_properties:
player.set_network_property_vector(prop, NULL_VECTOR)
# Don't go further if the 'fire_rate' hasn't been modified.
if not player.fire_rate_changed:
return
# Get the weapon instance we saved earlier (in weapon_fire_pre()).
weapon = player.fired_weapon
# Get the current time.
cur_time = global_vars.current_time
# Get the next primary attack time.
next_attack = weapon.get_datamap_property_float('m_flNextPrimaryAttack')
# Calculate when the next primary attack should happen.
next_attack = (next_attack - cur_time) * 1.0 / player.fire_rate + cur_time
weapon.set_datamap_property_float('m_flNextPrimaryAttack', next_attack)
player.set_datamap_property_float('m_flNextAttack', cur_time)
# wcs_speedfire <userid> <fire rate>
@TypedServerCommand('wcs_speedfire')
def wcs_speedfire_command(command_info, userid:int, fire_rate:float):
try:
player = player_instances.from_userid(userid)
except ValueError:
echo_console(f'wcs_speedfire: invalid userid {userid}')
player.fire_rate = abs(fire_rate)
# wcs_speedfireoff <userid>
@TypedServerCommand('wcs_speedfireoff')
def wcs_speedfireoff_command(command_info, userid:int):
try:
player = player_instances.from_userid(userid)
except ValueError:
echo_console(f'wcs_speedfireoff: invalid userid {userid}')
player.fire_rate = 1.0
# wcs_rapidfire <userid>
@TypedServerCommand('wcs_rapidfire')
def wcs_rapidfire_command(command_info, userid:int):
try:
player = player_instances.from_userid(userid)
except ValueError:
echo_console(f'wcs_rapidfire: invalid userid {userid}')
if player.no_recoil:
player.enable_recoil()
player.fire_rate = 1.0
else:
player.disable_recoil()
player.fire_rate = 1.3
I've added the same commands that the SM plugin you attached uses, but in case you want finer control, replace them with these (or just add them so you can use both):
Syntax: Select all
# wcs_disable_recoil <userid>
@TypedServerCommand('wcs_disable_recoil')
def wcs_disable_recoil_command(command_info, userid:int):
try:
player = player_instances.from_userid(userid)
except ValueError:
echo_console(f'wcs_disable_recoil: invalid userid {userid}')
player.disable_recoil()
# wcs_enable_recoil <userid>
@TypedServerCommand('wcs_enable_recoil')
def wcs_enable_recoil_command(command_info, userid:int):
try:
player = player_instances.from_userid(userid)
except ValueError:
echo_console(f'wcs_enable_recoil: invalid userid {userid}')
player.enable_recoil()
# wcs_fire_rate <userid> <fire rate>
@TypedServerCommand('wcs_fire_rate')
def wcs_fire_rate_command(command_info, userid:int, fire_rate:float):
try:
player = player_instances.from_userid(userid)
except ValueError:
echo_console(f'wcs_fire_rate: invalid userid {userid}')
player.fire_rate = fire_rate
# wcs_reset_fire_rate <userid>
@TypedServerCommand('wcs_reset_fire_rate')
def wcs_reset_fire_rate_command(command_info, userid:int):
try:
player = player_instances.from_userid(userid)
except ValueError:
echo_console(f'wcs_reset_fire_rate: invalid userid {userid}')
player.fire_rate = 1.0
And if you want to see where your shots are going (server-side), you can use this:
Syntax: Select all
from engines.precache import Model
from entities.entity import Entity
from events import Event
from mathlib import Vector
model_light_glow = Model('sprites/light_glow03.vmt')
@Event('bullet_impact')
def bullet_impact(event):
create_sprite(
position=Vector(event['x'], event['y'], event['z']),
size=0.05,
color_str='0 255 0',
lifetime=5
)
def create_sprite(position, size, color_str, alpha=150, lifetime=0):
sprite = Entity.create('env_sprite')
sprite.model = model_light_glow
sprite.origin = position
sprite.scale = size
sprite.set_key_value_float('GlowProxySize', 0)
sprite.set_key_value_float('framerate', 15)
sprite.set_key_value_bool('disablereceiveshadows', True)
sprite.set_key_value_float('HDRColorScale', 1)
sprite.set_key_value_int('renderamt', alpha)
sprite.set_key_value_int('rendermode', 9)
sprite.set_key_value_string('rendercolor', color_str)
sprite.set_key_value_int('renderfx', 0)
sprite.set_key_value_int('spawnflags', 1)
sprite.spawn()
if lifetime > 0:
sprite.delay(lifetime, sprite.remove)