[HL2:DM] Little Silent Hill

A place for requesting new Source.Python plugins to be made for your server.

Please request only one plugin per thread.
User avatar
Painkiller
Senior Member
Posts: 725
Joined: Sun Mar 01, 2015 8:09 am
Location: Germany
Contact:

Re: [HL2:DM] Little Silent Hill

Postby Painkiller » Mon Sep 07, 2020 8:40 am

daren adler wrote:"Yes"... your thought is right,,i do not have that no more PailKiller, If you could send me it or where to grab it again that would be a start. If i remember right, never could get that mod to work back then, but heck man i will give it a try again,,would be nice to have them come out after sound and silent hill starts. makes it more like the real silent hill. I am using a sound from a airraid that sounds real close to the game/movie sound.


(NEW THOUGHT), I have a skin i use for my hl2dm hurricane bots,,they are the hl2 zombies skins on combines. I could use them to spawn when the silent hill starts....i will try and see what i can do with it that way,,might work, might not. Would make it eaiser and less stuff to have to come up with!!!!.


Hello Daren, I looked but I do not have it anymore. Probably I deleted it because it was for Windows.

I looked at Alliedmodders but found nothing.
User avatar
Painkiller
Senior Member
Posts: 725
Joined: Sun Mar 01, 2015 8:09 am
Location: Germany
Contact:

Re: [HL2:DM] Little Silent Hill

Postby Painkiller » Mon Sep 07, 2020 10:26 am

VinciT wrote:
daren adler wrote:What could i do so the npc only spawn when the silent hill starts??. Is there another plugin you know of to do that, or some kind of a trigger that would make it work with your plugin?. stay cool :cool: and have a great weekend!!!. In the movies/games, when the sounds stops the silent hill bad guys come out to kill. That is what i am trying to do.
Here's a quick attempt at adding manhacks:

Syntax: Select all

# ../silent_hill/silent_hill.py (manhacks edition)

# Python
import random

# Source.Python
from colors import Color
from commands.server import ServerCommand
from core import PLATFORM
from cvars import ConVar
from engines.precache import Model
from engines.server import server, server_game_dll
from engines.sound import Sound
from entities.constants import RenderMode, RenderEffects
from entities.entity import BaseEntity, Entity
from entities.helpers import index_from_edict
from events import Event
from listeners import OnEntityDeleted, OnLevelInit, OnLevelEnd
from listeners.tick import Delay, Repeat
from mathlib import NULL_VECTOR, Vector
from memory import Convention, DataType, find_binary, get_virtual_function
from memory.hooks import PreHook
from players import PlayerGenerator
from players.entity import Player
from stringtables import string_tables


# How long until the apocalypse starts again (in seconds)?
INTERVALS = (30, 45, 60, 90)
# Seconds until the thick Silent Hill-like fog starts fading away.
FOG_FADE_DELAY = 60
# How long should the fading of the fog take (in seconds)?
FOG_FADE_TIME = 60

# Should NPCs spawn during the thick fog phase? (True/False)
NPCS_ENABLED = True
# Maximum number of NPCs at any given time.
NPCS_MAX = 32

# Apply a glowing green effect that can be seen through the fog when the
# manhacks spawn? (True/False)
MANHACKS_SPAWN_EFFECT = False
# How much health should manhacks spawn with?
MANHACKS_HEALTH = 100
# How much damage should they deal?
MANHACKS_DAMAGE = 25


FOG_COLOR = Color(185, 185, 185)
FLASH_COLOR = Color(255, 0, 0, 150)
FLASH_COLOR_END = Color(255, 0, 0, 255)


SIREN_SOUND = Sound('ambient/alarms/citadel_alert_loop2.wav')


# Sprite used for tinting the player's screen.
SCREEN_SPRITE = Model('sprites/white.vmt')
SCREEN_SPRITE_OFFSET = Vector(10, 0, 0)


# Offset for NPC spawn positions.
NPC_ORIGIN_OFFSET = Vector(0, 0, 32)
NPC_SPAWN_VELOCITY = (-500, -250, 250, 500)


# Dictionary used to keep track of 'env_sprite' entities we'll be using.
_black_screens = {}


# =============================================================================
# >> EVENTS AND LISTENERS
# =============================================================================
def load():
"""Called when the plugin gets loaded."""
# Modify the manhack related convars.
Manhack.initialize_settings()
# Are there any players on the server?
if server.num_players > 0:
dark_times.initialize()


def unload():
"""Called when the plugin gets unloaded."""
dark_times.stop(pause_init=False)
dark_times.remove_all_npcs()

# Remove any leftover player entities.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).remove_black_screen()


@OnLevelEnd
def on_level_end():
"""Called when the map starts changing."""
dark_times.stop()
# Remove old data (spawn points, npc indexes).
dark_times.clean_up_data()


@Event('round_start')
def round_start(event):
"""Called when a new round starts."""
dark_times.stop()
dark_times.initialize()


@OnLevelInit
def on_level_init(map_name):
"""Called when the new map is done loading."""
dark_times.initialize()


@OnEntityDeleted
def on_entity_deleted(base_entity):
"""Called when an entity gets deleted."""
try:
index = base_entity.index
except ValueError:
# Not a networked entity.
return

try:
# Was this one of our 'env_sprite' entities?
player_index = _black_screens.pop(index)
# Remove the instance reference from the player.
PlayerSH(player_index).black_screen = None
except KeyError:
pass

try:
# Was this an NPC we spawned?
dark_times.npc_indexes.remove(index)
except KeyError:
pass


@PreHook(get_virtual_function(server_game_dll, 'SetServerHibernation'))
def set_server_hibernation_pre(stack_data):
"""Called when the last player leaves, or the first player joins."""
# Did the last player just leave (server is being put into hibernation)?
if stack_data[1]:
dark_times.stop()
# Or did the first player just join?
else:
dark_times.initialize()


# =============================================================================
# >> PLAYER STUFF
# =============================================================================
class PlayerSH(Player):
"""Modified Player class."""

def __init__(self, index, caching=True):
"""Initializes the object."""
super().__init__(index, caching)
self.black_screen = None

@property
def viewmodel(self):
"""Returns the Entity instance of the player's viewmodel."""
return Entity.from_inthandle(self.get_property_int('m_hViewModel'))

def darken_view(self, amount):
"""Lowers the brightness of the player's screen."""
if self.black_screen is None:
self.black_screen = create_sprite(
origin=NULL_VECTOR, scale=30.0, model=SCREEN_SPRITE)

self.black_screen.set_parent(self.viewmodel, -1)
self.black_screen.teleport(SCREEN_SPRITE_OFFSET)

# Create a sprite:player reference for later use.
_black_screens[self.black_screen.index] = self.index

# Change the alpha/transparency of the 'env_sprite'.
self.black_screen.set_network_property_int('m_nBrightness', amount)

def remove_black_screen(self):
"""Removes the 'env_sprite' used for tinting the player's screen."""
try:
self.black_screen.remove()
except AttributeError:
return

self.black_screen = None


# =============================================================================
# >> DARK TIMES
# =============================================================================
class DarkTimes:
"""Class used to start and stop the apocalypse.

Attributes:
current_darkness (int): Level of darkness used for darkening players'
screens.
in_progress (bool): Is the apocalypse currently happening?
npc_indexes (set of int): Contains indexes of NPCs spawned during the
thick fog phase.
flash_think (Repeat): Instance of Repeat() used for looping the
`_flash_think()` function.
darken_think (Repeat): Instance of Repeat() used for looping the
`_darken_think()` function.
gather_data_think (Repeat): Instance of Repeat() used for looping the
`_gather_data_think()` function.
old_fog_values (tuple): Tuple that holds values of the previous fog.
_fog (Entity): Entity instance of the 'env_fog_controller' we'll be
using.
_delays (dict of Delay): Dictionary that holds any Delay() instances
we might be using.
_saved_time (float): Remaining time from the previous initialization.
_valid_npc_origins (list of Vector): List containing spawn positions
for NPCs.
"""

def __init__(self):
"""Initializes the object."""
self.current_darkness = 0
self.in_progress = False
self.npc_indexes = set()

self.flash_think = Repeat(self._flash_think)
self.darken_think = Repeat(self._darken_think)
self.gather_data_think = Repeat(self._gather_data_think)
self.spawn_npcs_think = Repeat(self._spawn_npcs_think)

self.old_fog_values = None
self._fog = None
self._delays = {}
self._saved_time = None
self._valid_npc_origins = []

def initialize(self, instant=False):
"""Starts the apocalypse after a randomly chosen delay."""
# Don't go further if the apocalypse is already happening.
if self.in_progress:
return

try:
self._delays['init'].cancel()
except (KeyError, ValueError):
pass

# Are we trying to instantly start the apocalypse?
if instant:
self.begin()
else:
self._delays['init'] = Delay(
# Resume the time to start from the previous initialization if
# there is one, otherwise pick a random time.
self._saved_time if self._saved_time else random.choice(
INTERVALS), self.begin)

def begin(self):
"""Starts the apocalypse."""
self.in_progress = True
self.current_darkness = 0
self._saved_time = None

try:
self._delays['init'].cancel()
except (KeyError, ValueError):
pass

SIREN_SOUND.play()
# Sync the red flashes with the siren sound - every time the players
# hear the siren, their screen will be flashed red.
self.flash_think.start(interval=6.5, limit=4, execute_on_start=True)
# Start lowering the brightness.
self.darken_think.start(interval=0.5, limit=40, execute_on_start=True)
# Look for valid spawn positions for NPCs during the siren phase.
self.gather_origin_data()

# Change to a thick fog similar to the one from Silent Hill.
# (credit: killer89 - https://gamebanana.com/prefabs/1308 )
self._delays['final_flash'] = Delay(
26, self.change_fog, (FOG_COLOR, FOG_COLOR, 0, 620))
# Start changing the fog back to normal.
self._delays['restoration'] = Delay(
26 + FOG_FADE_DELAY, self.restore_fog_smooth, (FOG_FADE_TIME,))

def stop(self, pause_init=True):
"""Stops the apocalypse and restores everything back to normal."""
SIREN_SOUND.stop()

# Stop the looping functions.
self.flash_think.stop()
self.darken_think.stop()
self.gather_data_think.stop()
self.spawn_npcs_think.stop()

if pause_init:
try:
# If we're stopping the apocalypse before it had a chance to
# begin, save the remaining time - so we can resume it later.
self._saved_time = self._delays['init'].time_remaining
except KeyError:
pass

# Cancel all delays.
for delay in self._delays.values():
try:
delay.cancel()
except ValueError:
continue

# Set the brightness back to normal levels.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).darken_view(0)

self.restore_fog()
self.in_progress = False
self._fog = None

def clean_up_data(self):
"""Removes data that's no longer needed/valid."""
self._valid_npc_origins.clear()
self.npc_indexes.clear()

def remove_all_npcs(self):
"""Removes all currently active NPCs."""
for index in self.npc_indexes.copy():
BaseEntity(index).remove()

def on_completed(self):
"""Called when the fog finally settles back to the default values."""
self.in_progress = False
# Prepare for the next apocalypse.
self.initialize()

def gather_origin_data(self):
"""Starts gathering valid spawn positions for NPCs."""
# Have we gathered a decent amount of spawn positions?
if len(self._valid_npc_origins) >= 32:
return

self.gather_data_think.start(
interval=2, limit=14, execute_on_start=True)

def _gather_data_think(self):
for edict in PlayerGenerator():
player = PlayerSH(index_from_edict(edict))

# Is this player dead?
if player.dead:
return

new_origin = player.origin + NPC_ORIGIN_OFFSET

# Go through previously added spawn positions.
for origin in self._valid_npc_origins:
# Is the new position too close to this one?
if origin.get_distance(new_origin) <= 256:
# No need to keep looking, stop the loop.
break
else:
# The new position is far enough, add it to the list.
self._valid_npc_origins.append(new_origin)

def _flash_think(self):
UTIL_ScreenFadeAll(FLASH_COLOR, 0.5, 0.25, 1)

def _darken_think(self):
# Increase the darkness.
self.current_darkness += 5

# Reduce the brightness for each player on the server.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).darken_view(
self.current_darkness)

def _spawn_npcs_think(self):
# Have we hit the NPC limit?
if len(self.npc_indexes) >= NPCS_MAX:
return

manhack = Manhack.create(origin=random.choice(self._valid_npc_origins))
# Push the manhack in a random direction.
manhack.set_property_vector(
'm_vForceVelocity', Vector(
random.choice(NPC_SPAWN_VELOCITY),
random.choice(NPC_SPAWN_VELOCITY),
500
))
# Add the manhack's index to the set.
self.npc_indexes.add(manhack.index)

def get_fog_instance(self):
"""Returns an Entity instance of an 'env_fog_controller'."""
if self._fog is not None:
return self._fog

old_fog = Entity.find('env_fog_controller')
# Does an 'env_fog_controller' already exist on this map?
if old_fog:
# Store the old values for later use.
self.old_fog_values = (
old_fog.get_property_color('m_fog.colorPrimary'),
old_fog.get_property_color('m_fog.colorSecondary'),
old_fog.fog_start,
old_fog.fog_end
)

# We'll use that one for our fog shenanigans.
self._fog = old_fog
return self._fog

# Guess we need to make a new one.
new_fog = Entity.create('env_fog_controller')
new_fog.fog_enable = True
new_fog.fog_blend = True
new_fog.fog_max_density = 1.0
new_fog.spawn_flags = 1
new_fog.spawn()

new_fog.target_name = 'silent_hill_fog'
# Fix for maps without fog.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).call_input(
'SetFogController', new_fog.target_name)

self._fog = new_fog
return self._fog

def remove_fog(self):
"""Removes the stored 'env_fog_controller' entity.

Note:
This is only used if we're making our own 'env_fog_controller'.
"""
try:
self._fog.remove()
except AttributeError:
return

self._fog = None

def change_fog(self, color1, color2, start, end, final_flash=True):
"""Changes the fog visuals.

Args:
color1 (Color): Primary color of the fog.
color2 (Color): Secondary color of the fog.
start (float): Distance at which the fog begins.
end (float): Distance at which the fog is at its maximum.
final_flash (bool): Is this the final red flash?
"""
fog = self.get_fog_instance()
fog.set_color(color1)
fog.set_color_secondary(color2)
fog.set_start_dist(start)
fog.set_end_dist(end)

# Is this the final flash?
if final_flash:
# Add a stronger flash to mask the changes in fog and screen
# brightness.
UTIL_ScreenFadeAll(FLASH_COLOR_END, 1, 0.5, 1)

for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).darken_view(0)

# Should we start spawning NPCs?
if NPCS_ENABLED:
# Spawn them only during the thick fog phase.
self.spawn_npcs_think.start(
interval=3, limit=FOG_FADE_DELAY / 3)

def restore_fog(self):
"""Restores the fog back to normal."""
if self._fog is None:
return

# Was the map without fog?
if self.old_fog_values is None:
self.remove_fog()
return

self.change_fog(*self.old_fog_values, False)

def restore_fog_smooth(self, duration):
"""Smoothly restores the fog back to normal over the given duration."""
if self._fog is None:
return

should_remove = False
old_values = self.old_fog_values
# Was the map missing an 'env_fog_controller' entity?
if old_values is None:
should_remove = True
# Just increase the 'start' and 'end' values for the fog before
# removing it - making the transition semi-smooth.
old_values = (FOG_COLOR, FOG_COLOR, 512, 14000)

self._fog.set_color_lerp_to(old_values[0])
self._fog.set_color_secondary_lerp_to(old_values[1])
self._fog.set_start_dist_lerp_to(old_values[2])
self._fog.set_end_dist_lerp_to(old_values[3])
# Add 0.001 to the transition duration to avoid flashes caused by the
# fog bouncing back (engine quirk) before being set in place.
self._fog.fog_lerp_time = duration + 0.001
self._fog.start_fog_transition()

# If we created our own fog, we need to remove it.
if should_remove:
self._delays['ending'] = self._fog.delay(duration, self.remove_fog)
else:
# Make sure the fog values stay put after the transition.
self._delays['ending'] = self._fog.delay(
duration, self.change_fog, (*self.old_fog_values, False))

# The cycle is complete - let's do that again!
self._delays['restart'] = Delay(duration, self.on_completed)


dark_times = DarkTimes()


# =============================================================================
# >> UTIL_SCREENFADEALL - https://git.io/JJXoe
# =============================================================================
server_binary = find_binary('server')


if PLATFORM == 'windows':
identifier = b'\x55\x8B\xEC\xD9\x45\x10\x8D\x45\xF4'
else:
identifier = '_Z18UTIL_ScreenFadeAllRK9color32_sffi'


UTIL_ScreenFadeAll = server_binary[identifier].make_function(
Convention.CDECL,
(DataType.POINTER, DataType.FLOAT, DataType.FLOAT, DataType.INT),
DataType.VOID
)


# =============================================================================
# >> ENV_SPRITE
# =============================================================================
def create_sprite(origin, scale, model):
"""Creates an 'env_sprite' entity.

Args:
origin (Vector): Spawn position of the 'env_sprite'.
scale (float): Size of the sprite (max size: 64.0).
model (Model): Appearance of the sprite.
"""
sprite = Entity.create('env_sprite')
sprite.model = model
sprite.origin = origin
sprite.set_key_value_float('scale', scale)
sprite.set_key_value_bool('disablereceiveshadows', True)
sprite.set_key_value_float('HDRColorScale', 0)
sprite.render_amt = 1
sprite.render_mode = RenderMode.TRANS_COLOR
sprite.set_key_value_string('rendercolor', '0 0 0')
sprite.render_fx = RenderEffects.NONE
sprite.spawn_flags = 1
sprite.spawn()
return sprite


# =============================================================================
# >> NPC_MANHACK
# =============================================================================
class Manhack(Entity):
"""Class used to create and manipulate 'npc_manhack' entities."""
caching = True
effect_name = 'vortigaunt_hand_glow'
settings = {
'health': ConVar('sk_manhack_health'),
'damage': ConVar('sk_manhack_melee_dmg')
}

@staticmethod
def initialize_settings():
"""Changes the convars for 'npc_manhack' health and damage."""
Manhack.settings['health'].set_int(MANHACKS_HEALTH)
Manhack.settings['damage'].set_int(MANHACKS_DAMAGE)

@classmethod
def create(cls, origin):
"""Creates an 'npc_manhack' entity at the specified origin."""
manhack = cls(BaseEntity.create('npc_manhack').index)
manhack.origin = origin
# Make the manhack fully transparent.
manhack.render_amt = 1
# Slowly fade in the manhack.
manhack.set_key_value_int('renderfx', 7)
# Set a couple of spawn flags.
# 2: Gag (No IDLE sounds until angry)
# 256: Long Visibility/Shoot
manhack.spawn_flags = 2 + 256
manhack.spawn()

if MANHACKS_SPAWN_EFFECT:
manhack.create_spawn_particle()

# Make the manhack hate the players.
manhack.call_input('SetRelationship', 'player D_HT 99')
return manhack

def create_spawn_particle(self):
"""Creates and parents an 'info_particle_system' to the manhack."""
particle = Entity.create('info_particle_system')
particle.origin = self.origin
particle.effect_name = Manhack.effect_name
particle.effect_index = string_tables.ParticleEffectNames.add_string(
Manhack.effect_name)
particle.start()

# Parent the particle to the 'npc_manhack'.
particle.set_parent(self)
# Remove the particle after a second.
particle.delay(1, particle.remove)


# =============================================================================
# >> SERVER COMMANDS
# =============================================================================
@ServerCommand('force_dark_times')
def force_dark_times_cmd(command):
dark_times.initialize(instant=True)
Manhacks will start spawning once the siren stops, and will continue to spawn until the fog starts to fade away. This can be easily expanded upon to include other types of NPCs as well. If anyone wishes to do so, this function is responsible for spawning NPCs during the fog phase:

Syntax: Select all

def _spawn_npcs_think(self):
# Have we hit the NPC limit?
if len(self.npc_indexes) >= NPCS_MAX:
return

manhack = Manhack.create(origin=random.choice(self._valid_npc_origins))
# Push the manhack in a random direction.
manhack.set_property_vector(
'm_vForceVelocity', Vector(
random.choice(NPC_SPAWN_VELOCITY),
random.choice(NPC_SPAWN_VELOCITY),
500
))
# Add the manhack's index to the set.
self.npc_indexes.add(manhack.index)
Make sure to setup the convars for any NPCs you add either in autoexec/server.cfg or through code (see how I did it in the Manhack class).

Also, I've updated the plugin in the original post to include two bugfixes:
  • Server could crash after a map change due to me not removing the reference to the fog from the previous map.
  • Apocalypse not starting if mp_restartgame was used.


It would be interesting to bring a fast_zombie back into action.

I think I haven't seen a fast_zombie in HL2DM or in Coop Maps for 12 years or more
User avatar
L'In20Cible
Project Leader
Posts: 1533
Joined: Sat Jul 14, 2012 9:29 pm
Location: Québec

Re: [HL2:DM] Little Silent Hill

Postby L'In20Cible » Mon Sep 07, 2020 11:01 am

VinciT wrote:

Syntax: Select all

@classmethod
def create(cls, origin):
"""Creates an 'npc_manhack' entity at the specified origin."""
manhack = cls(BaseEntity.create('npc_manhack').index)
As of #292, BaseEntity.create is a classmethod so you could use super() directly. :smile:

Syntax: Select all

manhack = super().create('npc_manhack')
User avatar
daren adler
Senior Member
Posts: 328
Joined: Sat May 18, 2019 7:42 pm

Re: [HL2:DM] Little Silent Hill

Postby daren adler » Mon Sep 07, 2020 4:43 pm

THANK YOU VinciT :cool: :cool: ........Its works great, Thank you Great job :smile: :smile: :smile:
PS-- Could you add the zombie for me please? i tryed and messed it all up...im not so good as ur.
and how to/or if you will,,lower the amount of manhacks please,,,i would like if the manhack/zombies spawn only like 4 to 5 each time they spawn
(i use bots and they dont kill the npc's,,so if you dont kill the manhacks or zombies, they will add up a lot.
User avatar
daren adler
Senior Member
Posts: 328
Joined: Sat May 18, 2019 7:42 pm

Re: [HL2:DM] Little Silent Hill

Postby daren adler » Mon Sep 07, 2020 8:37 pm

Code: Select all

 [SP] Caught an Exception:
Traceback (most recent call last):
  File "..\addons\source-python\packages\source-python\listeners\tick.py", line 80, in _tick
    self.pop(0).execute()
  File "..\addons\source-python\packages\source-python\listeners\tick.py", line 161, in execute
    return self.callback(*self.args, **self.kwargs)
  File "..\addons\source-python\packages\source-python\listeners\tick.py", line 606, in _execute
    self.callback(*self.args, **self.kwargs)
  File "..\addons\source-python\plugins\silent_hill\silent_hill.py", line 379, in _spawn_npcs_think
    manhack = Manhack.create(origin=random.choice(self._valid_npc_origins))
  File "..\addons\source-python\Python3\random.py", line 257, in choice
    raise IndexError('Cannot choose from an empty sequence') from None

IndexError: Cannot choose from an empty sequence


Got this error and i did not change anything. Please help. Thank you.
User avatar
VinciT
Senior Member
Posts: 331
Joined: Thu Dec 18, 2014 2:41 am

Re: [HL2:DM] Little Silent Hill

Postby VinciT » Mon Sep 07, 2020 8:52 pm

L'In20Cible wrote:As of #292, BaseEntity.create is a classmethod so you could use super() directly. :smile:

Syntax: Select all

manhack = super().create('npc_manhack')
Nice! Thanks for the tip.

daren adler wrote:THANK YOU VinciT :cool: :cool: ........Its works great, Thank you Great job :smile: :smile: :smile:
PS-- Could you add the zombie for me please? i tryed and messed it all up...im not so good as ur.
and how to/or if you will,,lower the amount of manhacks please,,,i would like if the manhack/zombies spawn only like 4 to 5 each time they spawn
(i use bots and they dont kill the npc's,,so if you dont kill the manhacks or zombies, they will add up a lot.
Here you go:

Syntax: Select all

# ../silent_hill/silent_hill.py (npc edition)

# Python
import random

# Source.Python
from colors import Color
from commands.server import ServerCommand
from core import PLATFORM
from cvars import ConVar
from engines.precache import Model
from engines.server import server, server_game_dll
from engines.sound import Sound
from entities.constants import RenderMode, RenderEffects
from entities.entity import BaseEntity, Entity
from entities.helpers import index_from_edict, pointer_from_edict
from events import Event
from listeners import OnEntityDeleted, OnLevelInit, OnLevelEnd
from listeners.tick import Delay, Repeat
from mathlib import NULL_VECTOR, Vector
from memory import (Convention, DataType, find_binary, get_virtual_function,
NULL)
from memory.hooks import PreHook
from players import PlayerGenerator
from players.entity import Player
from stringtables import string_tables


# How long until the apocalypse starts again (in seconds)?
INTERVALS = (30, 45, 60, 90)
# Seconds until the thick Silent Hill-like fog starts fading away.
FOG_FADE_DELAY = 60
# How long should the fading of the fog take (in seconds)?
FOG_FADE_TIME = 60

# Should NPCs spawn during the thick fog phase? (True/False)
NPCS_ENABLED = True
# Maximum number of NPCs at any given time.
NPCS_MAX = 5

# Apply a glowing green effect that can be seen through the fog when the
# manhacks spawn? (True/False)
MANHACKS_SPAWN_EFFECT = False
# How much health should manhacks spawn with?
MANHACKS_HEALTH = 100
# How much damage should they deal?
MANHACKS_DAMAGE = 25

# How much health should zombies spawn with?
ZOMBIE_HEALTH = 120
# How much damage should they deal?
ZOMBIE_DAMAGE = 30


FOG_COLOR = Color(185, 185, 185)
FLASH_COLOR = Color(255, 0, 0, 150)
FLASH_COLOR_END = Color(255, 0, 0, 255)


SIREN_SOUND = Sound('ambient/alarms/citadel_alert_loop2.wav')


# Sprite used for tinting the player's screen.
SCREEN_SPRITE = Model('sprites/white.vmt')
SCREEN_SPRITE_OFFSET = Vector(10, 0, 0)


# Offset for NPC spawn positions.
NPC_ORIGIN_OFFSET = Vector(0, 0, 32)
NPC_SPAWN_VELOCITY = (-500, -250, 250, 500)


# Dictionary used to keep track of 'env_sprite' entities we'll be using.
_black_screens = {}


# =============================================================================
# >> EVENTS AND LISTENERS
# =============================================================================
def load():
"""Called when the plugin gets loaded."""
# Modify the manhack related convars.
Manhack.initialize_settings()
# Modify the zombie related convars.
Zombie.initialize_settings()

# Are there any players on the server?
if server.num_players > 0:
dark_times.initialize()


def unload():
"""Called when the plugin gets unloaded."""
dark_times.stop(pause_init=False)
dark_times.remove_all_npcs()

# Remove any leftover player entities.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).remove_black_screen()


@OnLevelEnd
def on_level_end():
"""Called when the map starts changing."""
dark_times.stop()
# Remove old data (spawn points, npc indexes).
dark_times.clean_up_data()


@Event('round_start')
def round_start(event):
"""Called when a new round starts."""
dark_times.stop()
dark_times.initialize()


@OnLevelInit
def on_level_init(map_name):
"""Called when the new map is done loading."""
dark_times.initialize()


@OnEntityDeleted
def on_entity_deleted(base_entity):
"""Called when an entity gets deleted."""
try:
index = base_entity.index
except ValueError:
# Not a networked entity.
return

try:
# Was this one of our 'env_sprite' entities?
player_index = _black_screens.pop(index)
# Remove the instance reference from the player.
PlayerSH(player_index).black_screen = None
except (KeyError, ValueError):
pass

try:
# Was this an NPC we spawned?
dark_times.npc_indexes.remove(index)
except KeyError:
pass


@PreHook(get_virtual_function(server_game_dll, 'SetServerHibernation'))
def set_server_hibernation_pre(stack_data):
"""Called when the last player leaves, or the first player joins."""
# Did the last player just leave (server is being put into hibernation)?
if stack_data[1]:
dark_times.stop()
# Or did the first player just join?
else:
dark_times.initialize()


# =============================================================================
# >> PLAYER STUFF
# =============================================================================
class PlayerSH(Player):
"""Modified Player class."""

def __init__(self, index, caching=True):
"""Initializes the object."""
super().__init__(index, caching)
self.black_screen = None
self.target_name = f'player_{self.userid}'

@property
def viewmodel(self):
"""Returns the Entity instance of the player's viewmodel."""
return Entity.from_inthandle(self.get_property_int('m_hViewModel'))

def darken_view(self, amount):
"""Lowers the brightness of the player's screen."""
if self.black_screen is None:
self.black_screen = create_sprite(
origin=NULL_VECTOR, scale=30.0, model=SCREEN_SPRITE)

self.black_screen.set_parent(self.viewmodel, -1)
self.black_screen.teleport(SCREEN_SPRITE_OFFSET)

# Create a sprite:player reference for later use.
_black_screens[self.black_screen.index] = self.index

# Change the alpha/transparency of the 'env_sprite'.
self.black_screen.set_network_property_int('m_nBrightness', amount)

def remove_black_screen(self):
"""Removes the 'env_sprite' used for tinting the player's screen."""
try:
self.black_screen.remove()
except AttributeError:
return

self.black_screen = None


# =============================================================================
# >> DARK TIMES
# =============================================================================
class DarkTimes:
"""Class used to start and stop the apocalypse.

Attributes:
current_darkness (int): Level of darkness used for darkening players'
screens.
in_progress (bool): Is the apocalypse currently happening?
npc_indexes (set of int): Contains indexes of NPCs spawned during the
thick fog phase.
flash_think (Repeat): Instance of Repeat() used for looping the
`_flash_think()` function.
darken_think (Repeat): Instance of Repeat() used for looping the
`_darken_think()` function.
gather_data_think (Repeat): Instance of Repeat() used for looping the
`_gather_data_think()` function.
old_fog_values (tuple): Tuple that holds values of the previous fog.
_fog (Entity): Entity instance of the 'env_fog_controller' we'll be
using.
_delays (dict of Delay): Dictionary that holds any Delay() instances
we might be using.
_saved_time (float): Remaining time from the previous initialization.
_valid_npc_origins (list of Vector): List containing spawn positions
for NPCs.
"""

def __init__(self):
"""Initializes the object."""
self.current_darkness = 0
self.in_progress = False
self.npc_indexes = set()

self.flash_think = Repeat(self._flash_think)
self.darken_think = Repeat(self._darken_think)
self.gather_data_think = Repeat(self._gather_data_think)
self.spawn_npcs_think = Repeat(self._spawn_npcs_think)

self.old_fog_values = None
self._fog = None
self._delays = {}
self._saved_time = None
self._valid_npc_origins = []

def initialize(self, instant=False):
"""Starts the apocalypse after a randomly chosen delay."""
# Don't go further if the apocalypse is already happening.
if self.in_progress:
return

try:
self._delays['init'].cancel()
except (KeyError, ValueError):
pass

# Are we trying to instantly start the apocalypse?
if instant:
self.begin()
else:
self._delays['init'] = Delay(
# Resume the time to start from the previous initialization if
# there is one, otherwise pick a random time.
self._saved_time if self._saved_time else random.choice(
INTERVALS), self.begin)

def begin(self):
"""Starts the apocalypse."""
self.in_progress = True
self.current_darkness = 0
self._saved_time = None

try:
self._delays['init'].cancel()
except (KeyError, ValueError):
pass

SIREN_SOUND.play()
# Sync the red flashes with the siren sound - every time the players
# hear the siren, their screen will be flashed red.
self.flash_think.start(interval=6.5, limit=4, execute_on_start=True)
# Start lowering the brightness.
self.darken_think.start(interval=0.5, limit=40, execute_on_start=True)
# Look for valid spawn positions for NPCs during the siren phase.
self.gather_origin_data()

# Change to a thick fog similar to the one from Silent Hill.
# (credit: killer89 - https://gamebanana.com/prefabs/1308 )
self._delays['final_flash'] = Delay(
26, self.change_fog, (FOG_COLOR, FOG_COLOR, 0, 620))
# Start changing the fog back to normal.
self._delays['restoration'] = Delay(
26 + FOG_FADE_DELAY, self.restore_fog_smooth, (FOG_FADE_TIME,))

def stop(self, pause_init=True):
"""Stops the apocalypse and restores everything back to normal."""
SIREN_SOUND.stop()

# Stop the looping functions.
self.flash_think.stop()
self.darken_think.stop()
self.gather_data_think.stop()
self.spawn_npcs_think.stop()

if pause_init:
try:
# If we're stopping the apocalypse before it had a chance to
# begin, save the remaining time - so we can resume it later.
self._saved_time = self._delays['init'].time_remaining
except KeyError:
pass

# Cancel all delays.
for delay in self._delays.values():
try:
delay.cancel()
except ValueError:
continue

# Set the brightness back to normal levels.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).darken_view(0)

self.restore_fog()
self.in_progress = False
self._fog = None

def clean_up_data(self):
"""Removes data that's no longer needed/valid."""
self._valid_npc_origins.clear()
self.npc_indexes.clear()

def remove_all_npcs(self):
"""Removes all currently active NPCs."""
for index in self.npc_indexes.copy():
BaseEntity(index).remove()

def on_completed(self):
"""Called when the fog finally settles back to the default values."""
self.in_progress = False
# Prepare for the next apocalypse.
self.initialize()

def gather_origin_data(self):
"""Starts gathering valid spawn positions for NPCs."""
# Have we gathered a decent amount of spawn positions?
if len(self._valid_npc_origins) >= 32:
return

self.gather_data_think.start(
interval=2, limit=14, execute_on_start=True)

def _gather_data_think(self):
for edict in PlayerGenerator():
player = PlayerSH(index_from_edict(edict))

# Is this player dead?
if player.dead:
continue

new_origin = player.origin + NPC_ORIGIN_OFFSET

# Go through previously added spawn positions.
for origin in self._valid_npc_origins:
# Is the new position too close to this one?
if origin.get_distance(new_origin) <= 256:
# No need to keep looking, stop the loop.
break
else:
# The new position is far enough, add it to the list.
self._valid_npc_origins.append(new_origin)

def _flash_think(self):
UTIL_ScreenFadeAll(FLASH_COLOR, 0.5, 0.25, 1)

def _darken_think(self):
# Increase the darkness.
self.current_darkness += 5

# Reduce the brightness for each player on the server.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).darken_view(
self.current_darkness)

def _spawn_npcs_think(self):
# Have we hit the NPC limit?
if len(self.npc_indexes) >= NPCS_MAX:
return

try:
origin = random.choice(self._valid_npc_origins)
except IndexError:
return

# Pick an NPC to spawn (55% for a manhack, 45% for a zombie)
npc_class = random.choices((Manhack, Zombie), weights=(55, 45))[0]
npc = npc_class.create(origin)

# Add the NPC's index to the set.
self.npc_indexes.add(npc.index)

def get_fog_instance(self):
"""Returns an Entity instance of an 'env_fog_controller'."""
if self._fog is not None:
return self._fog

old_fog = Entity.find('env_fog_controller')
# Does an 'env_fog_controller' already exist on this map?
if old_fog:
# Store the old values for later use.
self.old_fog_values = (
old_fog.get_property_color('m_fog.colorPrimary'),
old_fog.get_property_color('m_fog.colorSecondary'),
old_fog.fog_start,
old_fog.fog_end
)

# We'll use that one for our fog shenanigans.
self._fog = old_fog
return self._fog

# Guess we need to make a new one.
new_fog = Entity.create('env_fog_controller')
new_fog.fog_enable = True
new_fog.fog_blend = True
new_fog.fog_max_density = 1.0
new_fog.spawn_flags = 1
new_fog.spawn()

new_fog.target_name = 'silent_hill_fog'
# Fix for maps without fog.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).call_input(
'SetFogController', new_fog.target_name)

self._fog = new_fog
return self._fog

def remove_fog(self):
"""Removes the stored 'env_fog_controller' entity.

Note:
This is only used if we're making our own 'env_fog_controller'.
"""
try:
self._fog.remove()
except AttributeError:
return

self._fog = None

def change_fog(self, color1, color2, start, end, final_flash=True):
"""Changes the fog visuals.

Args:
color1 (Color): Primary color of the fog.
color2 (Color): Secondary color of the fog.
start (float): Distance at which the fog begins.
end (float): Distance at which the fog is at its maximum.
final_flash (bool): Is this the final red flash?
"""
fog = self.get_fog_instance()
fog.set_color(color1)
fog.set_color_secondary(color2)
fog.set_start_dist(start)
fog.set_end_dist(end)

# Is this the final flash?
if final_flash:
# Add a stronger flash to mask the changes in fog and screen
# brightness.
UTIL_ScreenFadeAll(FLASH_COLOR_END, 1, 0.5, 1)

for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).darken_view(0)

# Should we start spawning NPCs?
if NPCS_ENABLED:
# Spawn them only during the thick fog phase.
self.spawn_npcs_think.start(
interval=3, limit=FOG_FADE_DELAY / 3)

def restore_fog(self):
"""Restores the fog back to normal."""
if self._fog is None:
return

# Was the map without fog?
if self.old_fog_values is None:
self.remove_fog()
return

self.change_fog(*self.old_fog_values, False)

def restore_fog_smooth(self, duration):
"""Smoothly restores the fog back to normal over the given duration."""
if self._fog is None:
return

should_remove = False
old_values = self.old_fog_values
# Was the map missing an 'env_fog_controller' entity?
if old_values is None:
should_remove = True
# Just increase the 'start' and 'end' values for the fog before
# removing it - making the transition semi-smooth.
old_values = (FOG_COLOR, FOG_COLOR, 512, 14000)

self._fog.set_color_lerp_to(old_values[0])
self._fog.set_color_secondary_lerp_to(old_values[1])
self._fog.set_start_dist_lerp_to(old_values[2])
self._fog.set_end_dist_lerp_to(old_values[3])
# Add 0.001 to the transition duration to avoid flashes caused by the
# fog bouncing back (engine quirk) before being set in place.
self._fog.fog_lerp_time = duration + 0.001
self._fog.start_fog_transition()

# If we created our own fog, we need to remove it.
if should_remove:
self._delays['ending'] = self._fog.delay(duration, self.remove_fog)
else:
# Make sure the fog values stay put after the transition.
self._delays['ending'] = self._fog.delay(
duration, self.change_fog, (*self.old_fog_values, False))

# The cycle is complete - let's do that again!
self._delays['restart'] = Delay(duration, self.on_completed)


dark_times = DarkTimes()


# =============================================================================
# >> UTIL_SCREENFADEALL - https://git.io/JJXoe
# =============================================================================
server_binary = find_binary('server')


if PLATFORM == 'windows':
identifier_screen = b'\x55\x8B\xEC\xD9\x45\x10\x8D\x45\xF4'
else:
identifier_screen = '_Z18UTIL_ScreenFadeAllRK9color32_sffi'


UTIL_ScreenFadeAll = server_binary[identifier_screen].make_function(
Convention.CDECL,
(DataType.POINTER, DataType.FLOAT, DataType.FLOAT, DataType.INT),
DataType.VOID
)


# =============================================================================
# >> UTIL_GETLOCALPLAYER FIX - (thank you Ayuto)
# viewtopic.php?f=20&t=1907#p12247
# =============================================================================
if PLATFORM == 'windows':
identifier_local = b'\xA1\x2A\x2A\x2A\x2A\x8B\x2A\x2A\x83\x2A\x01\x7E\x03\x33\xC0\xC3'
else:
identifier_local = '_Z19UTIL_GetLocalPlayerv'


UTIL_GetLocalPlayer = server_binary[identifier_local].make_function(
Convention.CDECL,
[],
DataType.POINTER
)


@PreHook(UTIL_GetLocalPlayer)
def get_local_player_pre(stack_data):
"""Called when the engine tries to get the local Player object.

This function was designed for single-player and should NOT be called in
multi-player games unless you want the server to crash.
"""
for edict in PlayerGenerator():
try:
return pointer_from_edict(edict)
except ValueError:
pass

return NULL


# =============================================================================
# >> ENV_SPRITE
# =============================================================================
def create_sprite(origin, scale, model):
"""Creates an 'env_sprite' entity.

Args:
origin (Vector): Spawn position of the 'env_sprite'.
scale (float): Size of the sprite (max size: 64.0).
model (Model): Appearance of the sprite.
"""
sprite = Entity.create('env_sprite')
sprite.model = model
sprite.origin = origin
sprite.set_key_value_float('scale', scale)
sprite.set_key_value_bool('disablereceiveshadows', True)
sprite.set_key_value_float('HDRColorScale', 0)
sprite.render_amt = 1
sprite.render_mode = RenderMode.TRANS_COLOR
sprite.set_key_value_string('rendercolor', '0 0 0')
sprite.render_fx = RenderEffects.NONE
sprite.spawn_flags = 1
sprite.spawn()
return sprite


# =============================================================================
# >> NPC_MANHACK
# =============================================================================
class Manhack(Entity):
"""Class used to create and manipulate 'npc_manhack' entities."""
caching = True
effect_name = 'vortigaunt_hand_glow'
settings = {
'health': ConVar('sk_manhack_health'),
'damage': ConVar('sk_manhack_melee_dmg')
}

@staticmethod
def initialize_settings():
"""Changes the convars for 'npc_manhack' health and damage."""
Manhack.settings['health'].set_int(MANHACKS_HEALTH)
Manhack.settings['damage'].set_int(MANHACKS_DAMAGE)

@classmethod
def create(cls, origin):
"""Creates an 'npc_manhack' entity at the specified origin."""
manhack = super().create('npc_manhack')
manhack.origin = origin
# Make the manhack fully transparent.
manhack.render_amt = 1
# Slowly fade in the manhack.
manhack.set_key_value_int('renderfx', 7)
# Set a couple of spawn flags.
# 2: Gag (No IDLE sounds until angry)
# 256: Long Visibility/Shoot
manhack.spawn_flags = 2 + 256
manhack.spawn()

if MANHACKS_SPAWN_EFFECT:
manhack.create_spawn_particle()

# Push the manhack in a random direction.
manhack.set_property_vector(
'm_vForceVelocity', Vector(
random.choice(NPC_SPAWN_VELOCITY),
random.choice(NPC_SPAWN_VELOCITY),
500
))

# Make the manhack hate the players.
manhack.call_input('SetRelationship', 'player D_HT 99')
return manhack

def create_spawn_particle(self):
"""Creates and parents an 'info_particle_system' to the manhack."""
particle = Entity.create('info_particle_system')
particle.origin = self.origin
particle.effect_name = Manhack.effect_name
particle.effect_index = string_tables.ParticleEffectNames.add_string(
Manhack.effect_name)
particle.start()

# Parent the particle to the 'npc_manhack'.
particle.set_parent(self)
# Remove the particle after a second.
particle.delay(1, particle.remove)


# =============================================================================
# >> NPC_ZOMBIE
# =============================================================================
class Zombie(Entity):
"""Class used to create 'npc_zombie' entities."""
caching = True
settings = {
'health': ConVar('sk_zombie_health'),
'damage': ConVar('sk_zombie_dmg_one_slash')
}

@staticmethod
def initialize_settings():
"""Changes the convars for 'npc_manhack' health and damage."""
Zombie.settings['health'].set_int(ZOMBIE_HEALTH)
Zombie.settings['damage'].set_int(ZOMBIE_DAMAGE)

@classmethod
def create(cls, origin):
"""Creates an 'npc_zombie' at the specified origin."""
zombie = super().create('npc_zombie')
zombie.target_name = f'zombie_{zombie.inthandle}'
zombie.origin = origin
zombie.render_amt = 1
zombie.set_key_value_int('renderfx', 7)
zombie.spawn_flags = 2 + 256
zombie.spawn()
zombie.call_input('SetRelationship', 'player D_HT 99')
# Start going towards a random player after a short delay.
# We need to do this so the zombie doesn't just stand still once it
# spawns. This way it won't block the spawn point.
zombie.delay(0.5, zombie.seek_random_player)
return zombie

def seek_random_player(self):
"""Starts heading towards a randomly selected player."""
targets = []
for edict in PlayerGenerator():
targets.append(PlayerSH(index_from_edict(edict)).target_name)

# We need an 'aiscripted_schedule' entity to make the zombie do things.
schedule = Entity.create('aiscripted_schedule')
# Set which entity we'd like to command.
schedule.set_key_value_string('m_iszEntity', self.target_name)
# Set their current state to 'Idle'.
# (0: None, 1: Idle, 2: Alert, 3: Combat)
schedule.set_key_value_int('forcestate', 1)
# Make the zombie set the target as their enemy and start going towards
# them. Here are all the options for this keyvalue:
# 0: <None>
# 1: Walk to Goal Entity
# 2: Run to Goal Entity
# 3: Set enemy to Goal Entity
# 4: Walk Goal Path
# 5: Run Goal Path
# 6: Set enemy to Goal Entity AND Run to Goal Entity
schedule.set_key_value_int('schedule', 6)
# In case the zombie takes damage, stop the schedule.
# (0: General, 1: Damage or death, 2: Death)
schedule.set_key_value_int('interruptability', 1)
# Pick a random player to be the enemy.
schedule.set_key_value_string('goalent', random.choice(targets))
schedule.spawn()
schedule.call_input('StartSchedule')
schedule.delay(5, schedule.remove)


# =============================================================================
# >> SERVER COMMANDS
# =============================================================================
@ServerCommand('force_dark_times')
def force_dark_times_cmd(command):
dark_times.initialize(instant=True)
You can modify the maximum number of active/alive NPCs with the NPCS_MAX setting, I've set it to 5 for you. I have also included a quick fix for the error you just posted. The error indicates that no spawn points were generated for the NPCs. What were you doing when it occured? Were you in spectate?

Side note: Painkiller and I figured out that the SetServerHibernation hook was preventing the plugin from working on Linux (at least on his server). The plugin was loading without any errors but basically anything below the function for the hook wasn't loaded. :confused: I'm really curious as to how that works.
Last edited by VinciT on Wed Sep 09, 2020 12:16 am, edited 2 times in total.
ImageImageImageImageImage
User avatar
daren adler
Senior Member
Posts: 328
Joined: Sat May 18, 2019 7:42 pm

Re: [HL2:DM] Little Silent Hill

Postby daren adler » Mon Sep 07, 2020 9:00 pm

VinciT wrote:
L'In20Cible wrote:As of #292, BaseEntity.create is a classmethod so you could use super() directly. :smile:

Syntax: Select all

manhack = super().create('npc_manhack')
Nice! Thanks for the tip.

daren adler wrote:THANK YOU VinciT :cool: :cool: ........Its works great, Thank you Great job :smile: :smile: :smile:
PS-- Could you add the zombie for me please? i tryed and messed it all up...im not so good as ur.
and how to/or if you will,,lower the amount of manhacks please,,,i would like if the manhack/zombies spawn only like 4 to 5 each time they spawn
(i use bots and they dont kill the npc's,,so if you dont kill the manhacks or zombies, they will add up a lot.
Here you go:

Syntax: Select all

# ../silent_hill/silent_hill.py (npc edition)

# Python
import random

# Source.Python
from colors import Color
from commands.server import ServerCommand
from core import PLATFORM
from cvars import ConVar
from engines.precache import Model
from engines.server import server, server_game_dll
from engines.sound import Sound
from entities.constants import RenderMode, RenderEffects
from entities.entity import BaseEntity, Entity
from entities.helpers import index_from_edict, pointer_from_edict
from events import Event
from listeners import OnEntityDeleted, OnLevelInit, OnLevelEnd
from listeners.tick import Delay, Repeat
from mathlib import NULL_VECTOR, Vector
from memory import (Convention, DataType, find_binary, get_virtual_function,
NULL)
from memory.hooks import PreHook
from players import PlayerGenerator
from players.entity import Player
from stringtables import string_tables


# How long until the apocalypse starts again (in seconds)?
INTERVALS = (30, 45, 60, 90)
# Seconds until the thick Silent Hill-like fog starts fading away.
FOG_FADE_DELAY = 60
# How long should the fading of the fog take (in seconds)?
FOG_FADE_TIME = 60

# Should NPCs spawn during the thick fog phase? (True/False)
NPCS_ENABLED = True
# Maximum number of NPCs at any given time.
NPCS_MAX = 5

# Apply a glowing green effect that can be seen through the fog when the
# manhacks spawn? (True/False)
MANHACKS_SPAWN_EFFECT = False
# How much health should manhacks spawn with?
MANHACKS_HEALTH = 100
# How much damage should they deal?
MANHACKS_DAMAGE = 25

# How much health should zombies spawn with?
ZOMBIE_HEALTH = 120
# How much damage should they deal?
ZOMBIE_DAMAGE = 30


FOG_COLOR = Color(185, 185, 185)
FLASH_COLOR = Color(255, 0, 0, 150)
FLASH_COLOR_END = Color(255, 0, 0, 255)


SIREN_SOUND = Sound('ambient/alarms/citadel_alert_loop2.wav')


# Sprite used for tinting the player's screen.
SCREEN_SPRITE = Model('sprites/white.vmt')
SCREEN_SPRITE_OFFSET = Vector(10, 0, 0)


# Offset for NPC spawn positions.
NPC_ORIGIN_OFFSET = Vector(0, 0, 32)
NPC_SPAWN_VELOCITY = (-500, -250, 250, 500)


# Dictionary used to keep track of 'env_sprite' entities we'll be using.
_black_screens = {}


# =============================================================================
# >> EVENTS AND LISTENERS
# =============================================================================
def load():
"""Called when the plugin gets loaded."""
# Modify the manhack related convars.
Manhack.initialize_settings()
# Modify the zombie related convars.
Zombie.initialize_settings()

# Are there any players on the server?
if server.num_players > 0:
dark_times.initialize()


def unload():
"""Called when the plugin gets unloaded."""
dark_times.stop(pause_init=False)
dark_times.remove_all_npcs()

# Remove any leftover player entities.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).remove_black_screen()


@OnLevelEnd
def on_level_end():
"""Called when the map starts changing."""
dark_times.stop()
# Remove old data (spawn points, npc indexes).
dark_times.clean_up_data()


@Event('round_start')
def round_start(event):
"""Called when a new round starts."""
dark_times.stop()
dark_times.initialize()


@OnLevelInit
def on_level_init(map_name):
"""Called when the new map is done loading."""
dark_times.initialize()


@OnEntityDeleted
def on_entity_deleted(base_entity):
"""Called when an entity gets deleted."""
try:
index = base_entity.index
except ValueError:
# Not a networked entity.
return

try:
# Was this one of our 'env_sprite' entities?
player_index = _black_screens.pop(index)
# Remove the instance reference from the player.
PlayerSH(player_index).black_screen = None
except KeyError:
pass

try:
# Was this an NPC we spawned?
dark_times.npc_indexes.remove(index)
except KeyError:
pass


@PreHook(get_virtual_function(server_game_dll, 'SetServerHibernation'))
def set_server_hibernation_pre(stack_data):
"""Called when the last player leaves, or the first player joins."""
# Did the last player just leave (server is being put into hibernation)?
if stack_data[1]:
dark_times.stop()
# Or did the first player just join?
else:
dark_times.initialize()


# =============================================================================
# >> PLAYER STUFF
# =============================================================================
class PlayerSH(Player):
"""Modified Player class."""

def __init__(self, index, caching=True):
"""Initializes the object."""
super().__init__(index, caching)
self.black_screen = None
self.target_name = f'player_{self.userid}'

@property
def viewmodel(self):
"""Returns the Entity instance of the player's viewmodel."""
return Entity.from_inthandle(self.get_property_int('m_hViewModel'))

def darken_view(self, amount):
"""Lowers the brightness of the player's screen."""
if self.black_screen is None:
self.black_screen = create_sprite(
origin=NULL_VECTOR, scale=30.0, model=SCREEN_SPRITE)

self.black_screen.set_parent(self.viewmodel, -1)
self.black_screen.teleport(SCREEN_SPRITE_OFFSET)

# Create a sprite:player reference for later use.
_black_screens[self.black_screen.index] = self.index

# Change the alpha/transparency of the 'env_sprite'.
self.black_screen.set_network_property_int('m_nBrightness', amount)

def remove_black_screen(self):
"""Removes the 'env_sprite' used for tinting the player's screen."""
try:
self.black_screen.remove()
except AttributeError:
return

self.black_screen = None


# =============================================================================
# >> DARK TIMES
# =============================================================================
class DarkTimes:
"""Class used to start and stop the apocalypse.

Attributes:
current_darkness (int): Level of darkness used for darkening players'
screens.
in_progress (bool): Is the apocalypse currently happening?
npc_indexes (set of int): Contains indexes of NPCs spawned during the
thick fog phase.
flash_think (Repeat): Instance of Repeat() used for looping the
`_flash_think()` function.
darken_think (Repeat): Instance of Repeat() used for looping the
`_darken_think()` function.
gather_data_think (Repeat): Instance of Repeat() used for looping the
`_gather_data_think()` function.
old_fog_values (tuple): Tuple that holds values of the previous fog.
_fog (Entity): Entity instance of the 'env_fog_controller' we'll be
using.
_delays (dict of Delay): Dictionary that holds any Delay() instances
we might be using.
_saved_time (float): Remaining time from the previous initialization.
_valid_npc_origins (list of Vector): List containing spawn positions
for NPCs.
"""

def __init__(self):
"""Initializes the object."""
self.current_darkness = 0
self.in_progress = False
self.npc_indexes = set()

self.flash_think = Repeat(self._flash_think)
self.darken_think = Repeat(self._darken_think)
self.gather_data_think = Repeat(self._gather_data_think)
self.spawn_npcs_think = Repeat(self._spawn_npcs_think)

self.old_fog_values = None
self._fog = None
self._delays = {}
self._saved_time = None
self._valid_npc_origins = []

def initialize(self, instant=False):
"""Starts the apocalypse after a randomly chosen delay."""
# Don't go further if the apocalypse is already happening.
if self.in_progress:
return

try:
self._delays['init'].cancel()
except (KeyError, ValueError):
pass

# Are we trying to instantly start the apocalypse?
if instant:
self.begin()
else:
self._delays['init'] = Delay(
# Resume the time to start from the previous initialization if
# there is one, otherwise pick a random time.
self._saved_time if self._saved_time else random.choice(
INTERVALS), self.begin)

def begin(self):
"""Starts the apocalypse."""
self.in_progress = True
self.current_darkness = 0
self._saved_time = None

try:
self._delays['init'].cancel()
except (KeyError, ValueError):
pass

SIREN_SOUND.play()
# Sync the red flashes with the siren sound - every time the players
# hear the siren, their screen will be flashed red.
self.flash_think.start(interval=6.5, limit=4, execute_on_start=True)
# Start lowering the brightness.
self.darken_think.start(interval=0.5, limit=40, execute_on_start=True)
# Look for valid spawn positions for NPCs during the siren phase.
self.gather_origin_data()

# Change to a thick fog similar to the one from Silent Hill.
# (credit: killer89 - https://gamebanana.com/prefabs/1308 )
self._delays['final_flash'] = Delay(
26, self.change_fog, (FOG_COLOR, FOG_COLOR, 0, 620))
# Start changing the fog back to normal.
self._delays['restoration'] = Delay(
26 + FOG_FADE_DELAY, self.restore_fog_smooth, (FOG_FADE_TIME,))

def stop(self, pause_init=True):
"""Stops the apocalypse and restores everything back to normal."""
SIREN_SOUND.stop()

# Stop the looping functions.
self.flash_think.stop()
self.darken_think.stop()
self.gather_data_think.stop()
self.spawn_npcs_think.stop()

if pause_init:
try:
# If we're stopping the apocalypse before it had a chance to
# begin, save the remaining time - so we can resume it later.
self._saved_time = self._delays['init'].time_remaining
except KeyError:
pass

# Cancel all delays.
for delay in self._delays.values():
try:
delay.cancel()
except ValueError:
continue

# Set the brightness back to normal levels.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).darken_view(0)

self.restore_fog()
self.in_progress = False
self._fog = None

def clean_up_data(self):
"""Removes data that's no longer needed/valid."""
self._valid_npc_origins.clear()
self.npc_indexes.clear()

def remove_all_npcs(self):
"""Removes all currently active NPCs."""
for index in self.npc_indexes.copy():
BaseEntity(index).remove()

def on_completed(self):
"""Called when the fog finally settles back to the default values."""
self.in_progress = False
# Prepare for the next apocalypse.
self.initialize()

def gather_origin_data(self):
"""Starts gathering valid spawn positions for NPCs."""
# Have we gathered a decent amount of spawn positions?
if len(self._valid_npc_origins) >= 32:
return

self.gather_data_think.start(
interval=2, limit=14, execute_on_start=True)

def _gather_data_think(self):
for edict in PlayerGenerator():
player = PlayerSH(index_from_edict(edict))

# Is this player dead?
if player.dead:
return

new_origin = player.origin + NPC_ORIGIN_OFFSET

# Go through previously added spawn positions.
for origin in self._valid_npc_origins:
# Is the new position too close to this one?
if origin.get_distance(new_origin) <= 256:
# No need to keep looking, stop the loop.
break
else:
# The new position is far enough, add it to the list.
self._valid_npc_origins.append(new_origin)

def _flash_think(self):
UTIL_ScreenFadeAll(FLASH_COLOR, 0.5, 0.25, 1)

def _darken_think(self):
# Increase the darkness.
self.current_darkness += 5

# Reduce the brightness for each player on the server.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).darken_view(
self.current_darkness)

def _spawn_npcs_think(self):
# Have we hit the NPC limit?
if len(self.npc_indexes) >= NPCS_MAX:
return

try:
origin = random.choice(self._valid_npc_origins)
except IndexError:
return

# Pick an NPC to spawn (55% for a manhack, 45% for a zombie)
npc_class = random.choices((Manhack, Zombie), weights=(55, 45))[0]
npc = npc_class.create(origin)

# Add the NPC's index to the set.
self.npc_indexes.add(npc.index)

def get_fog_instance(self):
"""Returns an Entity instance of an 'env_fog_controller'."""
if self._fog is not None:
return self._fog

old_fog = Entity.find('env_fog_controller')
# Does an 'env_fog_controller' already exist on this map?
if old_fog:
# Store the old values for later use.
self.old_fog_values = (
old_fog.get_property_color('m_fog.colorPrimary'),
old_fog.get_property_color('m_fog.colorSecondary'),
old_fog.fog_start,
old_fog.fog_end
)

# We'll use that one for our fog shenanigans.
self._fog = old_fog
return self._fog

# Guess we need to make a new one.
new_fog = Entity.create('env_fog_controller')
new_fog.fog_enable = True
new_fog.fog_blend = True
new_fog.fog_max_density = 1.0
new_fog.spawn_flags = 1
new_fog.spawn()

new_fog.target_name = 'silent_hill_fog'
# Fix for maps without fog.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).call_input(
'SetFogController', new_fog.target_name)

self._fog = new_fog
return self._fog

def remove_fog(self):
"""Removes the stored 'env_fog_controller' entity.

Note:
This is only used if we're making our own 'env_fog_controller'.
"""
try:
self._fog.remove()
except AttributeError:
return

self._fog = None

def change_fog(self, color1, color2, start, end, final_flash=True):
"""Changes the fog visuals.

Args:
color1 (Color): Primary color of the fog.
color2 (Color): Secondary color of the fog.
start (float): Distance at which the fog begins.
end (float): Distance at which the fog is at its maximum.
final_flash (bool): Is this the final red flash?
"""
fog = self.get_fog_instance()
fog.set_color(color1)
fog.set_color_secondary(color2)
fog.set_start_dist(start)
fog.set_end_dist(end)

# Is this the final flash?
if final_flash:
# Add a stronger flash to mask the changes in fog and screen
# brightness.
UTIL_ScreenFadeAll(FLASH_COLOR_END, 1, 0.5, 1)

for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).darken_view(0)

# Should we start spawning NPCs?
if NPCS_ENABLED:
# Spawn them only during the thick fog phase.
self.spawn_npcs_think.start(
interval=3, limit=FOG_FADE_DELAY / 3)

def restore_fog(self):
"""Restores the fog back to normal."""
if self._fog is None:
return

# Was the map without fog?
if self.old_fog_values is None:
self.remove_fog()
return

self.change_fog(*self.old_fog_values, False)

def restore_fog_smooth(self, duration):
"""Smoothly restores the fog back to normal over the given duration."""
if self._fog is None:
return

should_remove = False
old_values = self.old_fog_values
# Was the map missing an 'env_fog_controller' entity?
if old_values is None:
should_remove = True
# Just increase the 'start' and 'end' values for the fog before
# removing it - making the transition semi-smooth.
old_values = (FOG_COLOR, FOG_COLOR, 512, 14000)

self._fog.set_color_lerp_to(old_values[0])
self._fog.set_color_secondary_lerp_to(old_values[1])
self._fog.set_start_dist_lerp_to(old_values[2])
self._fog.set_end_dist_lerp_to(old_values[3])
# Add 0.001 to the transition duration to avoid flashes caused by the
# fog bouncing back (engine quirk) before being set in place.
self._fog.fog_lerp_time = duration + 0.001
self._fog.start_fog_transition()

# If we created our own fog, we need to remove it.
if should_remove:
self._delays['ending'] = self._fog.delay(duration, self.remove_fog)
else:
# Make sure the fog values stay put after the transition.
self._delays['ending'] = self._fog.delay(
duration, self.change_fog, (*self.old_fog_values, False))

# The cycle is complete - let's do that again!
self._delays['restart'] = Delay(duration, self.on_completed)


dark_times = DarkTimes()


# =============================================================================
# >> UTIL_SCREENFADEALL - https://git.io/JJXoe
# =============================================================================
server_binary = find_binary('server')


if PLATFORM == 'windows':
identifier_screen = b'\x55\x8B\xEC\xD9\x45\x10\x8D\x45\xF4'
else:
identifier_screen = '_Z18UTIL_ScreenFadeAllRK9color32_sffi'


UTIL_ScreenFadeAll = server_binary[identifier_screen].make_function(
Convention.CDECL,
(DataType.POINTER, DataType.FLOAT, DataType.FLOAT, DataType.INT),
DataType.VOID
)


# =============================================================================
# >> UTIL_GETLOCALPLAYER FIX - (thank you Ayuto)
# viewtopic.php?f=20&t=1907#p12247
# =============================================================================
if PLATFORM == 'windows':
identifier_local = b'\xA1\x2A\x2A\x2A\x2A\x8B\x2A\x2A\x83\x2A\x01\x7E\x03\x33\xC0\xC3'
else:
identifier_local = '_Z19UTIL_GetLocalPlayerv'


UTIL_GetLocalPlayer = server_binary[identifier_local].make_function(
Convention.CDECL,
[],
DataType.POINTER
)


@PreHook(UTIL_GetLocalPlayer)
def get_local_player_pre(stack_data):
"""Called when the engine tries to get the local Player object.

This function was designed for single-player and should NOT be called in
multi-player games unless you want the server to crash.
"""
for edict in PlayerGenerator():
try:
return pointer_from_edict(edict)
except ValueError:
pass

return NULL


# =============================================================================
# >> ENV_SPRITE
# =============================================================================
def create_sprite(origin, scale, model):
"""Creates an 'env_sprite' entity.

Args:
origin (Vector): Spawn position of the 'env_sprite'.
scale (float): Size of the sprite (max size: 64.0).
model (Model): Appearance of the sprite.
"""
sprite = Entity.create('env_sprite')
sprite.model = model
sprite.origin = origin
sprite.set_key_value_float('scale', scale)
sprite.set_key_value_bool('disablereceiveshadows', True)
sprite.set_key_value_float('HDRColorScale', 0)
sprite.render_amt = 1
sprite.render_mode = RenderMode.TRANS_COLOR
sprite.set_key_value_string('rendercolor', '0 0 0')
sprite.render_fx = RenderEffects.NONE
sprite.spawn_flags = 1
sprite.spawn()
return sprite


# =============================================================================
# >> NPC_MANHACK
# =============================================================================
class Manhack(Entity):
"""Class used to create and manipulate 'npc_manhack' entities."""
caching = True
effect_name = 'vortigaunt_hand_glow'
settings = {
'health': ConVar('sk_manhack_health'),
'damage': ConVar('sk_manhack_melee_dmg')
}

@staticmethod
def initialize_settings():
"""Changes the convars for 'npc_manhack' health and damage."""
Manhack.settings['health'].set_int(MANHACKS_HEALTH)
Manhack.settings['damage'].set_int(MANHACKS_DAMAGE)

@classmethod
def create(cls, origin):
"""Creates an 'npc_manhack' entity at the specified origin."""
manhack = cls(super().create('npc_manhack').index)
manhack.origin = origin
# Make the manhack fully transparent.
manhack.render_amt = 1
# Slowly fade in the manhack.
manhack.set_key_value_int('renderfx', 7)
# Set a couple of spawn flags.
# 2: Gag (No IDLE sounds until angry)
# 256: Long Visibility/Shoot
manhack.spawn_flags = 2 + 256
manhack.spawn()

if MANHACKS_SPAWN_EFFECT:
manhack.create_spawn_particle()

# Push the manhack in a random direction.
manhack.set_property_vector(
'm_vForceVelocity', Vector(
random.choice(NPC_SPAWN_VELOCITY),
random.choice(NPC_SPAWN_VELOCITY),
500
))

# Make the manhack hate the players.
manhack.call_input('SetRelationship', 'player D_HT 99')
return manhack

def create_spawn_particle(self):
"""Creates and parents an 'info_particle_system' to the manhack."""
particle = Entity.create('info_particle_system')
particle.origin = self.origin
particle.effect_name = Manhack.effect_name
particle.effect_index = string_tables.ParticleEffectNames.add_string(
Manhack.effect_name)
particle.start()

# Parent the particle to the 'npc_manhack'.
particle.set_parent(self)
# Remove the particle after a second.
particle.delay(1, particle.remove)


# =============================================================================
# >> NPC_ZOMBIE
# =============================================================================
class Zombie(Entity):
"""Class used to create 'npc_zombie' entities."""
caching = True
settings = {
'health': ConVar('sk_zombie_health'),
'damage': ConVar('sk_zombie_dmg_one_slash')
}

@staticmethod
def initialize_settings():
"""Changes the convars for 'npc_manhack' health and damage."""
Zombie.settings['health'].set_int(ZOMBIE_HEALTH)
Zombie.settings['damage'].set_int(ZOMBIE_DAMAGE)

@classmethod
def create(cls, origin):
"""Creates an 'npc_zombie' at the specified origin."""
zombie = cls(super().create('npc_zombie').index)
zombie.target_name = f'zombie_{zombie.inthandle}'
zombie.origin = origin
zombie.render_amt = 1
zombie.set_key_value_int('renderfx', 7)
zombie.spawn_flags = 2 + 256
zombie.spawn()
zombie.call_input('SetRelationship', 'player D_HT 99')
# Start going towards a random player after a short delay.
# We need to do this so the zombie doesn't just stand still once it
# spawns. This way it won't block the spawn point.
zombie.delay(0.5, zombie.seek_random_player)
return zombie

def seek_random_player(self):
"""Starts heading towards a randomly selected player."""
targets = []
for edict in PlayerGenerator():
targets.append(PlayerSH(index_from_edict(edict)).target_name)

# We need an 'aiscripted_schedule' entity to make the zombie do things.
schedule = Entity.create('aiscripted_schedule')
# Set which entity we'd like to command.
schedule.set_key_value_string('m_iszEntity', self.target_name)
# Set their current state to 'Idle'.
# (0: None, 1: Idle, 2: Alert, 3: Combat)
schedule.set_key_value_int('forcestate', 1)
# Make the zombie set the target as their enemy and start going towards
# them. Here are all the options for this keyvalue:
# 0: <None>
# 1: Walk to Goal Entity
# 2: Run to Goal Entity
# 3: Set enemy to Goal Entity
# 4: Walk Goal Path
# 5: Run Goal Path
# 6: Set enemy to Goal Entity AND Run to Goal Entity
schedule.set_key_value_int('schedule', 6)
# In case the zombie takes damage, stop the schedule.
# (0: General, 1: Damage or death, 2: Death)
schedule.set_key_value_int('interruptability', 1)
# Pick a random player to be the enemy.
schedule.set_key_value_string('goalent', random.choice(targets))
schedule.spawn()
schedule.call_input('StartSchedule')
schedule.delay(5, schedule.remove)


# =============================================================================
# >> SERVER COMMANDS
# =============================================================================
@ServerCommand('force_dark_times')
def force_dark_times_cmd(command):
dark_times.initialize(instant=True)
You can modify the maximum number of active/alive NPCs with the NPCS_MAX setting, I've set it to 5 for you. I have also included a quick fix for the error you just posted. The error indicates that no spawn points were generated for the NPCs. What were you doing when it occured? Were you in spectate?

Side note: Painkiller and I figured out that the SetServerHibernation hook was preventing the plugin from working on Linux (at least on his server). The plugin was loading without any errors but basically anything below the function for the hook wasn't loaded. :confused: I'm really curious as to how that works.


Yes,,i was in game,,waited for the silent hill to start,it started and worked,,and i even waited for the 2nd one to start and spawned the bots,,after the bots spawned and the 2nd silent hill started, i went again and looked at the log for python and it showed that error i showed ya.
Last edited by daren adler on Mon Sep 07, 2020 9:05 pm, edited 1 time in total.
User avatar
VinciT
Senior Member
Posts: 331
Joined: Thu Dec 18, 2014 2:41 am

Re: [HL2:DM] Little Silent Hill

Postby VinciT » Mon Sep 07, 2020 9:04 pm

I believe I know what the issue was:

Syntax: Select all

def _gather_data_think(self):
for edict in PlayerGenerator():
player = PlayerSH(index_from_edict(edict))

# Is this player dead?
if player.dead:
return

new_origin = player.origin + NPC_ORIGIN_OFFSET

# Go through previously added spawn positions.
for origin in self._valid_npc_origins:
# Is the new position too close to this one?
if origin.get_distance(new_origin) <= 256:
# No need to keep looking, stop the loop.
break
else:
# The new position is far enough, add it to the list.
self._valid_npc_origins.append(new_origin)
I used return instead of continue in this loop when checking if the player was dead - which would stop the loop even if there were more players to check. Basically it never gathered any data for the spawn points. I've updated the code in the previous post to correct that.
ImageImageImageImageImage
User avatar
daren adler
Senior Member
Posts: 328
Joined: Sat May 18, 2019 7:42 pm

Re: [HL2:DM] Little Silent Hill

Postby daren adler » Mon Sep 07, 2020 9:08 pm

VinciT wrote:I believe I know what the issue was:

Syntax: Select all

def _gather_data_think(self):
for edict in PlayerGenerator():
player = PlayerSH(index_from_edict(edict))

# Is this player dead?
if player.dead:
return

new_origin = player.origin + NPC_ORIGIN_OFFSET

# Go through previously added spawn positions.
for origin in self._valid_npc_origins:
# Is the new position too close to this one?
if origin.get_distance(new_origin) <= 256:
# No need to keep looking, stop the loop.
break
else:
# The new position is far enough, add it to the list.
self._valid_npc_origins.append(new_origin)
I used return instead of continue in this loop when checking if the player was dead - which would stop the loop even if there were more players to check. Basically it never gathered any data for the spawn points. I've updated the code in the previous post to correct that.


OK, i will give it a try. Thank you.
User avatar
daren adler
Senior Member
Posts: 328
Joined: Sat May 18, 2019 7:42 pm

Re: [HL2:DM] Little Silent Hill

Postby daren adler » Mon Sep 07, 2020 9:31 pm

daren adler wrote:
VinciT wrote:I believe I know what the issue was:

Syntax: Select all

def _gather_data_think(self):
for edict in PlayerGenerator():
player = PlayerSH(index_from_edict(edict))

# Is this player dead?
if player.dead:
return

new_origin = player.origin + NPC_ORIGIN_OFFSET

# Go through previously added spawn positions.
for origin in self._valid_npc_origins:
# Is the new position too close to this one?
if origin.get_distance(new_origin) <= 256:
# No need to keep looking, stop the loop.
break
else:
# The new position is far enough, add it to the list.
self._valid_npc_origins.append(new_origin)
I used return instead of continue in this loop when checking if the player was dead - which would stop the loop even if there were more players to check. Basically it never gathered any data for the spawn points. I've updated the code in the previous post to correct that.


OK, i will give it a try. Thank you.

Works great and no errors no more, Very good job,, :cool:
User avatar
L'In20Cible
Project Leader
Posts: 1533
Joined: Sat Jul 14, 2012 9:29 pm
Location: Québec

Re: [HL2:DM] Little Silent Hill

Postby L'In20Cible » Tue Sep 08, 2020 3:07 am

VinciT wrote:Nice! Thanks for the tip.

When you do this:
VinciT wrote:

Syntax: Select all

manhack = cls(super().create('npc_manhack').index)

You are effectively doing:

Syntax: Select all

manhack = Manhack(Manhack(BaseEntity.create('npc_manhack').index).index)

The following:

Syntax: Select all

manhack = super().create('npc_manhack')

Would have the same result because super is super. :tongue:
User avatar
Painkiller
Senior Member
Posts: 725
Joined: Sun Mar 01, 2015 8:09 am
Location: Germany
Contact:

Re: [HL2:DM] Little Silent Hill

Postby Painkiller » Tue Sep 08, 2020 1:25 pm

It works for me so far with the latest update of the script.

force_dark_times, I currently have to enter every time I want a Silent Hill wave to appear.

Suggestions for improvements to not burden the server performance:

-After the wave is over, all the dead bodies should disappear again.
-Various waves (different NPC)

Further: A list of NPCs you could use (with a function to turn off and on the NPC you need)


Translated with http://www.DeepL.com/Translator (free version)

Code: Select all

//----------------Zombie CFG------------------------------
//zombie
sk_zombie_health "100"
sk_zombie_dmg_one_slash "25"
sk_zombie_dmg_both_slash "35"
sk_zombie_poison_health "100"
sk_zombie_poison_dmg_spit "35"
ent_fire npc_fastzombie sethealth 100
ent_fire npc_fastzombie ondeath destroy
ent_fire npc_poisonzombie ondeath destroy
ent_fire npc_zombie ondeath destroy
ent_fire npc_zombie_poison ondeath destroy
//Vortigaunt
sk_vortigaunt_armor_charge "30"
sk_vortigaunt_armor_charge_per_token "5"
sk_vortigaunt_health "100"
sk_vortigaunt_dmg_claw "25"
sk_vortigaunt_dmg_rake "0"
sk_vortigaunt_dmg_zap "25"
sk_vortigaunt_zap_range "25"
sk_vortigaunt_vital_antlion_worker_dmg "0.2"
ent_fire npc_vortiguant ondeath destroy
//Manhack
sk_manhack_health "5"
sk_manhack_melee_dmg "30"
sk_manhack_v2 "1"
//Icthyosaur
sk_ichthyosaur_health "50"
sk_ichthyosaur_melee_dmg "20"
//Headcrab
sk_headcrab_health "20"
sk_headcrab_fast_health "20"
sk_headcrab_poison_health "20"
sk_headcrab_melee_dmg "5"
sk_headcrab_poison_npc_damage "10"
sk_headcrab_poison_health "25
ent_fire npc_headcrab ondeath destroy
ent_fire npc_headcrab_poison ondeath destroy
ent_fire npc_headcrab_black setdamage 10
ent_fire npc_headcrab_poison setdamage 10
ent_fire npc_headcrab_black ondeath destroy
SetRelationship npc_headcrab_black player d_ht 99
//Antlion
sk_antlion_health "100"
sk_antlion_swipe_damage "20"
sk_antlion_jump_damage "25"
sk_antlion_air_attack_dmg "25"
ent_fire npc_antlion ondeath destroy
//Antlionguard
sk_antlionguard_dmg_charge "75"
sk_antlionguard_dmg_shove "50"
sk_antlionguard_health "200"
ent_fire npc_antlionguard ondeath destroy
// strider
sk_strider_health  "300"
sk_strider_num_missiles1 "5"
sk_strider_num_missiles2 "7"
sk_strider_num_missiles3 "7"
// Metropolice
sk_metropolice_health "40"
sk_metropolice_stitch_reaction "1.0"
sk_metropolice_stitch_tight_hitcount "2"
sk_metropolice_stitch_at_hitcount "1"
sk_metropolice_stitch_behind_hitcount "3"
sk_metropolice_stitch_along_hitcount "2"
// Rollermine
sk_rollermine_shock  "10"
sk_rollermine_stun_delay "3"
sk_rollermine_vehicle_intercept "1"
// Scanner (City)
sk_scanner_health  "30"
sk_scanner_dmg_dive  "25"
// Stalker
sk_stalker_health  "100"
sk_stalker_melee_dmg "5"
// Combine Gunship
sk_gunship_burst_size  "15"
sk_gunship_health_increments "5"
sk_npc_dmg_gunship  "40"
sk_npc_dmg_gunship_to_plr "3"
// Combine Helicopter
sk_npc_dmg_helicopter   "6"
sk_npc_dmg_helicopter_to_plr "3"
sk_helicopter_grenadedamage  "30"
sk_helicopter_grenaderadius  "275"
sk_helicopter_grenadeforce  "55000"
// Combine Dropship
sk_npc_dmg_dropship  "2"
// Combine APC
sk_apc_health    "750"

// NPC damage adjusters
sk_npc_head  "75"
sk_npc_chest "30"
sk_npc_stomach "20"
sk_npc_arm  "10"
sk_npc_leg  "10"

sk_ally_regen_time "0.2"
// Jeep
sk_max_gauss_round "30"
 
ai_enable_fear_behavior "1"
// zombine
sk_zombie_soldier_health "100"
// Episodic APC - Experimental
sk_apc_missile_damage "50"
// Antlion air attack
sk_antlion_air_attack_dmg  "10"
// Antlion worker
sk_antlion_worker_spit_speed "600"
sk_antlion_worker_health "60"
// Vortigaunt charge limit
sk_vortigaunt_armor_charge  "15"
sk_vortigaunt_armor_charge_per_token "5"
sk_vortigaunt_dmg_zap   "25"
// Poison headcrab
sk_headcrab_poison_npc_damage "20.0"
// advisor
//sk_advisor_health "500"

// Custom ConVar in this extension
sk_npc_turret_floor_bullet_damage 5
 
//---------------------End Zombie CFG---------------------------------


To the fast_zombie I think you could create a vpk file that could bring back all the contents of the fast_zombie.
- models
- materials
- sounds
But I don't know if this is feasible for you "Vincit".
https://steamcommunity.com/sharedfiles/ ... =354131748


Otherwise I have to praise you for having managed to let the zombies spawn randomly at different locations without having to create coordinates or something like that.

After some test runs I found this error:

Code: Select all

[SP] Caught an Exception:
Traceback (most recent call last):
  File "../addons/source-python/plugins/silenthill/silenthill.py", line 136, in on_entity_deleted
    PlayerSH(player_index).black_screen = None
  File "../addons/source-python/packages/source-python/entities/_base.py", line 132, in __call__
    obj = super().__call__(index)
  File "../addons/source-python/plugins/silenthill/silenthill.py", line 166, in __init__
    super().__init__(index, caching)
  File "../addons/source-python/packages/source-python/players/_base.py", line 92, in __init__
    PlayerMixin.__init__(self, index)

ValueError: Conversion from "Index" (1) to "BaseEntity" failed.
User avatar
VinciT
Senior Member
Posts: 331
Joined: Thu Dec 18, 2014 2:41 am

Re: [HL2:DM] Little Silent Hill

Postby VinciT » Wed Sep 09, 2020 12:26 am

L'In20Cible wrote:

Syntax: Select all

manhack = super().create('npc_manhack')

Would have the same result because super is super. :tongue:
Whoops! I thought it would create a BaseEntity and not a Manhack instance. While I have you here L'In20Cible, any ideas as to why SetServerHibernation doesn't work on Linux?

Painkiller wrote:It works for me so far with the latest update of the script.
force_dark_times, I currently have to enter every time I want a Silent Hill wave to appear.
I'll fix the hibernation issue as soon as I have more time. You should only have to use the force_dark_times command once per server restart to kickstart the process. After the initial apocalypse, everything should be working properly.

I'll see what I can do about the features you've requested (more npcs, ragdoll cleanup, adjustable duration for the siren phase, and more).

Painkiller wrote:Otherwise I have to praise you for having managed to let the zombies spawn randomly at different locations without having to create coordinates or something like that.
Thanks! I feel like there's still room for improvement for the spawn system.

Painkiller wrote:After some test runs I found this error:

Code: Select all
[SP] Caught an Exception:
Traceback (most recent call last):
File "../addons/source-python/plugins/silenthill/silenthill.py", line 136, in on_entity_deleted
PlayerSH(player_index).black_screen = None
File "../addons/source-python/packages/source-python/entities/_base.py", line 132, in __call__
obj = super().__call__(index)
File "../addons/source-python/plugins/silenthill/silenthill.py", line 166, in __init__
super().__init__(index, caching)
File "../addons/source-python/packages/source-python/players/_base.py", line 92, in __init__
PlayerMixin.__init__(self, index)

ValueError: Conversion from "Index" (1) to "BaseEntity" failed.

Fixed the error you received and the got rid of the unnecessary super() stuff in this post. :tongue:
ImageImageImageImageImage
User avatar
daren adler
Senior Member
Posts: 328
Joined: Sat May 18, 2019 7:42 pm

Re: [HL2:DM] Little Silent Hill

Postby daren adler » Wed Sep 09, 2020 2:29 am

VinciT wrote:
L'In20Cible wrote:

Syntax: Select all

manhack = super().create('npc_manhack')

Would have the same result because super is super. :tongue:
Whoops! I thought it would create a BaseEntity and not a Manhack instance. While I have you here L'In20Cible, any ideas as to why SetServerHibernation doesn't work on Linux?

Painkiller wrote:It works for me so far with the latest update of the script.
force_dark_times, I currently have to enter every time I want a Silent Hill wave to appear.
I'll fix the hibernation issue as soon as I have more time. You should only have to use the force_dark_times command once per server restart to kickstart the process. After the initial apocalypse, everything should be working properly.

I'll see what I can do about the features you've requested (more npcs, ragdoll cleanup, adjustable duration for the siren phase, and more).

Painkiller wrote:Otherwise I have to praise you for having managed to let the zombies spawn randomly at different locations without having to create coordinates or something like that.
Thanks! I feel like there's still room for improvement for the spawn system.

Painkiller wrote:After some test runs I found this error:

Code: Select all
[SP] Caught an Exception:
Traceback (most recent call last):
File "../addons/source-python/plugins/silenthill/silenthill.py", line 136, in on_entity_deleted
PlayerSH(player_index).black_screen = None
File "../addons/source-python/packages/source-python/entities/_base.py", line 132, in __call__
obj = super().__call__(index)
File "../addons/source-python/plugins/silenthill/silenthill.py", line 166, in __init__
super().__init__(index, caching)
File "../addons/source-python/packages/source-python/players/_base.py", line 92, in __init__
PlayerMixin.__init__(self, index)

ValueError: Conversion from "Index" (1) to "BaseEntity" failed.

Fixed the error you received and the got rid of the unnecessary super() stuff in this post. :tongue:


Sounds like you guys are getting it worked out for PainKiller. :smile: Yes the removel of the dead npc would be nice, GREAT WORK, keeps getting better and better. VERY NICE WORK!!! :grin: :grin:
Last edited by daren adler on Wed Sep 09, 2020 4:17 am, edited 1 time in total.
User avatar
L'In20Cible
Project Leader
Posts: 1533
Joined: Sat Jul 14, 2012 9:29 pm
Location: Québec

Re: [HL2:DM] Little Silent Hill

Postby L'In20Cible » Wed Sep 09, 2020 3:48 am

VinciT wrote:While I have you here L'In20Cible, any ideas as to why SetServerHibernation doesn't work on Linux?

Maybe hibernation is disabled on their servers? Other than that; no idea.
User avatar
Painkiller
Senior Member
Posts: 725
Joined: Sun Mar 01, 2015 8:09 am
Location: Germany
Contact:

Re: [HL2:DM] Little Silent Hill

Postby Painkiller » Wed Sep 09, 2020 8:33 am

VinciT wrote:
L'In20Cible wrote:

Syntax: Select all

manhack = super().create('npc_manhack')

Would have the same result because super is super. :tongue:
Whoops! I thought it would create a BaseEntity and not a Manhack instance. While I have you here L'In20Cible, any ideas as to why SetServerHibernation doesn't work on Linux?

Painkiller wrote:It works for me so far with the latest update of the script.
force_dark_times, I currently have to enter every time I want a Silent Hill wave to appear.
I'll fix the hibernation issue as soon as I have more time. You should only have to use the force_dark_times command once per server restart to kickstart the process. After the initial apocalypse, everything should be working properly.

I'll see what I can do about the features you've requested (more npcs, ragdoll cleanup, adjustable duration for the siren phase, and more).

Painkiller wrote:Otherwise I have to praise you for having managed to let the zombies spawn randomly at different locations without having to create coordinates or something like that.
Thanks! I feel like there's still room for improvement for the spawn system.

Painkiller wrote:After some test runs I found this error:

Code: Select all
[SP] Caught an Exception:
Traceback (most recent call last):
File "../addons/source-python/plugins/silenthill/silenthill.py", line 136, in on_entity_deleted
PlayerSH(player_index).black_screen = None
File "../addons/source-python/packages/source-python/entities/_base.py", line 132, in __call__
obj = super().__call__(index)
File "../addons/source-python/plugins/silenthill/silenthill.py", line 166, in __init__
super().__init__(index, caching)
File "../addons/source-python/packages/source-python/players/_base.py", line 92, in __init__
PlayerMixin.__init__(self, index)

ValueError: Conversion from "Index" (1) to "BaseEntity" failed.

Fixed the error you received and the got rid of the unnecessary super() stuff in this post. :tongue:


Unfortunately I have to enter force_dark_times every time.

Once after server restart does not work.
User avatar
VinciT
Senior Member
Posts: 331
Joined: Thu Dec 18, 2014 2:41 am

Re: [HL2:DM] Little Silent Hill

Postby VinciT » Fri Sep 11, 2020 12:29 am

Here you go guys:

Syntax: Select all

# ../silent_hill/silent_hill.py (npc edition)

# Python
import random

# Source.Python
from colors import Color
from commands.server import ServerCommand
from core import PLATFORM, echo_console
from engines.precache import Model
from engines.server import server
from engines.sound import Sound
from entities.constants import RenderMode, RenderEffects
from entities.entity import Entity
from entities.helpers import index_from_edict, pointer_from_edict
from events import Event
from listeners import (OnEntityCreated, OnEntityDeleted, OnLevelInit,
OnLevelEnd, OnClientActive, OnClientDisconnect)
from listeners.tick import Delay, Repeat
from mathlib import NULL_VECTOR, Vector
from memory import Convention, DataType, NULL, find_binary
from memory.hooks import PreHook
from players import PlayerGenerator
from players.entity import Player


# NOTE: This plugin was designed to work with the default settings for
# 'SIREN_SOUND' and 'SIREN_TIME'. Modifying these values might degrade your
# experience.


# Sound that plays during the siren phase - before the fog sets in.
# Default: 'ambient/alarms/citadel_alert_loop2.wav'
SIREN_SOUND = Sound('ambient/alarms/citadel_alert_loop2.wav')
# How long should the siren phase last? (in seconds)
# Default: 26
SIREN_TIME = 26


# How long until the apocalypse starts again? (in seconds)
INTERVALS = (30, 45, 60, 90)
# Seconds until the thick Silent Hill-like fog starts fading away.
FOG_FADE_DELAY = 60
# How long should the fading of the fog take? (in seconds)
FOG_FADE_TIME = 60


# Should NPCs spawn during the thick fog phase? (True/False)
NPCS_ENABLED = True
# Maximum number of NPCs at any given time.
NPCS_MAX = 5
# NPCs that can spawn. Be sure to properly configure the needed convars. These
# should be located in your server.cfg file. (e.g. sk_manhack_health,
# sk_manhack_damage, sk_zombie_health, sk_zombie_dmg_one_slash, and so on)
NPCS = (
'npc_zombie',
'npc_manhack',
'npc_headcrab',
'npc_antlion'
)


FOG_COLOR = Color(185, 185, 185)
FLASH_COLOR = Color(255, 0, 0, 150)
FLASH_COLOR_END = Color(255, 0, 0, 255)


# Sprite used for tinting the player's screen.
SCREEN_SPRITE = Model('sprites/white.vmt')
SCREEN_SPRITE_OFFSET = Vector(10, 0, 0)


# Offset for NPC spawn positions.
NPC_ORIGIN_OFFSET = Vector(0, 0, 32)


# Dictionary used to keep track of 'env_sprite' entities we'll be using.
_black_screens = {}


# =============================================================================
# >> EVENTS AND LISTENERS
# =============================================================================
def load():
"""Called when the plugin gets loaded."""
# Are there any players on the server?
if server.num_players > 0:
dark_times.initialize()


def unload():
"""Called when the plugin gets unloaded."""
dark_times.stop(pause_init=False)
dark_times.remove_all_npcs()

# Remove any leftover player entities.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).remove_black_screen()


@OnLevelEnd
def on_level_end():
"""Called when the map starts changing."""
dark_times.stop()
# Remove old data (spawn points, npc indexes).
dark_times.clean_up_data()


@Event('round_start')
def round_start(event):
"""Called when a new round starts."""
dark_times.stop()
dark_times.initialize()


@OnLevelInit
def on_level_init(map_name):
"""Called when the new map is done loading."""
dark_times.initialize()


@OnClientActive
def on_client_active(index):
"""Called when a player fully connects to the server."""
# Is this the first player to join the server? (server was empty)
if server.num_players == 1:
dark_times.initialize()


@OnClientDisconnect
def on_client_disconnect(index):
"""Called when a player leaves the server."""
# Delay the call by a single frame - otherwise we might get incorrect info.
Delay(0, _on_client_disconnect)


def _on_client_disconnect():
# Did the last player just leave the server? (server is now empty)
if server.num_players == 0:
dark_times.stop()


@OnEntityCreated
def on_entity_created(base_entity):
"""Called when an entity gets created/spawned."""
try:
index = base_entity.index
except ValueError:
# Not a networked entity.
return

# Did a headcrab just spawn?
if 'npc_headcrab' in base_entity.classname:
npc = NPC(index)
npc.delay(0, npc.check_headcrab)


@OnEntityDeleted
def on_entity_deleted(base_entity):
"""Called when an entity gets deleted."""
try:
index = base_entity.index
except ValueError:
return

try:
# Was this one of our 'env_sprite' entities?
player_index = _black_screens.pop(index)
# Remove the instance reference from the player.
PlayerSH(player_index).black_screen = None
except (KeyError, ValueError):
pass

try:
# Was this an NPC we spawned?
dark_times.npc_indexes.remove(index)
except KeyError:
pass


@Event('entity_killed')
def npc_killed(event):
"""Called when an entity gets killed."""
index = event['entindex_killed']
# Is this one of our NPCs?
if index in dark_times.npc_indexes:
NPC(index).dissolve()


# =============================================================================
# >> PLAYER STUFF
# =============================================================================
class PlayerSH(Player):
"""Modified Player class."""

def __init__(self, index, caching=True):
"""Initializes the object."""
super().__init__(index, caching)
self.black_screen = None
self.target_name = f'player_{self.userid}'

@property
def viewmodel(self):
"""Returns the Entity instance of the player's viewmodel."""
return Entity.from_inthandle(self.get_property_int('m_hViewModel'))

def darken_view(self, amount):
"""Lowers the brightness of the player's screen."""
if self.black_screen is None:
self.black_screen = create_sprite(
origin=NULL_VECTOR, scale=30.0, model=SCREEN_SPRITE)

self.black_screen.set_parent(self.viewmodel, -1)
self.black_screen.teleport(SCREEN_SPRITE_OFFSET)

# Create a sprite:player reference for later use.
_black_screens[self.black_screen.index] = self.index

# Change the alpha/transparency of the 'env_sprite'.
self.black_screen.set_network_property_int('m_nBrightness', amount)

def remove_black_screen(self):
"""Removes the 'env_sprite' used for tinting the player's screen."""
try:
self.black_screen.remove()
except AttributeError:
return

self.black_screen = None


# =============================================================================
# >> DARK TIMES
# =============================================================================
class DarkTimes:
"""Class used to start and stop the apocalypse.

Attributes:
current_darkness (int): Level of darkness used for darkening players'
screens.
in_progress (bool): Is the apocalypse currently happening?
npc_indexes (set of int): Contains indexes of NPCs spawned during the
thick fog phase.
flash_think (Repeat): Instance of Repeat() used for looping the
`_flash_think()` function.
darken_think (Repeat): Instance of Repeat() used for looping the
`_darken_think()` function.
gather_data_think (Repeat): Instance of Repeat() used for looping the
`_gather_data_think()` function.
old_fog_values (tuple): Tuple that holds values of the previous fog.
_fog (Entity): Entity instance of the 'env_fog_controller' we'll be
using.
_delays (dict of Delay): Dictionary that holds any Delay() instances
we might be using.
_saved_time (float): Remaining time from the previous initialization.
_valid_npc_origins (list of Vector): List containing spawn positions
for NPCs.
"""

def __init__(self):
"""Initializes the object."""
self.current_darkness = 0
self.in_progress = False
self.npc_indexes = set()

self.flash_think = Repeat(self._flash_think)
self.darken_think = Repeat(self._darken_think)
self.gather_data_think = Repeat(self._gather_data_think)
self.spawn_npcs_think = Repeat(self._spawn_npcs_think)

self.old_fog_values = None
self._fog = None
self._delays = {}
self._saved_time = None
self._valid_npc_origins = []

def initialize(self, instant=False):
"""Starts the apocalypse after a randomly chosen delay."""
# Don't go further if the apocalypse is already happening.
if self.in_progress:
return

try:
self._delays['init'].cancel()
except (KeyError, ValueError):
pass

# Are we trying to instantly start the apocalypse?
if instant:
self.begin()
else:
self._delays['init'] = Delay(
# Resume the time to start from the previous initialization if
# there is one, otherwise pick a random time.
self._saved_time if self._saved_time else random.choice(
INTERVALS), self.begin)

def begin(self):
"""Starts the apocalypse."""
self.in_progress = True
self.current_darkness = 0
self._saved_time = None

try:
self._delays['init'].cancel()
except (KeyError, ValueError):
pass

SIREN_SOUND.play()
# Sync the red flashes with the siren sound - every time the players
# hear the siren, their screen will be flashed red.
# NOTE: This was synced with the default 'SIREN_SOUND'.
self.flash_think.start(
interval=6.5, limit=SIREN_TIME / 6.5, execute_on_start=True)
# Start lowering the brightness.
self.darken_think.start(interval=0.5, limit=40, execute_on_start=True)
# Look for valid spawn positions for NPCs during the siren phase.
self.gather_origin_data()

# Change to a thick fog similar to the one from Silent Hill.
# (credit: killer89 - https://gamebanana.com/prefabs/1308 )
self._delays['final_flash'] = Delay(
SIREN_TIME, self.change_fog, (FOG_COLOR, FOG_COLOR, 0, 620))
# Start changing the fog back to normal.
self._delays['restoration'] = Delay(
SIREN_TIME + FOG_FADE_DELAY, self.restore_fog_smooth, (
FOG_FADE_TIME,))

def stop(self, pause_init=True):
"""Stops the apocalypse and restores everything back to normal."""
SIREN_SOUND.stop()

# Stop the looping functions.
self.flash_think.stop()
self.darken_think.stop()
self.gather_data_think.stop()
self.spawn_npcs_think.stop()

if pause_init:
try:
# If we're stopping the apocalypse before it had a chance to
# begin, save the remaining time - so we can resume it later.
self._saved_time = self._delays['init'].time_remaining
except KeyError:
pass

# Cancel all delays.
for delay in self._delays.values():
try:
delay.cancel()
except ValueError:
continue

# Set the brightness back to normal levels.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).darken_view(0)

self.restore_fog()
self.in_progress = False
self._fog = None

def clean_up_data(self):
"""Removes data that's no longer needed/valid."""
self._valid_npc_origins.clear()
self.npc_indexes.clear()

def remove_all_npcs(self):
"""Removes all currently active NPCs."""
for index in self.npc_indexes.copy():
NPC(index).remove()

def on_completed(self):
"""Called when the fog finally settles back to the default values."""
self.in_progress = False
# Prepare for the next apocalypse.
self.initialize()

def gather_origin_data(self):
"""Starts gathering valid spawn positions for NPCs."""
# Have we gathered a decent amount of spawn positions?
if len(self._valid_npc_origins) >= 32:
return

self.gather_data_think.start(
interval=2, limit=14, execute_on_start=True)

def _gather_data_think(self):
for edict in PlayerGenerator():
player = PlayerSH(index_from_edict(edict))

# Is this player dead?
if player.dead:
continue

new_origin = player.origin + NPC_ORIGIN_OFFSET

# Go through previously added spawn positions.
for origin in self._valid_npc_origins:
# Is the new position too close to this one?
if origin.get_distance(new_origin) <= 256:
# No need to keep looking, stop the loop.
break
else:
# The new position is far enough, add it to the list.
self._valid_npc_origins.append(new_origin)

def _flash_think(self):
UTIL_ScreenFadeAll(FLASH_COLOR, 0.5, 0.25, 1)

def _darken_think(self):
# Increase the darkness.
self.current_darkness += 5

# Reduce the brightness for each player on the server.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).darken_view(
self.current_darkness)

def _spawn_npcs_think(self):
# Have we hit the NPC limit?
if len(self.npc_indexes) >= NPCS_MAX:
return

try:
origin = random.choice(self._valid_npc_origins)
except IndexError:
# Missing data for NPC spawn points.
return

# Pick an NPC to spawn.
npc = NPC.create(npc_name=random.choice(NPCS), origin=origin)

try:
# Try to add the NPC's index to the set.
self.npc_indexes.add(npc.index)
except AttributeError:
# The NPC wasn't properly created - skipping this wave.
pass

def get_fog_instance(self):
"""Returns an Entity instance of an 'env_fog_controller'."""
if self._fog is not None:
return self._fog

old_fog = Entity.find('env_fog_controller')
# Does an 'env_fog_controller' already exist on this map?
if old_fog:
# Store the old values for later use.
self.old_fog_values = (
old_fog.get_property_color('m_fog.colorPrimary'),
old_fog.get_property_color('m_fog.colorSecondary'),
old_fog.fog_start,
old_fog.fog_end
)

# We'll use that one for our fog shenanigans.
self._fog = old_fog
return self._fog

# Guess we need to make a new one.
new_fog = Entity.create('env_fog_controller')
new_fog.fog_enable = True
new_fog.fog_blend = True
new_fog.fog_max_density = 1.0
new_fog.spawn_flags = 1
new_fog.spawn()

new_fog.target_name = 'silent_hill_fog'
# Fix for maps without fog.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).call_input(
'SetFogController', new_fog.target_name)

self._fog = new_fog
return self._fog

def remove_fog(self):
"""Removes the stored 'env_fog_controller' entity.

Note:
This is only used if we're making our own 'env_fog_controller'.
"""
try:
self._fog.remove()
except AttributeError:
return

self._fog = None

def change_fog(self, color1, color2, start, end, final_flash=True):
"""Changes the fog visuals.

Args:
color1 (Color): Primary color of the fog.
color2 (Color): Secondary color of the fog.
start (float): Distance at which the fog begins.
end (float): Distance at which the fog is at its maximum.
final_flash (bool): Is this the final red flash?
"""
fog = self.get_fog_instance()
fog.set_color(color1)
fog.set_color_secondary(color2)
fog.set_start_dist(start)
fog.set_end_dist(end)

# Is this the final flash?
if final_flash:
# Add a stronger flash to mask the changes in fog and screen
# brightness.
UTIL_ScreenFadeAll(FLASH_COLOR_END, 1, 0.5, 1)

for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).darken_view(0)

# Should we start spawning NPCs?
if NPCS_ENABLED:
# Spawn them only during the thick fog phase.
self.spawn_npcs_think.start(
interval=3, limit=FOG_FADE_DELAY / 3)

def restore_fog(self):
"""Restores the fog back to normal."""
if self._fog is None:
return

# Was the map without fog?
if self.old_fog_values is None:
self.remove_fog()
return

self.change_fog(*self.old_fog_values, False)

def restore_fog_smooth(self, duration):
"""Smoothly restores the fog back to normal over the given duration."""
if self._fog is None:
return

should_remove = False
old_values = self.old_fog_values
# Was the map missing an 'env_fog_controller' entity?
if old_values is None:
should_remove = True
# Just increase the 'start' and 'end' values for the fog before
# removing it - making the transition semi-smooth.
old_values = (FOG_COLOR, FOG_COLOR, 512, 14000)

self._fog.set_color_lerp_to(old_values[0])
self._fog.set_color_secondary_lerp_to(old_values[1])
self._fog.set_start_dist_lerp_to(old_values[2])
self._fog.set_end_dist_lerp_to(old_values[3])
# Add 0.001 to the transition duration to avoid flashes caused by the
# fog bouncing back (engine quirk) before being set in place.
self._fog.fog_lerp_time = duration + 0.001
self._fog.start_fog_transition()

# If we created our own fog, we need to remove it.
if should_remove:
self._delays['ending'] = self._fog.delay(duration, self.remove_fog)
else:
# Make sure the fog values stay put after the transition.
self._delays['ending'] = self._fog.delay(
duration, self.change_fog, (*self.old_fog_values, False))

# The cycle is complete - let's do that again!
self._delays['restart'] = Delay(duration, self.on_completed)


dark_times = DarkTimes()


# =============================================================================
# >> UTIL_SCREENFADEALL - https://git.io/JJXoe
# =============================================================================
server_binary = find_binary('server')


if PLATFORM == 'windows':
identifier_screen = b'\x55\x8B\xEC\xD9\x45\x10\x8D\x45\xF4'
else:
identifier_screen = '_Z18UTIL_ScreenFadeAllRK9color32_sffi'


UTIL_ScreenFadeAll = server_binary[identifier_screen].make_function(
Convention.CDECL,
(DataType.POINTER, DataType.FLOAT, DataType.FLOAT, DataType.INT),
DataType.VOID
)


# =============================================================================
# >> UTIL_GETLOCALPLAYER FIX - (thank you Ayuto)
# viewtopic.php?f=20&t=1907#p12247
# =============================================================================
if PLATFORM == 'windows':
identifier_local = \
b'\xA1\x2A\x2A\x2A\x2A\x8B\x2A\x2A\x83\x2A\x01\x7E\x03\x33\xC0\xC3'
else:
identifier_local = '_Z19UTIL_GetLocalPlayerv'


UTIL_GetLocalPlayer = server_binary[identifier_local].make_function(
Convention.CDECL,
[],
DataType.POINTER
)


@PreHook(UTIL_GetLocalPlayer)
def get_local_player_pre(stack_data):
"""Called when the engine tries to get the local Player object.

This function was designed for single-player and should NOT be called in
multi-player games unless you want the server to crash.
"""
for edict in PlayerGenerator():
try:
return pointer_from_edict(edict)
except ValueError:
pass

return NULL


# =============================================================================
# >> ENV_SPRITE
# =============================================================================
def create_sprite(origin, scale, model):
"""Creates an 'env_sprite' entity.

Args:
origin (Vector): Spawn position of the 'env_sprite'.
scale (float): Size of the sprite (max size: 64.0).
model (Model): Appearance of the sprite.
"""
sprite = Entity.create('env_sprite')
sprite.model = model
sprite.origin = origin
sprite.set_key_value_float('scale', scale)
sprite.set_key_value_bool('disablereceiveshadows', True)
sprite.set_key_value_float('HDRColorScale', 0)
sprite.render_amt = 1
sprite.render_mode = RenderMode.TRANS_COLOR
sprite.set_key_value_string('rendercolor', '0 0 0')
sprite.render_fx = RenderEffects.NONE
sprite.spawn_flags = 1
sprite.spawn()
return sprite


# =============================================================================
# >> NPC
# =============================================================================
class NPC(Entity):
"""Class used to create and interact with NPC entities."""

def __init__(self, index, caching=True):
"""Initializes the object."""
super().__init__(index, caching)
# Give the NPC a name.
self.target_name = f'sh_npc_{self.inthandle}'

@classmethod
def create(cls, npc_name, origin, seek_after_spawn=True):
"""Creates the NPC entity.

Args:
npc_name (str): Name of the NPC to spawn. (e.g. 'npc_zombie')
origin (Vector): Spawn position of the NPC.
seek_after_spawn (bool): Should the NPC start looking for players
after spawning?
"""
try:
npc = super().create(npc_name)
except ValueError:
# Invalid entity class name.
echo_console(f'ERROR! Attempted to create invalid NPC: {npc_name}')
return

npc.origin = origin
# Make the NPC fully transparent.
npc.render_amt = 1
# Slowly fade it into existence.
npc.set_key_value_int('renderfx', 7)
# Set a couple of spawn flags.
# 2: Gag (No IDLE sounds until angry)
# 256: Long Visibility/Shoot
npc.spawn_flags = 2 + 256
npc.spawn()
npc.become_hostile()

if seek_after_spawn:
# Start going towards a random player after a short delay.
# We need to do this so the NPC doesn't just stand still once it
# spawns. This way it won't block the spawn point.
npc.delay(0.2, npc.seek_random_player)

return npc

def become_hostile(self):
"""Makes the NPC hate the players."""
self.call_input('SetRelationship', 'player D_HT 99')

def seek_random_player(self):
"""Starts heading towards a randomly selected player."""
targets = []
for edict in PlayerGenerator():
targets.append(PlayerSH(index_from_edict(edict)).target_name)

# We need an 'aiscripted_schedule' entity to make the NPC do things.
schedule = Entity.create('aiscripted_schedule')
# Set which entity we'd like to command.
schedule.set_key_value_string('m_iszEntity', self.target_name)
# Set their current state to 'Idle'.
# (0: None, 1: Idle, 2: Alert, 3: Combat)
schedule.set_key_value_int('forcestate', 1)
# Make the NPC set the target as their enemy and start going towards
# them. Here are all the options for this keyvalue:
# 0: <None>
# 1: Walk to Goal Entity
# 2: Run to Goal Entity
# 3: Set enemy to Goal Entity
# 4: Walk Goal Path
# 5: Run Goal Path
# 6: Set enemy to Goal Entity AND Run to Goal Entity
schedule.set_key_value_int('schedule', 6)
# In case the NPC takes damage, stop the schedule.
# (0: General, 1: Damage or death, 2: Death)
schedule.set_key_value_int('interruptability', 1)
# Pick a random player to be the enemy.
schedule.set_key_value_string('goalent', random.choice(targets))
schedule.spawn()
schedule.call_input('StartSchedule')
schedule.delay(5, schedule.remove)

def dissolve(self):
"""Removes the ragdoll of the NPC."""
dissolver = Entity.find_or_create('env_entity_dissolver')
dissolver.dissolve_type = 0
dissolver.dissolve(self.target_name)

def check_headcrab(self):
"""Checks if the headcrab was previously attached to a zombie."""
# Does this headcrab lack an owner? (invalid inthandle)
if self.owner_handle == -1:
# No need to go further.
return

self.become_hostile()
self.delay(0.1, self.seek_random_player)
dark_times.npc_indexes.add(self.index)



# =============================================================================
# >> SERVER COMMANDS
# =============================================================================
@ServerCommand('force_dark_times')
def force_dark_times_cmd(command):
dark_times.initialize(instant=True)
Features added:
  • You can now adjust the duration of the siren phase.
  • Add in any NPC you want.
  • Ragdolls/bodies should get removed as soon as the NPC dies.
  • And last but not least, the plugin should now work properly on Linux.
Before adding NPCs, I suggest you setup the convars for NPC health and damage in your server.cfg file. I believe Painkiller posted a .cfg file earlier in this thread. It had most, if not all of the needed convars.

L'In20Cible wrote:Maybe hibernation is disabled on their servers? Other than that; no idea.
Would that cause anything below the hook not to load within the plugin file? I mean, the ServerCommand at the bottom of the file wasn't registered at all - he wasn't able to run it until we commented out the SetServerHibernation hook. I've switched to OnClientActive and OnClientDisconnect listeners, but I'm still curious how that issue works. :tongue:

Painkiller wrote:To the fast_zombie I think you could create a vpk file that could bring back all the contents of the fast_zombie.
- models
- materials
- sounds
But I don't know if this is feasible for you "Vincit".
https://steamcommunity.com/sharedfiles/ ... =354131748
So I took a look at how this could be done. It appears that the npc_fastzombie.cpp isn't included in the server binaries. Which is why it can't be spawned. The way to fix that is to compile the binaries yourself with the included npc_fastzombie file and use them instead of the default ones. I'm not even sure how clients could connect to a server with modified binaries? :confused:

Either way, I've given up on the fast zombie thing.
Last edited by VinciT on Sat Sep 12, 2020 3:42 am, edited 1 time in total.
ImageImageImageImageImage
User avatar
Painkiller
Senior Member
Posts: 725
Joined: Sun Mar 01, 2015 8:09 am
Location: Germany
Contact:

Re: [HL2:DM] Little Silent Hill

Postby Painkiller » Fri Sep 11, 2020 9:09 am

@Vincit

I tested the new version and it works for the moment.

Many people have already bitten their teeth out with the Fast_Zombie and failed miserably.

(Daren and I use a sourcemod plugin "customguns".
this plugin can implement new weapons in the game.
This plugin uses "hl2mp/scripts/weapon_gauss.txt" which contains the values.

Code: Select all

WeaponData
{
   // Weapon data is loaded by both the Game and Client DLLs.
   "viewmodel"            "models/weapons/v_gauss_suit.mdl"
   "playermodel"         "models/weapons/w_gauss_suit.mdl"
      
   // this prefix determines how player holds the gun in third person and will be used by customguns if this gun is custom
   // more prefixes can be found in weapon scripts files
   "anim_prefix"         "ar2"

   "primary_ammo"         "SMG1"
   "secondary_ammo"      "None"
   "clip_size"            "-1"
   "default_clip"         "100"
   "clip2_size"         "-1"
   "default_clip2"         "-1"
   
   "autoswitchto"         "0"
   "autoswitchfrom"      "0"

   // In order to get custom weapon sounds working, copy game_sounds_weapons.txt (can be found in /hl2/scripts)
   // to /hl2mp/custom/Custom_Weapons/scripts and add a new sound entry, f.e. Weapon_Minigun.Single and then just link it here.
   
   
   SoundData
   {
      "single_shot"                   "Weapon_Gauss.Single"
      "special1"                      "Weapon_Gauss.ChargeLoop"
      "special2"                      "Weapon_Gauss.Charged"
      "special3"                      "Weapon_Gauss.OverCharged"
   }
   
   "CustomGunsPluginData"
   {
      "name"                  "Gauss"
      
      // mdl or vmt for selection menu
      "model"                  "models/weapons/w_gauss_suit.mdl"

      // make the gun usable only by admins who have this flag(s), or -1 to alow everyone to use the gun
      "admin_level"            "-1"
      
      // add this gun to player's inventory on spawn?
      "give_on_spawn"            "1"
      
      // add this gun to player's inventory when he equips this weapon
      "give_with_weapon"         "weapon_smg1"
      
      // binds to this weapon, auto switching when player selects it; if set, both weapons should use the same ammo type and give_with_weapon should be set the same as this!
      "bind_to_weapon"         ""
      
      // if 1, does not disappear from inventory when player drops the physical weapon or is stripped from weapons; recommended for admin weapons or weapons given on spawn
      "persistent"            "0"
      
      // weapon_type - possible values:
      //      bullet - (default) Standard bullet weapon, uses clip sizes and ammo type defined above
      //      throwable - Throws something away from the player - grenades, molotovs, ..
      //      custom - Custom coded weapon
      "weapon_type"      "custom"
      
      "custom_settings"
      {
         // * If set to 1, this custom gun will use game-defined ammo type (set above) and behavior instead of plugin managed ammo.
         // * Fire functions will be managed by game, so they won't be called when the weapon runs out of ammo.
         // * Use with CG_RemovePlayerAmmo() native
         //
         // ** Setting this to 0 will allow you to manage ammo ("m_iClip1" value) yourself via plugin. This overrides ammotype to an unknown value.
         // ** Fire functions will always be called when the weapon is ready to fire, without any ammo checks. Also set this to 0 if your weapon doesn't use ammo.
         // ** Set "primary_ammo" other than "None" to enable HUD ammo display.
         "uses_game_ammo"      "1"
      }
      
      "download"
      {
         "item"   "models/weapons/w_gauss_suit.mdl"
         "item"   "models/weapons/w_gauss_suit.sw.vtx"
         "item"   "models/weapons/w_gauss_suit.dx90.vtx"
         "item"   "models/weapons/w_gauss_suit.dx80.vtx"
         "item"   "models/weapons/w_gauss_suit.vvd"
         "item"   "models/weapons/w_gauss_suit.phy"
         
         "item"   "models/weapons/v_gauss_suit.mdl"
         "item"   "models/weapons/v_gauss_suit.sw.vtx"
         "item"   "models/weapons/v_gauss_suit.dx90.vtx"
         "item"   "models/weapons/v_gauss_suit.dx80.vtx"
         "item"   "models/weapons/v_gauss_suit.vvd"
         
         "item"   "materials/models/weapons/v_gauss/back.vmt"
         "item"   "materials/models/weapons/v_gauss/back.vtf"
         "item"   "materials/models/weapons/v_gauss/capacitor.vmt"
         "item"   "materials/models/weapons/v_gauss/capacitor.vtf"
         "item"   "materials/models/weapons/v_gauss/coils.vmt"
         "item"   "materials/models/weapons/v_gauss/coils.vtf"
         "item"   "materials/models/weapons/v_gauss/details1.vmt"
         "item"   "materials/models/weapons/v_gauss/details1.vtf"
         "item"   "materials/models/weapons/v_gauss/generator.vmt"
         "item"   "materials/models/weapons/v_gauss/generator.vtf"
         "item"   "materials/models/weapons/v_gauss/glowchrome.vmt"
         "item"   "materials/models/weapons/v_gauss/glowchrome.vtf"
         "item"   "materials/models/weapons/v_gauss/spindle.vmt"
         "item"   "materials/models/weapons/v_gauss/spindle.vtf"
         "item"   "materials/models/weapons/v_gauss/supportarm.vmt"
         "item"   "materials/models/weapons/v_gauss/supportarm.vtf"
         
         "item"   "materials/models/weapons/v_gauss/hand.vmt"
         "item"   "materials/models/weapons/v_gauss/hand.vtf"
      }
   }
}


Maybe you could do something similar for the fast_zombie? )

Here is the link to the Sourcemod plugin:https://forums.alliedmods.net/showthread.php?p=2369342

Maybe you can learn something for SP.
Greeting Pain


Edit: By testing for a long time I get out of the server crashing without error messages

Edit2: @Vincit
I found someone who did it and it works.
He told me to edit the server_srv.so.
I wanted to let you know and see if you could help.
https://github.com/ValveSoftware/source ... /mod_hl2mp
User avatar
daren adler
Senior Member
Posts: 328
Joined: Sat May 18, 2019 7:42 pm

Re: [HL2:DM] Little Silent Hill

Postby daren adler » Sat Sep 12, 2020 3:09 am

VinciT wrote:Here you go guys:

Syntax: Select all

# ../silent_hill/silent_hill.py (npc edition)

# Python
import random

# Source.Python
from colors import Color
from commands.server import ServerCommand
from core import PLATFORM
from engines.precache import Model
from engines.server import server
from engines.sound import Sound
from entities.constants import RenderMode, RenderEffects
from entities.entity import Entity
from entities.helpers import index_from_edict, pointer_from_edict
from events import Event
from listeners import (OnEntityCreated, OnEntityDeleted, OnLevelInit,
OnLevelEnd, OnClientActive, OnClientDisconnect)
from listeners.tick import Delay, Repeat
from mathlib import NULL_VECTOR, Vector
from memory import Convention, DataType, find_binary, NULL
from memory.hooks import PreHook
from players import PlayerGenerator
from players.entity import Player


# NOTE: This plugin was designed to work with the default settings for
# 'SIREN_SOUND' and 'SIREN_TIME'. Modifying these values might degrade your
# experience.


# Sound that plays during the siren phase - before the fog sets in.
# Default: 'ambient/alarms/citadel_alert_loop2.wav'
SIREN_SOUND = Sound('ambient/alarms/citadel_alert_loop2.wav')
# How long should the siren phase last? (in seconds)
# Default: 26
SIREN_TIME = 26


# How long until the apocalypse starts again? (in seconds)
INTERVALS = (30, 45, 60, 90)
# Seconds until the thick Silent Hill-like fog starts fading away.
FOG_FADE_DELAY = 60
# How long should the fading of the fog take? (in seconds)
FOG_FADE_TIME = 60


# Should NPCs spawn during the thick fog phase? (True/False)
NPCS_ENABLED = True
# Maximum number of NPCs at any given time.
NPCS_MAX = 5
# NPCs that can spawn. Be sure to properly configure the needed convars. These
# should be located in your server.cfg file. (e.g. sk_manhack_health,
# sk_manhack_damage, sk_zombie_health, sk_zombie_dmg_one_slash, and so on)
NPCS = (
'npc_zombie',
'npc_manhack',
'npc_headcrab',
'npc_antlion'
)


FOG_COLOR = Color(185, 185, 185)
FLASH_COLOR = Color(255, 0, 0, 150)
FLASH_COLOR_END = Color(255, 0, 0, 255)


# Sprite used for tinting the player's screen.
SCREEN_SPRITE = Model('sprites/white.vmt')
SCREEN_SPRITE_OFFSET = Vector(10, 0, 0)


# Offset for NPC spawn positions.
NPC_ORIGIN_OFFSET = Vector(0, 0, 32)


# Dictionary used to keep track of 'env_sprite' entities we'll be using.
_black_screens = {}


# =============================================================================
# >> EVENTS AND LISTENERS
# =============================================================================
def load():
"""Called when the plugin gets loaded."""
# Are there any players on the server?
if server.num_players > 0:
dark_times.initialize()


def unload():
"""Called when the plugin gets unloaded."""
dark_times.stop(pause_init=False)
dark_times.remove_all_npcs()

# Remove any leftover player entities.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).remove_black_screen()


@OnLevelEnd
def on_level_end():
"""Called when the map starts changing."""
dark_times.stop()
# Remove old data (spawn points, npc indexes).
dark_times.clean_up_data()


@Event('round_start')
def round_start(event):
"""Called when a new round starts."""
dark_times.stop()
dark_times.initialize()


@OnLevelInit
def on_level_init(map_name):
"""Called when the new map is done loading."""
dark_times.initialize()


@OnClientActive
def on_client_active(index):
"""Called when a player fully connects to the server."""
# Is this the first player to join the server? (server was empty)
if server.num_players == 1:
dark_times.initialize()


@OnClientDisconnect
def on_client_disconnect(index):
"""Called when a player leaves the server."""
# Delay the call by a single frame - otherwise we might get incorrect info.
Delay(0, _on_client_disconnect)


def _on_client_disconnect():
# Did the last player just leave the server? (server is now empty)
if server.num_players == 0:
dark_times.stop()


@OnEntityCreated
def on_entity_created(base_entity):
"""Called when an entity gets created/spawned."""
try:
index = base_entity.index
except ValueError:
# Not a networked entity.
return

# Did a headcrab just spawn?
if 'npc_headcrab' in base_entity.classname:
npc = NPC(index)
npc.delay(0, npc.check_headcrab)


@OnEntityDeleted
def on_entity_deleted(base_entity):
"""Called when an entity gets deleted."""
try:
index = base_entity.index
except ValueError:
return

try:
# Was this one of our 'env_sprite' entities?
player_index = _black_screens.pop(index)
# Remove the instance reference from the player.
PlayerSH(player_index).black_screen = None
except (KeyError, ValueError):
pass

try:
# Was this an NPC we spawned?
dark_times.npc_indexes.remove(index)
except KeyError:
pass


@Event('entity_killed')
def npc_killed(event):
"""Called when an entity gets killed."""
index = event['entindex_killed']
# Is this one of our NPCs?
if index in dark_times.npc_indexes:
NPC(index).dissolve()


# =============================================================================
# >> PLAYER STUFF
# =============================================================================
class PlayerSH(Player):
"""Modified Player class."""

def __init__(self, index, caching=True):
"""Initializes the object."""
super().__init__(index, caching)
self.black_screen = None
self.target_name = f'player_{self.userid}'

@property
def viewmodel(self):
"""Returns the Entity instance of the player's viewmodel."""
return Entity.from_inthandle(self.get_property_int('m_hViewModel'))

def darken_view(self, amount):
"""Lowers the brightness of the player's screen."""
if self.black_screen is None:
self.black_screen = create_sprite(
origin=NULL_VECTOR, scale=30.0, model=SCREEN_SPRITE)

self.black_screen.set_parent(self.viewmodel, -1)
self.black_screen.teleport(SCREEN_SPRITE_OFFSET)

# Create a sprite:player reference for later use.
_black_screens[self.black_screen.index] = self.index

# Change the alpha/transparency of the 'env_sprite'.
self.black_screen.set_network_property_int('m_nBrightness', amount)

def remove_black_screen(self):
"""Removes the 'env_sprite' used for tinting the player's screen."""
try:
self.black_screen.remove()
except AttributeError:
return

self.black_screen = None


# =============================================================================
# >> DARK TIMES
# =============================================================================
class DarkTimes:
"""Class used to start and stop the apocalypse.

Attributes:
current_darkness (int): Level of darkness used for darkening players'
screens.
in_progress (bool): Is the apocalypse currently happening?
npc_indexes (set of int): Contains indexes of NPCs spawned during the
thick fog phase.
flash_think (Repeat): Instance of Repeat() used for looping the
`_flash_think()` function.
darken_think (Repeat): Instance of Repeat() used for looping the
`_darken_think()` function.
gather_data_think (Repeat): Instance of Repeat() used for looping the
`_gather_data_think()` function.
old_fog_values (tuple): Tuple that holds values of the previous fog.
_fog (Entity): Entity instance of the 'env_fog_controller' we'll be
using.
_delays (dict of Delay): Dictionary that holds any Delay() instances
we might be using.
_saved_time (float): Remaining time from the previous initialization.
_valid_npc_origins (list of Vector): List containing spawn positions
for NPCs.
"""

def __init__(self):
"""Initializes the object."""
self.current_darkness = 0
self.in_progress = False
self.npc_indexes = set()

self.flash_think = Repeat(self._flash_think)
self.darken_think = Repeat(self._darken_think)
self.gather_data_think = Repeat(self._gather_data_think)
self.spawn_npcs_think = Repeat(self._spawn_npcs_think)

self.old_fog_values = None
self._fog = None
self._delays = {}
self._saved_time = None
self._valid_npc_origins = []

def initialize(self, instant=False):
"""Starts the apocalypse after a randomly chosen delay."""
# Don't go further if the apocalypse is already happening.
if self.in_progress:
return

try:
self._delays['init'].cancel()
except (KeyError, ValueError):
pass

# Are we trying to instantly start the apocalypse?
if instant:
self.begin()
else:
self._delays['init'] = Delay(
# Resume the time to start from the previous initialization if
# there is one, otherwise pick a random time.
self._saved_time if self._saved_time else random.choice(
INTERVALS), self.begin)

def begin(self):
"""Starts the apocalypse."""
self.in_progress = True
self.current_darkness = 0
self._saved_time = None

try:
self._delays['init'].cancel()
except (KeyError, ValueError):
pass

SIREN_SOUND.play()
# Sync the red flashes with the siren sound - every time the players
# hear the siren, their screen will be flashed red.
# NOTE: This was synced with the default 'SIREN_SOUND'.
self.flash_think.start(
interval=6.5, limit=SIREN_TIME / 6.5, execute_on_start=True)
# Start lowering the brightness.
self.darken_think.start(interval=0.5, limit=40, execute_on_start=True)
# Look for valid spawn positions for NPCs during the siren phase.
self.gather_origin_data()

# Change to a thick fog similar to the one from Silent Hill.
# (credit: killer89 - https://gamebanana.com/prefabs/1308 )
self._delays['final_flash'] = Delay(
SIREN_TIME, self.change_fog, (FOG_COLOR, FOG_COLOR, 0, 620))
# Start changing the fog back to normal.
self._delays['restoration'] = Delay(
SIREN_TIME + FOG_FADE_DELAY, self.restore_fog_smooth, (
FOG_FADE_TIME,))

def stop(self, pause_init=True):
"""Stops the apocalypse and restores everything back to normal."""
SIREN_SOUND.stop()

# Stop the looping functions.
self.flash_think.stop()
self.darken_think.stop()
self.gather_data_think.stop()
self.spawn_npcs_think.stop()

if pause_init:
try:
# If we're stopping the apocalypse before it had a chance to
# begin, save the remaining time - so we can resume it later.
self._saved_time = self._delays['init'].time_remaining
except KeyError:
pass

# Cancel all delays.
for delay in self._delays.values():
try:
delay.cancel()
except ValueError:
continue

# Set the brightness back to normal levels.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).darken_view(0)

self.restore_fog()
self.in_progress = False
self._fog = None

def clean_up_data(self):
"""Removes data that's no longer needed/valid."""
self._valid_npc_origins.clear()
self.npc_indexes.clear()

def remove_all_npcs(self):
"""Removes all currently active NPCs."""
for index in self.npc_indexes.copy():
NPC(index).remove()

def on_completed(self):
"""Called when the fog finally settles back to the default values."""
self.in_progress = False
# Prepare for the next apocalypse.
self.initialize()

def gather_origin_data(self):
"""Starts gathering valid spawn positions for NPCs."""
# Have we gathered a decent amount of spawn positions?
if len(self._valid_npc_origins) >= 32:
return

self.gather_data_think.start(
interval=2, limit=14, execute_on_start=True)

def _gather_data_think(self):
for edict in PlayerGenerator():
player = PlayerSH(index_from_edict(edict))

# Is this player dead?
if player.dead:
continue

new_origin = player.origin + NPC_ORIGIN_OFFSET

# Go through previously added spawn positions.
for origin in self._valid_npc_origins:
# Is the new position too close to this one?
if origin.get_distance(new_origin) <= 256:
# No need to keep looking, stop the loop.
break
else:
# The new position is far enough, add it to the list.
self._valid_npc_origins.append(new_origin)

def _flash_think(self):
UTIL_ScreenFadeAll(FLASH_COLOR, 0.5, 0.25, 1)

def _darken_think(self):
# Increase the darkness.
self.current_darkness += 5

# Reduce the brightness for each player on the server.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).darken_view(
self.current_darkness)

def _spawn_npcs_think(self):
# Have we hit the NPC limit?
if len(self.npc_indexes) >= NPCS_MAX:
return

try:
origin = random.choice(self._valid_npc_origins)
except IndexError:
# Missing data for NPC spawn points.
return

# Pick an NPC to spawn.
npc = NPC.create(npc_name=random.choice(NPCS), origin=origin)

# Add the NPC's index to the set.
self.npc_indexes.add(npc.index)

def get_fog_instance(self):
"""Returns an Entity instance of an 'env_fog_controller'."""
if self._fog is not None:
return self._fog

old_fog = Entity.find('env_fog_controller')
# Does an 'env_fog_controller' already exist on this map?
if old_fog:
# Store the old values for later use.
self.old_fog_values = (
old_fog.get_property_color('m_fog.colorPrimary'),
old_fog.get_property_color('m_fog.colorSecondary'),
old_fog.fog_start,
old_fog.fog_end
)

# We'll use that one for our fog shenanigans.
self._fog = old_fog
return self._fog

# Guess we need to make a new one.
new_fog = Entity.create('env_fog_controller')
new_fog.fog_enable = True
new_fog.fog_blend = True
new_fog.fog_max_density = 1.0
new_fog.spawn_flags = 1
new_fog.spawn()

new_fog.target_name = 'silent_hill_fog'
# Fix for maps without fog.
for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).call_input(
'SetFogController', new_fog.target_name)

self._fog = new_fog
return self._fog

def remove_fog(self):
"""Removes the stored 'env_fog_controller' entity.

Note:
This is only used if we're making our own 'env_fog_controller'.
"""
try:
self._fog.remove()
except AttributeError:
return

self._fog = None

def change_fog(self, color1, color2, start, end, final_flash=True):
"""Changes the fog visuals.

Args:
color1 (Color): Primary color of the fog.
color2 (Color): Secondary color of the fog.
start (float): Distance at which the fog begins.
end (float): Distance at which the fog is at its maximum.
final_flash (bool): Is this the final red flash?
"""
fog = self.get_fog_instance()
fog.set_color(color1)
fog.set_color_secondary(color2)
fog.set_start_dist(start)
fog.set_end_dist(end)

# Is this the final flash?
if final_flash:
# Add a stronger flash to mask the changes in fog and screen
# brightness.
UTIL_ScreenFadeAll(FLASH_COLOR_END, 1, 0.5, 1)

for edict in PlayerGenerator():
PlayerSH(index_from_edict(edict)).darken_view(0)

# Should we start spawning NPCs?
if NPCS_ENABLED:
# Spawn them only during the thick fog phase.
self.spawn_npcs_think.start(
interval=3, limit=FOG_FADE_DELAY / 3)

def restore_fog(self):
"""Restores the fog back to normal."""
if self._fog is None:
return

# Was the map without fog?
if self.old_fog_values is None:
self.remove_fog()
return

self.change_fog(*self.old_fog_values, False)

def restore_fog_smooth(self, duration):
"""Smoothly restores the fog back to normal over the given duration."""
if self._fog is None:
return

should_remove = False
old_values = self.old_fog_values
# Was the map missing an 'env_fog_controller' entity?
if old_values is None:
should_remove = True
# Just increase the 'start' and 'end' values for the fog before
# removing it - making the transition semi-smooth.
old_values = (FOG_COLOR, FOG_COLOR, 512, 14000)

self._fog.set_color_lerp_to(old_values[0])
self._fog.set_color_secondary_lerp_to(old_values[1])
self._fog.set_start_dist_lerp_to(old_values[2])
self._fog.set_end_dist_lerp_to(old_values[3])
# Add 0.001 to the transition duration to avoid flashes caused by the
# fog bouncing back (engine quirk) before being set in place.
self._fog.fog_lerp_time = duration + 0.001
self._fog.start_fog_transition()

# If we created our own fog, we need to remove it.
if should_remove:
self._delays['ending'] = self._fog.delay(duration, self.remove_fog)
else:
# Make sure the fog values stay put after the transition.
self._delays['ending'] = self._fog.delay(
duration, self.change_fog, (*self.old_fog_values, False))

# The cycle is complete - let's do that again!
self._delays['restart'] = Delay(duration, self.on_completed)


dark_times = DarkTimes()


# =============================================================================
# >> UTIL_SCREENFADEALL - https://git.io/JJXoe
# =============================================================================
server_binary = find_binary('server')


if PLATFORM == 'windows':
identifier_screen = b'\x55\x8B\xEC\xD9\x45\x10\x8D\x45\xF4'
else:
identifier_screen = '_Z18UTIL_ScreenFadeAllRK9color32_sffi'


UTIL_ScreenFadeAll = server_binary[identifier_screen].make_function(
Convention.CDECL,
(DataType.POINTER, DataType.FLOAT, DataType.FLOAT, DataType.INT),
DataType.VOID
)


# =============================================================================
# >> UTIL_GETLOCALPLAYER FIX - (thank you Ayuto)
# viewtopic.php?f=20&t=1907#p12247
# =============================================================================
if PLATFORM == 'windows':
identifier_local = \
b'\xA1\x2A\x2A\x2A\x2A\x8B\x2A\x2A\x83\x2A\x01\x7E\x03\x33\xC0\xC3'
else:
identifier_local = '_Z19UTIL_GetLocalPlayerv'


UTIL_GetLocalPlayer = server_binary[identifier_local].make_function(
Convention.CDECL,
[],
DataType.POINTER
)


@PreHook(UTIL_GetLocalPlayer)
def get_local_player_pre(stack_data):
"""Called when the engine tries to get the local Player object.

This function was designed for single-player and should NOT be called in
multi-player games unless you want the server to crash.
"""
for edict in PlayerGenerator():
try:
return pointer_from_edict(edict)
except ValueError:
pass

return NULL


# =============================================================================
# >> ENV_SPRITE
# =============================================================================
def create_sprite(origin, scale, model):
"""Creates an 'env_sprite' entity.

Args:
origin (Vector): Spawn position of the 'env_sprite'.
scale (float): Size of the sprite (max size: 64.0).
model (Model): Appearance of the sprite.
"""
sprite = Entity.create('env_sprite')
sprite.model = model
sprite.origin = origin
sprite.set_key_value_float('scale', scale)
sprite.set_key_value_bool('disablereceiveshadows', True)
sprite.set_key_value_float('HDRColorScale', 0)
sprite.render_amt = 1
sprite.render_mode = RenderMode.TRANS_COLOR
sprite.set_key_value_string('rendercolor', '0 0 0')
sprite.render_fx = RenderEffects.NONE
sprite.spawn_flags = 1
sprite.spawn()
return sprite


# =============================================================================
# >> NPC
# =============================================================================
class NPC(Entity):
"""Class used to create and interact with NPC entities."""

def __init__(self, index, caching=True):
"""Initializes the object."""
super().__init__(index, caching)
# Give the NPC a name.
self.target_name = f'sh_npc_{self.inthandle}'

@classmethod
def create(cls, npc_name, origin, seek_after_spawn=True):
"""Creates the NPC entity.

Args:
npc_name (str): Name of the NPC to spawn. (e.g. 'npc_zombie')
origin (Vector): Spawn position of the NPC.
seek_after_spawn (bool): Should the NPC start looking for players
after spawning?
"""
try:
npc = super().create(npc_name)
except ValueError:
# Invalid entity class name.
return

npc.origin = origin
# Make the NPC fully transparent.
npc.render_amt = 1
# Slowly fade it into existence.
npc.set_key_value_int('renderfx', 7)
# Set a couple of spawn flags.
# 2: Gag (No IDLE sounds until angry)
# 256: Long Visibility/Shoot
npc.spawn_flags = 2 + 256
npc.spawn()
npc.become_hostile()

if seek_after_spawn:
# Start going towards a random player after a short delay.
# We need to do this so the NPC doesn't just stand still once it
# spawns. This way it won't block the spawn point.
npc.delay(0.2, npc.seek_random_player)

return npc

def become_hostile(self):
"""Makes the NPC hate the players."""
self.call_input('SetRelationship', 'player D_HT 99')

def seek_random_player(self):
"""Starts heading towards a randomly selected player."""
targets = []
for edict in PlayerGenerator():
targets.append(PlayerSH(index_from_edict(edict)).target_name)

# We need an 'aiscripted_schedule' entity to make the NPC do things.
schedule = Entity.create('aiscripted_schedule')
# Set which entity we'd like to command.
schedule.set_key_value_string('m_iszEntity', self.target_name)
# Set their current state to 'Idle'.
# (0: None, 1: Idle, 2: Alert, 3: Combat)
schedule.set_key_value_int('forcestate', 1)
# Make the NPC set the target as their enemy and start going towards
# them. Here are all the options for this keyvalue:
# 0: <None>
# 1: Walk to Goal Entity
# 2: Run to Goal Entity
# 3: Set enemy to Goal Entity
# 4: Walk Goal Path
# 5: Run Goal Path
# 6: Set enemy to Goal Entity AND Run to Goal Entity
schedule.set_key_value_int('schedule', 6)
# In case the NPC takes damage, stop the schedule.
# (0: General, 1: Damage or death, 2: Death)
schedule.set_key_value_int('interruptability', 1)
# Pick a random player to be the enemy.
schedule.set_key_value_string('goalent', random.choice(targets))
schedule.spawn()
schedule.call_input('StartSchedule')
schedule.delay(5, schedule.remove)

def dissolve(self):
"""Removes the ragdoll of the NPC."""
dissolver = Entity.find_or_create('env_entity_dissolver')
dissolver.dissolve_type = 0
dissolver.dissolve(self.target_name)

def check_headcrab(self):
"""Checks if the headcrab was previously attached to a zombie."""
# Does this headcrab lack an owner? (invalid inthandle)
if self.owner_handle == -1:
# No need to go further.
return

self.become_hostile()
self.delay(0.1, self.seek_random_player)
dark_times.npc_indexes.add(self.index)



# =============================================================================
# >> SERVER COMMANDS
# =============================================================================
@ServerCommand('force_dark_times')
def force_dark_times_cmd(command):
dark_times.initialize(instant=True)
Features added:
  • You can now adjust the duration of the siren phase.
  • Add in any NPC you want.
  • Ragdolls/bodies should get removed as soon as the NPC dies.
  • And last but not least, the plugin should now work properly on Linux.
Before adding NPCs, I suggest you setup the convars for NPC health and damage in your server.cfg file. I believe Painkiller posted a .cfg file earlier in this thread. It had most, if not all of the needed convars.

L'In20Cible wrote:Maybe hibernation is disabled on their servers? Other than that; no idea.
Would that cause anything below the hook not to load within the plugin file? I mean, the ServerCommand at the bottom of the file wasn't registered at all - he wasn't able to run it until we commented out the SetServerHibernation hook. I've switched to OnClientActive and OnClientDisconnect listeners, but I'm still curious how that issue works. :tongue:

Painkiller wrote:To the fast_zombie I think you could create a vpk file that could bring back all the contents of the fast_zombie.
- models
- materials
- sounds
But I don't know if this is feasible for you "Vincit".
https://steamcommunity.com/sharedfiles/ ... =354131748
So I took a look at how this could be done. It appears that the npc_fastzombie.cpp isn't included in the server binaries. Which is why it can't be spawned. The way to fix that is to compile the binaries yourself with the included npc_fastzombie file and use them instead of the default ones. I'm not even sure how clients could connect to a server with modified binaries? :confused:

Either way, I've given up on the fast zombie thing.


Code: Select all

2020-09-11 22:05:10 - sp   -   EXCEPTION   
[SP] Caught an Exception:
Traceback (most recent call last):
  File "..\addons\source-python\packages\source-python\listeners\tick.py", line 80, in _tick
    self.pop(0).execute()
  File "..\addons\source-python\packages\source-python\listeners\tick.py", line 161, in execute
    return self.callback(*self.args, **self.kwargs)
  File "..\addons\source-python\packages\source-python\listeners\tick.py", line 606, in _execute
    self.callback(*self.args, **self.kwargs)
  File "..\addons\source-python\plugins\silent_hill\silent_hill.py", line 440, in _spawn_npcs_think
    self.npc_indexes.add(npc.index)

AttributeError: 'NoneType' object has no attribute 'index'


Getting this error now.
User avatar
VinciT
Senior Member
Posts: 331
Joined: Thu Dec 18, 2014 2:41 am

Re: [HL2:DM] Little Silent Hill

Postby VinciT » Sat Sep 12, 2020 3:44 am

daren adler wrote:

Code: Select all

2020-09-11 22:05:10 - sp   -   EXCEPTION   
[SP] Caught an Exception:
Traceback (most recent call last):
  File "..\addons\source-python\packages\source-python\listeners\tick.py", line 80, in _tick
    self.pop(0).execute()
  File "..\addons\source-python\packages\source-python\listeners\tick.py", line 161, in execute
    return self.callback(*self.args, **self.kwargs)
  File "..\addons\source-python\packages\source-python\listeners\tick.py", line 606, in _execute
    self.callback(*self.args, **self.kwargs)
  File "..\addons\source-python\plugins\silent_hill\silent_hill.py", line 440, in _spawn_npcs_think
    self.npc_indexes.add(npc.index)

AttributeError: 'NoneType' object has no attribute 'index'


Getting this error now.
Can you show me which NPCs you've added to the tuple? I believe this is caused by an invalid NPC classname. I've updated the plugin in this post to make it easier to spot such errors.

Painkiller wrote:By testing for a long time I get out of the server crashing without error messages
Do you mean testing the plugin or the fast zombie thing? If it's the former, I'm going to need more details. What were you doing in-game when it crashed?

Painkiller wrote:I found someone who did it and it works.
He told me to edit the server_srv.so.
I wanted to let you know and see if you could help.
https://github.com/ValveSoftware/source ... /mod_hl2mp
I'm afraid this is beyond my current skill set. You could try asking one of the big three (Ayuto, L'In20Cible, or satoon101) for assistance with that.
ImageImageImageImageImage

Return to “Plugin Requests”

Who is online

Users browsing this forum: No registered users and 16 guests