Published: Jan 18, 2024
This is a list of the design goals and implementation of my many different plugins for the game Left 4 Dead 2. This was originally hosted on my L4D2 Server admin panel, and has instead been migrated here.
TKStopper
Original goal was to simple prevent teamkillers, but it has evolved into a friendly-fire management and suicider-prevention plugin as well.
.smx file | Source Code (.sp) |
The current system only applies for this condition:
- A non-admin survivor attacks another survivor.
- The victim is a real player or an idle player bot.
There are three scenarios that can affect damage dealt / reversed:
- Within first 2 minutes of joining, no damage is dealt to either the victim or attacker. This is to prevent the next person to join from being punished.
- During the finale vehicle arrival, they deal 0 damage and take 2x reverse friendly fire.
- In saferooms, no damage is dealt to either party.
- If none of the above, then any reverse friendly fire damage is applied on an increasing scale.
Every friendly fire event causes the attacker’s reversed friendly fire to increase up-to a maximum (5x). This way, the worst their aim is, the more they get punished over time. In addition, this scale is slowly reduced over time, allowing accidental friendly-fires to slowly be forgotten if enough time passes without another incident.
During any of the above three conditions, if they deal (or attempt to deal) over 75 HP in 15 seconds, an action is immediately applied. On my public servers, the user is automatically banned for 1 hour, which allows admins to manually increase their ban length. On my reserved servers, the attacker is marked as a troll, where they deal 0 damage, and we can mess with them. Most teamkillers get very angry when their plans are foiled and we just watch them deal zero damage.
Auto Reverse-FF
The automatic reverse friendly fire system works as followed:
Everytime a friendly fire is inflicted as a survivor, first any previously earned reverse ff rate is subtracted based on the minutes since their last ff and a decreasing rate cvar, which the default is 0.02:
rffRate[attacker] -= minutes * rffDecrementRate
Then, the increment scale (default is 0.01) cvar is applied, multiplied by the damage dealt:
rffRate[attacker] += rffIncrementScale * dmg
Then that new rate is multiplied against damage again rffRate[attacker] * dmg
for the amount of damage they themselves take (the friendly fire attacker), and the victim of the friendly fire takes a perpendicular amount of damage (as the attacker friendly fire more, the victim receives less)
The goal of this system is to not punish accidental friendly fire (as it slowly is forgiven, based on time.) and to punish those that constantly, although typically unintentionally, with a slowly increasing punishment which they are in control of.
Side notes
When a teamkiller is detected (see above section), the attacker earns 2x increment scale, making them quickly get reverse friendly-fired hard.
Crescendo Stopper
.smx file | Source Code (.sp) |
This plugin prevents the activation of buttons ahead of the team. It will prevent players from starting crescendos (and some small other activities as a side effect) until a certain threshold of the team has reached the area.
Flow is an internal system in l4d2 that measures how far along a survivor is in a map. The plugin caches the highest flow rate the survivor has achieved. (Therefore players need to be at the button only once) If a button press activates a horde, then the next button press will be allowed by any player.
The algorithm for detecting if the button should be pressable or not is as followed:
-
If the previous button press caused a horde, the new button press is allowed
- All alive, survivors are checked for their flow. If their flow is less than the button activator’s flow
player flow < (activator flow - threshold)
they are added to a far-away list- Threshold is to give a little buffer space, incase the other players are just a few meters away from button
- If the amount of survivors added to far-away list is 0, allow the button press
- Everyone is ahead of or at the button, so allow
- If the the difference between the averaged flow of the far-away players and the activator flow is less than threshold, allow
activator flow - average <= threshold
- If the % of survivors that are far is less than or equal to a percent threshold (30%), allow
players / totalPlayers <= percentThreshold
Sadly, althrough this works great at preventing some rushers, it also has its own issues that are hard to fix:
- Sometimes the buttons are far from the team, but should still be pressable (swamp fever finale, or death toll 4th chapter van button)
- One or more admins can move forward near the button, then walk back. As their highest flow is what is used in calculation, this will allow button activation
- Sometimes the buttons are too close, but shouldn’t be pressable
- Unfortunately, its very difficult to balance the distances without making per-map configurations.
- Some events don’t play the mob panic sound effect, causing the stop-event button from being blocked
- Unfortuantely, there is not an easy way to detect these events to properly allow the activation.
Extra Player Items
.smx file | Source Code (.sp) |
This plugin provides extra items for a game of 5+ players or more. This includes providing extra health kits in saferooms and extra items outside of the room.
Extra Items
The original system was simple, after a few seconds, once everyone has loaded in (and the player count is stabilized), iterate through every _spawn
entity that is valid, and with a random chance, increment the m_itemCount
dataprop by one. This would make some items provide more than one.
That system served well for a long time, but it can make the game confusing. Suddenly, you go find an item, pick it up, and it’s still there, because it had a count of 2. I also got so used to the normal spawn locations of weapons, it started feeling dull, so I introduce my new system: Randomized item spawners. This system will randomly create new item spawns throughout the map.
The system works as follows:
- Get all nav areas
- Filter out any nav area that have these spawn attributes:
FLOW_BLOCKED
,ESCAPE_ROUTE
,DOOR or DESTROYED_DOOR
,CHECKPOINT
,NO_MOBS
andSTOP_SCAN
and no base attributeFLOW_BLOCKED
.- See the List of L4D Series Nav Mesh Attributes for descriptions of the attributes
- For those that remain, 4 traces are ran in every 90° turn, checking if there is a solid wall in that direction.
- If the # of walls found is ≥ 2, there is a 50% to spawn a melee weapon and 50% to spawn any non-melee.
Spawning Items
For normal item spawns, which exclude melee, there is a list of weapon_..._spawn
classnames in a big list. The list is divided into 4 tiers, which are not the same as L4D2’s internal tier system but close. The tiers dictate the right-side bound to randomly pick from the array. So a tier 0 has the possible indexes [0, 11] where tier 1 has [0, 19]. The tier itself is randomly chosen between 0 and 1 on the first chapter, and on all other chapters, a left-biased dice is rolled.
Melee weapon spawns are simply handled by the game’s weapon_melee_spawn
itself.
Extra Saferoom Kits
This feature has gone through multiple iterations to figure out the best and most reliable method. Due to some custom maps having item spawns in different locations the following system is used:
- Real survivor player count is all the EXTRA in game survivors (including idle) that are not bots
- This means if there is 5 players, the count will be 1
- A player enters a saferoom, this computes the total real player survivor count. At a minimum, the amount of extra kits provided will be this value, but bonus kits can be given depending on the team’s average health, to compensate for the increased 5+ difficulty players might not be used to.
- If the average team health is ≤ 30, 100% more kits are given.
- If the average team health is ≤ 50, 50% more kits are given.
- There is a random chance of a bonus kit
- Every
first_aid_kit_spawn
entity is looped, and checked if they are in the ending saferoom (using L4DHooks’sL4D_IsPositionInLastCheckpoint
). If the kit is in the safe room, another kit is spawned slightly above, then the loop continues- Loop continues until either the # of extra kits hits 0 or after one full loop, no kits were found in saferoom
- Previously, the system worked instead that when a user picked up a kit, it was cancelled and another kit was given to them directly.
- When they load back in from a map transition, if they do not already have a kit they are given an extra from the extra kit count pool.
- This was for the old system of kit pickups, but it’s still included in the fallback that no kits in saferoom were found.
Extra Cabinet Items
This feature took over 10 hours of development and brainstorming for such a simple feature. This feature simply will scale the amount of items in a cabinet (pills or kits) depending on the player count.
On map load, the plugin will search for all cabinets and add them to a list. It will also search for any pill spawners that are near a cabinet and add them to the list of items for that cabinet.
The plugin keeps the amount of physical items in cabinet the same, just increases the amount you can pickup. This is the cabinetItems
variable (a value between 1-4)
Then once the round freeze ends, it will check all cabinets and generate the amount of items to increase:
playerCount * (cabinetItems/4.0) - cabinetItems
Then each item in the cabinet is constantly looped and its item count increased by one until the extra item count is 0.
Saferoom Door Locking
This took a bit to figure out how to accurately check the saferoom door, as you can find all saferoom doors but you cannot easily find out if it is the beginning or ending saferoom.
The plugin checks all prop_door_rotating_checkpoint
and find the first door with the m_bLocked
property set to 1 (the door is locked). It then will keep that door locked until either a timer expires or when a percentage of players has loaded in.
First Spawning
On first spawn, another feature is to give the incoming player’s a random weapon. The system scans every player’s weapons (primary and secondary/melee) and puts them into their respective lists. The new player is then given a random item from these lists.
There was some conflicts - one being ABM’s spawning item feature, and another being giving them to their idle bot.
The ABM feature was easy to disable, but the other conflict was hard to figure out. When players first join, they are technically idle as the bot until they fully load in. When my plugin attempted to give items, the items would tend to drop on the floor in-front. Now items are given directly to the player, which 80% of the time causes no dropped items.
Extra Tanks
The goal is simple:
During games where there are 5 or more players, tanks are by default (determined by another plugin, ABM) (now built-in) spawned with an increased health dependent on the player count. An example is, in Advanced difficulty, tanks spawn with 4000 tank by default. If you have 8 players, tanks will spawn with 4000 + 2500(N-4) = 14000
health where N = # of players.
The problem becomes that when you have more than 4 players, some players exclude themselves from the fight, leaving a super strong tank but only half the players fighting him. There is also the problem of space, some areas might be super cramped with even just 4 players, putting a tank in there means there might not be an angle to shoot at the tank. Usually only around 3-4 players typically fight a single tank.
The solution is during non-finale tanks is to wait for a tank to spawn, and then “split” it’s health, such that the increased health tank (lets say 14,000 HP) would now have 7000 HP and another tank spawns with 7,000 HP as well. This has the benefit that a game of 7-8 players can hopefully break into two squads that fight the tanks, which helps mimic the traditional 4-player behavior.
Finales
The original system only did the splitting on the second finale tank, but after some 8 player games with friends, I felt its necessary for it to always occur, but I didn’t want to scrap the finale system.
The current system as it stands instead just spawns a second tank, normal health such that you’ll fight three tanks total during the finale.
Problems / Misc
A logical issue in the first trial for the system to get stuck in an infinite spawn loop of tanks after the spawn timer for the second timer ran. So… there was upto ~18 tanks at once all around…
Another issue is the health, when I had the original finale split tanks health means they die really fast, like normal difficulty tanks, which isn’t as enjoyable. This is why in finales, it still spawns two tanks, but they both have 5plus-accounted health.
The last problem is when its too much, such as Hard Rain’s finale. Two 8,000 HP tanks is a little bit extreme, when surrounded by slow water and only one rooftop. This has been disabled for now by hardcoded checks for c4m5_milltown_escape
and disable the system.
Extra Witches
I wanted to have extra witches spawn, scaled by player count. This system has a lot of complexity, to ensure it still makes the game random and unpredictable. The system works as follows:
-
On map start, knowing the amount of players from the previous round (
abmExtraCount
in code), two biased, up-to 7-sided, dice are rolled.- The dice are biased to the left, with this distribution (https://anydice.com/program/c91)
- The minimum value of the dice is determined by
ceil((abmExtraCount - 4)/ 4)
where abmExtraCount is the internal variable tracking the number of players. TheabmExtraCount - 4
is to extract the number of extra players (minus the 4 base players), and it’s divided by 4 to slow it’s increment.
- The dice roll determines how many extra witches (the normal game director still can spawn its own witch) will spawn. Let’s call that N.
- For every number of extra witches, a random flow position is calculated between [0, M]
- M is the map’s maximum flow (see Crescendo Stopper for explanation)
- The value is offset by FLOW_CUTOFF which is an arbitrary value that is chosen to prevent tanks from spawning too close to the beginning and ending saferooms / areas.
- The end result is
[0 + FLOW_CUTOFF, M - FLOW_CUTOFF]
- A timer is created that runs periodically that first:
-
Checks that at least DIRECTOR_WITCH_MIN_TIME (120 seconds) has passed since the last time any witches (not just ours) have spawned
-
There has not been N amount of witches spawned (
totalWitchesSpawned < N
)
-
- The timer iterates over all flow positions calculated earlier, and checks if the highest flow achieved on the chapter is greater or equal to the flow.
- If it is, a witch is spawned and the flow value is reset to 0.0 (to stop it continuously spawning more). The iteration breaks on the first witch spawn, we don’t want to spawn more than one at a time.
Witch Spawning
How the witch is spawned is pretty simple, there is a built in game command z_spawn_old
that has an option to spawn like the director, z_spawn_old witch auto
, that will spawn just like the director would.
The only gotcha is that the command runs around the player who executed the command. We run the command on behalf of the client with the highest flow value on the map, hoping that the director spawns the witch ahead and not on-top or behind the survivors. Let’s hope that works.