# ../addons/source-python/plugins/supermod/modules/flaregun.py
# Python
from random import uniform
# Source.Python
import core
from core import PLATFORM
from engines.sound import Attenuation
from entities.constants import CollisionGroup, SolidFlags
from entities.entity import Entity
from entities.helpers import index_from_pointer
from entities.hooks import EntityCondition, EntityPreHook
from events import Event
from listeners import (
ButtonStatus, OnButtonStateChanged, get_button_combination_status
)
from mathlib import QAngle
from memory import Convention, DataType, find_binary
from players.constants import PlayerButtons
from players.entity import Player
# Burn duration (in seconds) applied to entities that touch the flare.
IGNITE_TIME = 2
# How much flaregun ammo should the players spawn with?
MAX_FLARE_AMMO = 15
# Seconds until the player can fire another flare.
FLAREGUN_FIRE_RATE = 0.1
# Speed at which the flare is fired.
FLARE_THROW_FORCE = 1000
# Size of the flare.
FLARE_SCALE = 4.5
# Size of the signal particle effects.
FLARE_PARTICLES_SCALE = 4.5
# Should the flare only ignite players?
ONLY_IGNITE_PLAYERS = False
# Should the flare ignite the player that fired it?
IGNITE_OWNER = False
# Should the flare go through players?
PASS_THROUGH_PLAYERS = False
# Should the player's view shake/wobble when they fire a flare?
VIEW_PUNCH = True
# Message that will be displayed when the plugin gets loaded.
core.console_message("\n[Supermod] Flaregun loaded!")
# Condition used for the 'start_touch' hook.
is_flare = EntityCondition.equals_entity_classname('env_flare')
# =============================================================================
# >> CBasePlayer::ViewPunch
# =============================================================================
if PLATFORM == 'windows':
signature = \
b'\x55\x8B\xEC\xA1\x2A\x2A\x2A\x2A\x83\xEC\x0C\x83\x78\x30\x00\x56\x8B'
else:
signature = '_ZN11CBasePlayer9ViewPunchERK6QAngle'
view_punch = find_binary('server')[signature].make_function(
Convention.THISCALL,
(DataType.POINTER, DataType.POINTER),
DataType.VOID
)
# =============================================================================
# >> FLARE CLASS
# =============================================================================
class Flare(Entity):
"""Flare class."""
caching = True
@classmethod
def create(cls, owner_handle=None):
"""Creates a flare.
Args:
owner_handle (int): Inthandle of the entity that created the flare.
"""
flare = super().create('env_flare')
flare.set_key_value_float('scale', FLARE_PARTICLES_SCALE)
flare.spawn()
flare.model_scale = FLARE_SCALE
if owner_handle is not None:
flare.owner_handle = owner_handle
flare.solid_flags = SolidFlags.TRIGGER
if PASS_THROUGH_PLAYERS:
flare.collision_group = CollisionGroup.DEBRIS
else:
flare.collision_group = CollisionGroup.NONE
return flare
# =============================================================================
# >> PLAYER CLASS
# =============================================================================
class PlayerF(Player):
"""Extended Player class.
Args:
index (int): A valid player index.
caching (bool): Check for a cached instance?
Attributes:
flare_ammo (int): Current amount of ammo for the flare gun.
flare_ready (bool): Can the player shoot a flare?
flare_cooldown (Delay): Instance of Delay() used for reloading the
flaregun.
"""
sound_flare_empty = {
'sample': 'weapons/pistol/pistol_empty.wav',
'attenuation': Attenuation.NONE,
'volume': 1.0,
'download': True
}
sound_flare_shoot = {
'sample': 'weapons/flaregun/fire.wav',
'attenuation': Attenuation.NONE,
'volume': 1.0,
'download': True
}
def __init__(self, index, caching=True):
"""Initializes the object."""
super().__init__(index, caching)
self.flare_ammo = 0
self.flare_ready = False
self.flare_cooldown = None
def initialize_flares(self, ammo):
"""Gives the player ammo for the flaregun, and lets them use it."""
self.flare_ammo = ammo
self.flare_ready = True
def disable_flares(self):
"""Makes the player unable to use the flaregun."""
try:
# Stop the delay responsible for reloading the flaregun.
self.flare_cooldown.cancel()
except (AttributeError, ValueError):
# AttributeError: Missing Delay() instance.
# ValueError: Delay() already executed the callback.
pass
self.flare_ready = False
def shoot_flare(self):
"""Shoots a flare."""
if not self.flare_ready:
return
try:
# Try to get the name of the weapon the player is holding.
weapon_name = self.active_weapon.classname
except AttributeError:
# Player might be dead or missing their weapons.
return
# Is the player not holding a pistol?
if weapon_name != 'weapon_pistol':
return
# Is the player out of ammo?
if self.flare_ammo < 1:
self.play_sound(**PlayerF.sound_flare_empty)
return
flare = Flare.create(owner_handle=self.inthandle)
origin = self.eye_location
view_vector = self.view_vector
velocity = self.velocity.length_2D / 10
forward = view_vector * (25 + velocity)
origin += forward
flare.teleport(origin, self.view_angle)
flare.call_input('Launch', 0.0)
flare.base_velocity = view_vector * FLARE_THROW_FORCE
# Reduce the amount of ammo.
self.flare_ammo -= 1
self.flare_ready = False
self.flare_cooldown = self.delay(FLAREGUN_FIRE_RATE, setattr, (
self, 'flare_ready', True))
self.play_sound(**PlayerF.sound_flare_shoot)
if VIEW_PUNCH:
view_punch(self, QAngle(uniform(0.7, 1.1), uniform(-1.4, 1.4), 0))
# =============================================================================
# >> EVENTS AND LISTENERS
# =============================================================================
@Event('player_spawn')
def player_spawn(event):
"""Called when a player spawns."""
PlayerF.from_userid(event['userid']).initialize_flares(ammo=MAX_FLARE_AMMO)
@Event('player_death')
def player_death(event):
"""Called when a player dies."""
PlayerF.from_userid(event['userid']).disable_flares()
@OnButtonStateChanged
def on_button_state_changed(player, old_buttons, new_buttons):
"""Called when the button state of a player changed."""
status = get_button_combination_status(
old_buttons, new_buttons, PlayerButtons.ATTACK2)
# Did the player just press right click (+attack2)?
if status == ButtonStatus.PRESSED:
PlayerF(player.index).shoot_flare()
# =============================================================================
# >> HOOKS
# =============================================================================
@EntityPreHook(is_flare, 'start_touch')
def start_touch_pre(stack_data):
"""Called when an 'env_flare' entity touches another entity."""
try:
flare = Flare.cache[index_from_pointer(stack_data[0])]
except KeyError:
return
entity = Entity._obj(stack_data[1])
# Is this entity not a player (and do we only want to ignite players)?
if not entity.is_player() and ONLY_IGNITE_PLAYERS:
# Don't go further.
return
# Is this the entity that fired the flare (and do we want to avoid igniting
# the owner of the flare)?
if entity.inthandle == flare.owner_handle and not IGNITE_OWNER:
return
try:
# Try to set the entity on fire.
entity.ignite_lifetime(IGNITE_TIME)
except AttributeError:
# This entity can't be set on fire.
pass