[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 » Sat Sep 12, 2020 9:21 am

VinciT wrote:
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?

I start the server and as soon as the npc enter the server crashes.
I do not find any error messages.

I would like to tell you what the problem is.
Maybe you could look at it again directly on the server.



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.


Daren says the same as me the problem seems to be a bit more complicated I hope that one of the three big helpers can help.
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 » Sun Sep 13, 2020 2:48 am

VinciT wrote:
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.

YES. I added

NPCS = (
'npc_zombie',
'npc_rollermine',
'npc_poisonzombie',
'npc_manhack',
'npc_headcrab_black'
)

they all work, they just get spawned right on top of each other sometimes. I also changed the sound, (you said something about it not working right if changes), its works tho. players spawn that sound on another script i am using for sounds., works great for me,,some things you might not want to spawn is antlionguard, at least not right now,,when spawns,,he dont move till he sees you,,so while hes there, other npc spawn right on top of him,that is why i am not using him or vortigaunt ,both same reason. I removed the npc_clawscanner, it works and does what the clawscanner does, but to many of them(like 5 to 7) spawn at one time do i removed it. :grin: i set the max npc to 50 also. I would like it if you could put back on the light for the manhacks to be able to see them coming from the fog.
Last edited by daren adler on Fri Sep 18, 2020 9:18 pm, edited 1 time in total.
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 18, 2020 7:16 am

I think I have narrowed down the problem.

It was the npc_antlion that caused the server to freeze.
User avatar
VinciT
Senior Member
Posts: 331
Joined: Thu Dec 18, 2014 2:41 am

Re: [HL2:DM] Little Silent Hill

Postby VinciT » Sun Sep 20, 2020 5:43 am

daren adler wrote:they all work, they just get spawned right on top of each other sometimes.
I'll modify the spawning function tomorrow to lower (maybe even remove) the chance of that happening.

daren adler wrote:some things you might not want to spawn is antlionguard, at least not right now,,when spawns,,he dont move till he sees you,,so while hes there, other npc spawn right on top of him,that is why i am not using him or vortigaunt ,both same reason.
Huh, that's odd. I'll see if I can push them away from the spawn point or something.

daren adler wrote:I would like it if you could put back on the light for the manhacks to be able to see them coming from the fog.
Sure thing, I'll add it back in!

Painkiller wrote:I think I have narrowed down the problem.

It was the npc_antlion that caused the server to freeze.
I'm not sure what to do about that. Antlions worked fine on my test server. Are you running any other plugins that modify NPCs?
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 » Sun Sep 20, 2020 8:25 am

VinciT wrote:
daren adler wrote:they all work, they just get spawned right on top of each other sometimes.
I'll modify the spawning function tomorrow to lower (maybe even remove) the chance of that happening.

daren adler wrote:some things you might not want to spawn is antlionguard, at least not right now,,when spawns,,he dont move till he sees you,,so while hes there, other npc spawn right on top of him,that is why i am not using him or vortigaunt ,both same reason.
Huh, that's odd. I'll see if I can push them away from the spawn point or something.

daren adler wrote:I would like it if you could put back on the light for the manhacks to be able to see them coming from the fog.
Sure thing, I'll add it back in!

Painkiller wrote:I think I have narrowed down the problem.

It was the npc_antlion that caused the server to freeze.
I'm not sure what to do about that. Antlions worked fine on my test server. Are you running any other plugins that modify NPCs?


Actually only the npc_points developed by them.
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 » Sun Sep 20, 2020 1:53 pm

Painkiller wrote:
VinciT wrote:
daren adler wrote:they all work, they just get spawned right on top of each other sometimes.
I'll modify the spawning function tomorrow to lower (maybe even remove) the chance of that happening.

daren adler wrote:some things you might not want to spawn is antlionguard, at least not right now,,when spawns,,he dont move till he sees you,,so while hes there, other npc spawn right on top of him,that is why i am not using him or vortigaunt ,both same reason.
Huh, that's odd. I'll see if I can push them away from the spawn point or something.

daren adler wrote:I would like it if you could put back on the light for the manhacks to be able to see them coming from the fog.
Sure thing, I'll add it back in!

Painkiller wrote:I think I have narrowed down the problem.

It was the npc_antlion that caused the server to freeze.
I'm not sure what to do about that. Antlions worked fine on my test server. Are you running any other plugins that modify NPCs?


Actually only the npc_points developed by them.


Except for them once in awhile being on top of each other, they all work. i even tryed the antlionguard and it worked,,they all work on my side.
User avatar
VinciT
Senior Member
Posts: 331
Joined: Thu Dec 18, 2014 2:41 am

Re: [HL2:DM] Little Silent Hill

Postby VinciT » Tue Sep 22, 2020 5:43 am

Popping in to give you a status update. I will most likely post the updated plugin later today. Just gotta work out some tiny details.
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 » Tue Sep 22, 2020 7:22 am

VinciT wrote:Popping in to give you a status update. I will most likely post the updated plugin later today. Just gotta work out some tiny details.


Ok,,thank you for all of your help.
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 28, 2020 5:18 am

Sorry for the late update, life kinda got in the way. Here's the updated plugin:

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 engines.trace import ContentMasks, engine_trace, GameTrace, Ray
from entities.constants import (EntityEffects, RenderMode, RenderEffects,
MoveType, WORLD_ENTITY_INDEX)
from entities.entity import Entity
from entities.helpers import index_from_edict, pointer_from_edict
from events import Event
from filters.entities import EntityIter
from listeners import (OnEntityCreated, OnEntityDeleted, OnLevelInit,
OnLevelEnd, OnClientActive, OnClientDisconnect)
from listeners.tick import Delay, Repeat
from mathlib import NULL_VECTOR, Vector, QAngle
from memory import Convention, DataType, NULL, find_binary
from memory.hooks import PreHook
from players import PlayerGenerator
from players.entity import Player
from stringtables import string_tables


# 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 = 10
# 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'
)

# For how many seconds should 'npc_manhack' entities glow after spawning?
# (setting this to 0 disables the glow)
MANHACK_GLOW_DURATION = 2


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)
NPC_ORIGIN_MINS = Vector(-64, -64, 0)
NPC_ORIGIN_MAXS = Vector(64, 64, 64)


# 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.
_push (Entity): Entity instance of the 'point_push' entity.
_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._push = 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

try:
self._push.remove()
except AttributeError:
pass

self._push = 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 push_away_from(self, origin, radius, magnitude):
"""Pushes all movable entities at the specified origin.

Args:
origin (Vector): Point from which to push away entities from.
radius (float): Maximum distance an entity can be from the `origin`
and still be pushed away.
magnitude (float): Power/strength of the push.
"""
if self._push is None:
push = Entity.create('point_push')
# Set a couple of spawn flags.
# 8: Push players
# 16: Push physics
push.spawn_flags = 8 + 16
push.spawn()
self._push = push

self._push.origin = origin
self._push.set_property_float('m_flRadius', radius)
self._push.set_property_float('m_flMagnitude', magnitude)
self._push.call_input('Enable')
self._push.delay(0.15, self._push.call_input, ('Disable',))

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=SIREN_TIME / 2, 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
# Let's see if there's enough room for NPCs to spawn here.
enough_space = NPC.check_space(
origin=new_origin,
mins=NPC_ORIGIN_MINS,
maxs=NPC_ORIGIN_MAXS
)

if not enough_space:
continue

# 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

# Push away any entities (props, players, npcs) from the spawn point.
self.push_away_from(origin, 200, 500)
# Spawn the NPC after a short delay.
self._delays['npc_spawn'] = Delay(0.15, self._spawn_npc, (origin,))

def _spawn_npc(self, origin):
# 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.
return

# Move the NPC slightly in a random direction after spawning.
npc.base_velocity = get_random_direction() * 180

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


# =============================================================================
# >> CNPC_BaseZombie::ReleaseHeadcrab() - https://git.io/JUou2
# =============================================================================
if PLATFORM == 'windows':
identifier_release = \
b'\x55\x8B\xEC\x83\xEC\x18\x53\x56\x57\x8B\xF9\x8B\x4D\x08\x80\xBF\x60'
else:
identifier_release = \
'_ZN15CNPC_BaseZombie15ReleaseHeadcrabERK6VectorS2_bbb'


ReleaseHeadcrab = server_binary[identifier_release].make_function(
Convention.THISCALL,
(
DataType.POINTER, DataType.POINTER, DataType.POINTER, DataType.BOOL,
DataType.BOOL, DataType.BOOL),
DataType.VOID
)


@PreHook(ReleaseHeadcrab)
def release_headcrab_pre(stack_data):
"""Called when a zombie is about to release a headcrab."""
# Is the zombie trying to release a headcrab ragdoll?
if stack_data[5]:
# Block the release - stop the ragdoll from spawning.
return False


# =============================================================================
# >> 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."""
effect_name = 'vortigaunt_hand_glow'

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}'

@staticmethod
def check_space(origin, mins, maxs):
"""Checks if there is enough space at the given origin."""
trace = GameTrace()
engine_trace.clip_ray_to_entity(
Ray(origin, origin, mins, maxs),
ContentMasks.ALL,
Entity(WORLD_ENTITY_INDEX),
trace
)

return not trace.did_hit()

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

# Is this an 'npc_manhack' and are glows enabled?
if 'manhack' in npc_name and MANHACK_GLOW_DURATION > 0:
npc.create_spawn_particle(MANHACK_GLOW_DURATION)

return npc

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

# Parent the particle to the NPC.
particle.set_parent(self)
# Stop the effect before removing it. This will prevent the effect
# from stopping (detaching from the NPC) in place and looking weird.
particle.delay(life_time, particle.stop)
# Remove the particle after a bit of a delay.
particle.delay(life_time + 2, particle.remove)

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():
player = PlayerSH(index_from_edict(edict))

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

targets.append(player.target_name)

# Is the list empty?
if not targets:
return

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


# =============================================================================
# >> HELPERS
# =============================================================================
def get_random_direction():
"""Returns a random directional vector without the Z axis (up/down)."""
direction = Vector()
QAngle(0, random.randint(0, 360), 0).get_angle_vectors(forward=direction)
return direction.normalized()


# =============================================================================
# >> SERVER COMMANDS
# =============================================================================
@ServerCommand('force_dark_times')
def force_dark_times_cmd(command):
dark_times.initialize(instant=True)

New (and returning) features:
  • Manhacks have regained the ability to glow! Set the duration of the glow by changing the MANHACK_GLOW_DURATION value.
  • Zombies no longer drop headcrab ragdolls when killed with a headshot.
  • Added a space/room check when gathering NPC spawn point data - this should prevent NPCs from spawning inside walls and static props.
  • Before an NPC spawns, all movable entities around the chosen spawn point will be pushed away.
Image
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 28, 2020 7:33 am

Very good job my friend :cool: :cool: Works very good and now no spawning on top of each other and now can see the manhack going through the fog. Nice job. :smile: :smile: THANK YOU :cool:
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 28, 2020 9:32 am

VinciT wrote:Sorry for the late update, life kinda got in the way. Here's the updated plugin:

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 engines.trace import ContentMasks, engine_trace, GameTrace, Ray
from entities.constants import (EntityEffects, RenderMode, RenderEffects,
MoveType, WORLD_ENTITY_INDEX)
from entities.entity import Entity
from entities.helpers import index_from_edict, pointer_from_edict
from events import Event
from filters.entities import EntityIter
from listeners import (OnEntityCreated, OnEntityDeleted, OnLevelInit,
OnLevelEnd, OnClientActive, OnClientDisconnect)
from listeners.tick import Delay, Repeat
from mathlib import NULL_VECTOR, Vector, QAngle
from memory import Convention, DataType, NULL, find_binary
from memory.hooks import PreHook
from players import PlayerGenerator
from players.entity import Player
from stringtables import string_tables


# 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 = 10
# 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'
)

# For how many seconds should 'npc_manhack' entities glow after spawning?
# (setting this to 0 disables the glow)
MANHACK_GLOW_DURATION = 2


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)
NPC_ORIGIN_MINS = Vector(-64, -64, 0)
NPC_ORIGIN_MAXS = Vector(64, 64, 64)


# 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.
_push (Entity): Entity instance of the 'point_push' entity.
_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._push = 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

try:
self._push.remove()
except AttributeError:
pass

self._push = 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 push_away_from(self, origin, radius, magnitude):
"""Pushes all movable entities at the specified origin.

Args:
origin (Vector): Point from which to push away entities from.
radius (float): Maximum distance an entity can be from the `origin`
and still be pushed away.
magnitude (float): Power/strength of the push.
"""
if self._push is None:
push = Entity.create('point_push')
# Set a couple of spawn flags.
# 8: Push players
# 16: Push physics
push.spawn_flags = 8 + 16
push.spawn()
self._push = push

self._push.origin = origin
self._push.set_property_float('m_flRadius', radius)
self._push.set_property_float('m_flMagnitude', magnitude)
self._push.call_input('Enable')
self._push.delay(0.15, self._push.call_input, ('Disable',))

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=SIREN_TIME / 2, 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
# Let's see if there's enough room for NPCs to spawn here.
enough_space = NPC.check_space(
origin=new_origin,
mins=NPC_ORIGIN_MINS,
maxs=NPC_ORIGIN_MAXS
)

if not enough_space:
continue

# 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

# Push away any entities (props, players, npcs) from the spawn point.
self.push_away_from(origin, 200, 500)
# Spawn the NPC after a short delay.
self._delays['npc_spawn'] = Delay(0.15, self._spawn_npc, (origin,))

def _spawn_npc(self, origin):
# 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.
return

# Move the NPC slightly in a random direction after spawning.
npc.base_velocity = get_random_direction() * 180

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


# =============================================================================
# >> CNPC_BaseZombie::ReleaseHeadcrab() - https://git.io/JUou2
# =============================================================================
if PLATFORM == 'windows':
identifier_release = \
b'\x55\x8B\xEC\x83\xEC\x18\x53\x56\x57\x8B\xF9\x8B\x4D\x08\x80\xBF\x60'
else:
identifier_release = \
'_ZN15CNPC_BaseZombie15ReleaseHeadcrabERK6VectorS2_bbb'


ReleaseHeadcrab = server_binary[identifier_release].make_function(
Convention.THISCALL,
(
DataType.POINTER, DataType.POINTER, DataType.POINTER, DataType.BOOL,
DataType.BOOL, DataType.BOOL),
DataType.VOID
)


@PreHook(ReleaseHeadcrab)
def release_headcrab_pre(stack_data):
"""Called when a zombie is about to release a headcrab."""
# Is the zombie trying to release a headcrab ragdoll?
if stack_data[5]:
# Block the release - stop the ragdoll from spawning.
return False


# =============================================================================
# >> 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."""
effect_name = 'vortigaunt_hand_glow'

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}'

@staticmethod
def check_space(origin, mins, maxs):
"""Checks if there is enough space at the given origin."""
trace = GameTrace()
engine_trace.clip_ray_to_entity(
Ray(origin, origin, mins, maxs),
ContentMasks.ALL,
Entity(WORLD_ENTITY_INDEX),
trace
)

return not trace.did_hit()

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

# Is this an 'npc_manhack' and are glows enabled?
if 'manhack' in npc_name and MANHACK_GLOW_DURATION > 0:
npc.create_spawn_particle(MANHACK_GLOW_DURATION)

return npc

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

# Parent the particle to the NPC.
particle.set_parent(self)
# Stop the effect before removing it. This will prevent the effect
# from stopping (detaching from the NPC) in place and looking weird.
particle.delay(life_time, particle.stop)
# Remove the particle after a bit of a delay.
particle.delay(life_time + 2, particle.remove)

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():
player = PlayerSH(index_from_edict(edict))

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

targets.append(player.target_name)

# Is the list empty?
if not targets:
return

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


# =============================================================================
# >> HELPERS
# =============================================================================
def get_random_direction():
"""Returns a random directional vector without the Z axis (up/down)."""
direction = Vector()
QAngle(0, random.randint(0, 360), 0).get_angle_vectors(forward=direction)
return direction.normalized()


# =============================================================================
# >> SERVER COMMANDS
# =============================================================================
@ServerCommand('force_dark_times')
def force_dark_times_cmd(command):
dark_times.initialize(instant=True)

New (and returning) features:
  • Manhacks have regained the ability to glow! Set the duration of the glow by changing the MANHACK_GLOW_DURATION value.
  • Zombies no longer drop headcrab ragdolls when killed with a headshot.
  • Added a space/room check when gathering NPC spawn point data - this should prevent NPCs from spawning inside walls and static props.
  • Before an NPC spawns, all movable entities around the chosen spawn point will be pushed away.
Image


Tested: It works great.

Thanks for the renewals.

'npc_antlion' Still crashes the server under Linux. But it is not bad. I can get along fine without it.

Improvement: When everything gets dark, could you add the "torch" to an exception?
User avatar
VinciT
Senior Member
Posts: 331
Joined: Thu Dec 18, 2014 2:41 am

Re: [HL2:DM] Little Silent Hill

Postby VinciT » Tue Sep 29, 2020 3:42 am

Cool, I'm glad you guys are enjoying the update! :grin:

Painkiller wrote:'npc_antlion' Still crashes the server under Linux. But it is not bad. I can get along fine without it.
This might be a long shot, but let's try it anyway. Go to line 795 (might be different if you've modified the plugin) and change this:

Syntax: Select all

if seek_after_spawn:
To this:

Syntax: Select all

if seek_after_spawn and npc_name != 'npc_antlion':
Try running the plugin with antlions again and tell me if the crashes still occur.

Painkiller wrote:Improvement: When everything gets dark, could you add the "torch" to an exception?
I'm not sure I can do that. At least not with the current darkening method. It's basically a big sprite in front of the player's head/view that changes opacity over time - thus creating the darkening effect.

When I started working on this plugin, my initial plan/idea was to use two Fade messages, one for the darkening effect and another for the red flashes. But that came with two major issues:
  • The red flashes not being able to use fade in/fade out (the two Fades kept overriding each other's parameters) - this made the flashes extremely jarring.
  • Having to constantly spam out Fade messages to make sure every player had their screen darkened.
I'm not even sure if this initial method had a working flashlight/torch. Anyway.. I've rambled on for long enough. If anyone knows a better way to achieve the same thing and allow the flashlight to function properly, I'm all ears! :smile:
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 » Tue Sep 29, 2020 7:22 am

VinciT wrote:Cool, I'm glad you guys are enjoying the update! :grin:

Painkiller wrote:'npc_antlion' Still crashes the server under Linux. But it is not bad. I can get along fine without it.
This might be a long shot, but let's try it anyway. Go to line 795 (might be different if you've modified the plugin) and change this:

Syntax: Select all

if seek_after_spawn:
To this:

Syntax: Select all

if seek_after_spawn and npc_name != 'npc_antlion':
Try running the plugin with antlions again and tell me if the crashes still occur.

Painkiller wrote:Improvement: When everything gets dark, could you add the "torch" to an exception?
I'm not sure I can do that. At least not with the current darkening method. It's basically a big sprite in front of the player's head/view that changes opacity over time - thus creating the darkening effect.

When I started working on this plugin, my initial plan/idea was to use two Fade messages, one for the darkening effect and another for the red flashes. But that came with two major issues:
  • The red flashes not being able to use fade in/fade out (the two Fades kept overriding each other's parameters) - this made the flashes extremely jarring.
  • Having to constantly spam out Fade messages to make sure every player had their screen darkened.
I'm not even sure if this initial method had a working flashlight/torch. Anyway.. I've rambled on for long enough. If anyone knows a better way to achieve the same thing and allow the flashlight to function properly, I'm all ears! :smile:


Works great VinciT :smile: :smile: This is a great plugin my friend!!!!. you do a very good job at what you do, :cool: :cool: Stay cool and have a great week.
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 29, 2020 1:51 pm

VinciT wrote:Cool, I'm glad you guys are enjoying the update! :grin:

Painkiller wrote:'npc_antlion' Still crashes the server under Linux. But it is not bad. I can get along fine without it.
This might be a long shot, but let's try it anyway. Go to line 795 (might be different if you've modified the plugin) and change this:

Syntax: Select all

if seek_after_spawn:
To this:

Syntax: Select all

if seek_after_spawn and npc_name != 'npc_antlion':
Try running the plugin with antlions again and tell me if the crashes still occur.

Painkiller wrote:Improvement: When everything gets dark, could you add the "torch" to an exception?
I'm not sure I can do that. At least not with the current darkening method. It's basically a big sprite in front of the player's head/view that changes opacity over time - thus creating the darkening effect.

When I started working on this plugin, my initial plan/idea was to use two Fade messages, one for the darkening effect and another for the red flashes. But that came with two major issues:
  • The red flashes not being able to use fade in/fade out (the two Fades kept overriding each other's parameters) - this made the flashes extremely jarring.
  • Having to constantly spam out Fade messages to make sure every player had their screen darkened.
I'm not even sure if this initial method had a working flashlight/torch. Anyway.. I've rambled on for long enough. If anyone knows a better way to achieve the same thing and allow the flashlight to function properly, I'm all ears! :smile:


I have to tell you that I noticed the lightning and the red is annoying. So I set both to 0.

The darkening effect is quite enough.
I thought it would be cool in the beginning but once I saw it it was not my case anymore.

So you could take it out from my side.
Let's hear Daren's thoughts on that.

Greats Pain.

Edit: Antlion no longer spawn and server does not crash
One question, can we also spawn our own models ?
User avatar
VinciT
Senior Member
Posts: 331
Joined: Thu Dec 18, 2014 2:41 am

Re: [HL2:DM] Little Silent Hill

Postby VinciT » Thu Oct 01, 2020 5:42 am

I checked if the flashlight/torch worked with my initial method for the darkening - it didn't. :frown:

Painkiller wrote:One question, can we also spawn our own models ?

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 engines.trace import ContentMasks, engine_trace, GameTrace, Ray
from entities.constants import (EntityEffects, RenderMode, RenderEffects,
MoveType, WORLD_ENTITY_INDEX)
from entities.entity import Entity
from entities.helpers import index_from_edict, pointer_from_edict
from events import Event
from filters.entities import EntityIter
from listeners import (OnEntityCreated, OnEntityDeleted, OnLevelInit,
OnLevelEnd, OnClientActive, OnClientDisconnect)
from listeners.tick import Delay, Repeat
from mathlib import NULL_VECTOR, Vector, QAngle
from memory import Convention, DataType, NULL, find_binary
from memory.hooks import PreHook
from players import PlayerGenerator
from players.entity import Player
from stringtables import string_tables


# 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 = 10
# 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'
)

# If you wish to change models for certain NPCs, add them here.
NPC_MODELS = {
'npc_zombie': (Model('models/zombie/fast.mdl'),),
'npc_headcrab': (Model('models/headcrab.mdl'),)
}

# For how many seconds should 'npc_manhack' entities glow after spawning?
# (setting this to 0 disables the glow)
MANHACK_GLOW_DURATION = 2


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)
NPC_ORIGIN_MINS = Vector(-64, -64, 0)
NPC_ORIGIN_MAXS = Vector(64, 64, 64)


# 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.
_push (Entity): Entity instance of the 'point_push' entity.
_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._push = 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

try:
self._push.remove()
except AttributeError:
pass

self._push = 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 push_away_from(self, origin, radius, magnitude):
"""Pushes all movable entities at the specified origin.

Args:
origin (Vector): Point from which to push away entities from.
radius (float): Maximum distance an entity can be from the `origin`
and still be pushed away.
magnitude (float): Power/strength of the push.
"""
if self._push is None:
push = Entity.create('point_push')
# Set a couple of spawn flags.
# 8: Push players
# 16: Push physics
push.spawn_flags = 8 + 16
push.spawn()
self._push = push

self._push.origin = origin
self._push.set_property_float('m_flRadius', radius)
self._push.set_property_float('m_flMagnitude', magnitude)
self._push.call_input('Enable')
self._push.delay(0.15, self._push.call_input, ('Disable',))

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=SIREN_TIME / 2, 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
# Let's see if there's enough room for NPCs to spawn here.
enough_space = NPC.check_space(
origin=new_origin,
mins=NPC_ORIGIN_MINS,
maxs=NPC_ORIGIN_MAXS
)

if not enough_space:
continue

# 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

# Push away any entities (props, players, npcs) from the spawn point.
self.push_away_from(origin, 200, 500)
# Spawn the NPC after a short delay.
self._delays['npc_spawn'] = Delay(0.15, self._spawn_npc, (origin,))

def _spawn_npc(self, origin):
# 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.
return

# Move the NPC slightly in a random direction after spawning.
npc.base_velocity = get_random_direction() * 180

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


# =============================================================================
# >> CNPC_BaseZombie::ReleaseHeadcrab() - https://git.io/JUou2
# =============================================================================
if PLATFORM == 'windows':
identifier_release = \
b'\x55\x8B\xEC\x83\xEC\x18\x53\x56\x57\x8B\xF9\x8B\x4D\x08\x80\xBF\x60'
else:
identifier_release = \
'_ZN15CNPC_BaseZombie15ReleaseHeadcrabERK6VectorS2_bbb'


ReleaseHeadcrab = server_binary[identifier_release].make_function(
Convention.THISCALL,
(
DataType.POINTER, DataType.POINTER, DataType.POINTER, DataType.BOOL,
DataType.BOOL, DataType.BOOL),
DataType.VOID
)


@PreHook(ReleaseHeadcrab)
def release_headcrab_pre(stack_data):
"""Called when a zombie is about to release a headcrab."""
# Is the zombie trying to release a headcrab ragdoll?
if stack_data[5]:
# Block the release - stop the ragdoll from spawning.
return False


# =============================================================================
# >> 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."""
effect_name = 'vortigaunt_hand_glow'

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}'

@staticmethod
def check_space(origin, mins, maxs):
"""Checks if there is enough space at the given origin."""
trace = GameTrace()
engine_trace.clip_ray_to_entity(
Ray(origin, origin, mins, maxs),
ContentMasks.ALL,
Entity(WORLD_ENTITY_INDEX),
trace
)

return not trace.did_hit()

@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()

try:
# Are there any alternative models for this NPC?
npc.model = random.choice(NPC_MODELS[npc_name])
except KeyError:
# Nope, moving on..
pass

if seek_after_spawn and npc_name != 'npc_antlion':
# 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)

# Is this an 'npc_manhack' and are glows enabled?
if 'manhack' in npc_name and MANHACK_GLOW_DURATION > 0:
npc.create_spawn_particle(MANHACK_GLOW_DURATION)

return npc

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

# Parent the particle to the NPC.
particle.set_parent(self)
# Stop the effect before removing it. This will prevent the effect
# from stopping (detaching from the NPC) in place and looking weird.
particle.delay(life_time, particle.stop)
# Remove the particle after a bit of a delay.
particle.delay(life_time + 2, particle.remove)

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():
player = PlayerSH(index_from_edict(edict))

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

targets.append(player.target_name)

# Is the list empty?
if not targets:
return

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


# =============================================================================
# >> HELPERS
# =============================================================================
def get_random_direction():
"""Returns a random directional vector without the Z axis (up/down)."""
direction = Vector()
QAngle(0, random.randint(0, 360), 0).get_angle_vectors(forward=direction)
return direction.normalized()


# =============================================================================
# >> SERVER COMMANDS
# =============================================================================
@ServerCommand('force_dark_times')
def force_dark_times_cmd(command):
dark_times.initialize(instant=True)
You can add alternative models for NPCs in the NPC_MODELS dictionary. If you add in more than one for the same NPC, the model will be chosen at random.
Take a look at this example:

Syntax: Select all

NPC_MODELS = {
'npc_zombie': (
Model('models/zombie/fast.mdl'),
Model('models/zombie/poison.mdl'),
Model('models/zombie/classic.mdl')
)
}
Anytime an npc_zombie spawns, it'll randomly choose between the fast, poison, and regular(classic) zombie models.
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 » Thu Oct 01, 2020 7:49 am

Hi vincit, I have added these models. Unfortunately they now move like headcraps and not as they should.

Code: Select all

materials/models/characters/ph/ph1.vmt
materials/models/characters/ph/ph1.vtf
materials/models/characters/ph/ph1_normal.vtf
models/pyramid_head.dx80.vtx
models/pyramid_head.dx90.vtx
models/pyramid_head.mdl
models/pyramid_head.phy
models/pyramid_head.sw.vtx
models/pyramid_head.vvd
models/pyramid_head.xbox.vtx
materials/models/psychedelicum/silent_hill/bubble_nurse/demon_nurse_face.vmt
materials/models/psychedelicum/silent_hill/bubble_nurse/demon_nurse_face.vtf
materials/models/psychedelicum/silent_hill/bubble_nurse/demon_nurse_face_bump.vtf
materials/models/psychedelicum/silent_hill/bubble_nurse/demon_nurse_hat.vmt
materials/models/psychedelicum/silent_hill/bubble_nurse/demon_nurse_hat.vtf
materials/models/psychedelicum/silent_hill/bubble_nurse/demon_nurse_hat_bump.vtf
materials/models/psychedelicum/silent_hill/bubble_nurse/great_knife.vmt
materials/models/psychedelicum/silent_hill/bubble_nurse/great_knife.vtf
materials/models/psychedelicum/silent_hill/bubble_nurse/great_knife_bump.vtf
materials/models/psychedelicum/silent_hill/bubble_nurse/nurse_arms.vmt
materials/models/psychedelicum/silent_hill/bubble_nurse/nurse_arms.vtf
materials/models/psychedelicum/silent_hill/bubble_nurse/nurse_arms_bump.vtf
materials/models/psychedelicum/silent_hill/bubble_nurse/nurse_arms_spec.vtf
materials/models/psychedelicum/silent_hill/bubble_nurse/nurse_body.vmt
materials/models/psychedelicum/silent_hill/bubble_nurse/nurse_body.vtf
materials/models/psychedelicum/silent_hill/bubble_nurse/nurse_body_bump.vtf
materials/models/psychedelicum/silent_hill/bubble_nurse/nurse_body_spec.vtf
materials/models/psychedelicum/silent_hill/bubble_nurse/nurse_head.vmt
materials/models/psychedelicum/silent_hill/bubble_nurse/nurse_head.vtf
materials/models/psychedelicum/silent_hill/bubble_nurse/nurse_head_bump.vtf
materials/models/psychedelicum/silent_hill/bubble_nurse/nurse_head_spec.vtf
materials/models/psychedelicum/silent_hill/bubble_nurse/nurse_leggings.vmt
materials/models/psychedelicum/silent_hill/bubble_nurse/nurse_leggings.vtf
materials/models/psychedelicum/silent_hill/bubble_nurse/nurse_leggings_bump.vtf
materials/models/psychedelicum/silent_hill/bubble_nurse/nurse_leggings_spec.vtf
materials/models/psychedelicum/silent_hill/bubble_nurse/nurse_lips.vmt
materials/models/psychedelicum/silent_hill/bubble_nurse/nurse_lips.vtf
materials/models/psychedelicum/silent_hill/bubble_nurse/nurse_lips_bump.vtf
materials/models/psychedelicum/silent_hill/bubble_nurse/nurse_lips_spec.vtf
models/psychedelicum/silent_hill/bubble_nurse/npc/bubble_nurse_combine.dx80.vtx
models/psychedelicum/silent_hill/bubble_nurse/npc/bubble_nurse_combine.dx90.vtx
models/psychedelicum/silent_hill/bubble_nurse/npc/bubble_nurse_combine.mdl
models/psychedelicum/silent_hill/bubble_nurse/npc/bubble_nurse_combine.phy
models/psychedelicum/silent_hill/bubble_nurse/npc/bubble_nurse_combine.sw.vtx
models/psychedelicum/silent_hill/bubble_nurse/npc/bubble_nurse_combine.vvd
models/psychedelicum/weapons/bubble_nurse_pm/bubble_nurse_arms.dx80.vtx
models/psychedelicum/weapons/bubble_nurse_pm/bubble_nurse_arms.dx90.vtx
models/psychedelicum/weapons/bubble_nurse_pm/bubble_nurse_arms.mdl
models/psychedelicum/weapons/bubble_nurse_pm/bubble_nurse_arms.sw.vtx
models/psychedelicum/weapons/bubble_nurse_pm/bubble_nurse_arms.vvd
User avatar
VinciT
Senior Member
Posts: 331
Joined: Thu Dec 18, 2014 2:41 am

Re: [HL2:DM] Little Silent Hill

Postby VinciT » Thu Oct 01, 2020 8:09 pm

Painkiller wrote:Hi vincit, I have added these models. Unfortunately they now move like headcraps and not as they should.
Can you show me your NPC_MODELS dictionary?
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 » Thu Oct 01, 2020 8:27 pm

Code: Select all

# If you wish to change models for certain NPCs, add them here.
NPC_MODELS = {
    'npc_zombie': (Model('models/pyramid_head.mdl'),),
    'npc_headcrab': (Model('models/psychedelicum/silent_hill/bubble_nurse/npc/bubble_nurse_combine.mdl'),)
}


:confused:
User avatar
VinciT
Senior Member
Posts: 331
Joined: Thu Dec 18, 2014 2:41 am

Re: [HL2:DM] Little Silent Hill

Postby VinciT » Thu Oct 01, 2020 9:08 pm

Okay, the way you have it setup right now, the pyramid head model is being set for the npc_zombie, and the bubble nurse model for the npc_headcrab - which is why the nurse is moving like a headcrab. If your intention was to set both models for the npc_zombie, replace the dictionary with this:

Syntax: Select all

NPC_MODELS = {
'npc_zombie': (
Model('models/pyramid_head.mdl'),
Model('models/psychedelicum/silent_hill/bubble_nurse/npc/bubble_nurse_combine.mdl')
)
}
If that's not the case, replace npc_headcrab in your dictionary with the name of the NPC that's supposed to have the nurse model.
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 » Thu Oct 01, 2020 10:06 pm

VinciT wrote:Okay, the way you have it setup right now, the pyramid head model is being set for the npc_zombie, and the bubble nurse model for the npc_headcrab - which is why the nurse is moving like a headcrab. If your intention was to set both models for the npc_zombie, replace the dictionary with this:

Syntax: Select all

NPC_MODELS = {
'npc_zombie': (
Model('models/pyramid_head.mdl'),
Model('models/psychedelicum/silent_hill/bubble_nurse/npc/bubble_nurse_combine.mdl')
)
}
If that's not the case, replace npc_headcrab in your dictionary with the name of the NPC that's supposed to have the nurse model.


To me (now remember i am not a script maker), its better to leave it the way it is on the (npc's) models, I am saying this becouse i tryed the way VinciT shows us (both ways) and it will give you the skin, but the model do not move right at all, and there is no weapon in hands and hands/arms do not move at all, also in the OLD t-pose. For me, i use the sourcemod skinchooser https://forums.alliedmods.net/showthread.php?p=780243 for the skins, I am using the pyramid_head and nurse also.
I guess you might be able to get it to do better and play right, i think you would have to have them 2 models with some kind of setup for the way they are going to fight/move/use weapons (the nurse has a knife) stuff like that. but like i said "im no scripter".

Return to “Plugin Requests”

Who is online

Users browsing this forum: No registered users and 22 guests