CatStation/Content.Server/Zombies/ZombieSystem.cs
Prole 502af6955d
Predict Forensics V2.0 (Ready for Review) (#40693)
* What sitting on code does to a branch + no mo owner v2

* Replacing ViewVariables with AutoNetworkedField + Missed One V2

* Removing Virtual Methods & Renaming ForensicsSystem to SharedForensicsSystem (Also made it abstract) V2

* IProduceWidgets Review Changes & some of my own V2

* Lil Cleanup V2

* Part 1 of Review Changes (Only thing left is the BUI stuff and the constructor) V2

* It now WORKS!!! (Still needs the BUI and constuctor changes, ill figure it out later)

* Test (Removal of ServerSide VomitSystem.cs)

* Ok...

* Moved System BloodstreamSystem.cs to SharedBloodstreamSystem.cs & deleted client BloodStreamSystem.cs to make SharedBloodstreamSystem.cs sealed. (No Unregistered errors)

* Prediction

* Fix?

* No mo log error?

* Revert "No mo log error?"

This reverts commit 50c15a3deab6af9dffd7baed45820bae5558e41f.

* Constructor going up

* No ForensicScannerBoundUserInterface.cs (It doesnt wanna scan anymore but it doesnt crash)

* For Conflicts

* Guh

* Yea

* Organization & no ErrorHell (maybe)

* Dirtying

* Dirtying take 2

* Revert "Dirtying take 2"

This reverts commit 5735012e21f43808db3ae010fce268eac24d0583.

* the dirties dont do much here

* For Conflict

* Readded

* Another Conflict Commit Fix

* Conflict Fix Thrice

* Empty Space

* Keep it simple

* Add it back in

* Move BloodstreamSystem stuff thats causing the error back to system. This'll prob fix the conflict

* Conflict fix?

* Trust the fix

* Looked at other prediction PR's, they had better code.

* To not mix up the system in client

* Removing Conflict

* For conflict

* For the conflic fix, will be removed later

* Scuffed Fix

* Removal & Adding the Decouple

* Took some HEAVY pointers from SirWarocks work. The Roadblock has been CLEARED, DOAFTERS WORK NOW!!

* Cleanup & Optimizing, mostly comps.

* More Optimization, most of this is just imported Warock Code, his QOL included. Did not want to remove SolutionDNA's tho, mostly due to a Log.Debug in OnPrint.

* Forgot that the Print Refreshes, more of Warocks work.

* Removing Loc.GetString

* All of BloodStream to Shared, hopefully no errors.

* Sketchy Dirty Removed

* Removed Timer.Spawn, added FrameUpdate cause anything else only updated when the window opened, & a bit of cleanup.

* No mo Mispredict

* GenerateDnaEvent Predicted

* Forgot the user

* I knew it was real, bloodstutter is REAL and now GONE.

* Cant dirty NewEntityBloodData due to potential stutter, so its safer to put it all back into server. again...

* Clearer

* Thank you leahcat, no more identity leak.

* Routine Maintenance

* sealing the class

* it checks for this now?!

* While im here, FileScope Namespaces & removing unused namespaces

* Update Attribute Type (SirWarock Import), it works and is much easier to read.

* Forever Fixing Conflicts

* Readonly Genocide & Partial Maxing

* LGTM, BloodstreamSystem moved to SharedBloodstreamSystem!

* Sohn

* If OG Github back push this commit

* One Billionth Commit Fix

* Removed client/server stuff

* Made implicit

* NewLine & comment on why AutoGenCompState is true

* Updatin the rest of the Popups to use Entity

* I think that's what they meant? It looks cleaner at least.

* Mass Attribute for LocalEvents that are safe to change.

* Putting it back as abstract

* maybe?

* Guhh, rolling back empty client/server of BloodStream

* Removed Client/Server BloodstreamSystems, changed access modifier from protected to private, & renamed SharedBloodstreamSystem to BloodstreamSystem

* From Protected to Private

* Removed comment & made it explicit on what changed to true

* Guh, hopefully thats all of it...

* Forgot

* Review

* Test Push

* Line 316 in SharedForensicsSystem was causing the errors, better to play it safe

* Review Changes

* Update Content.Shared/Forensics/Events.cs

Co-authored-by: Whatstone <166147148+whatston3@users.noreply.github.com>

* The only thing SolutionDNAs does is search and clears itself. I see no reason to keep it

* The print.disabled didnt properly disable when cleared. So ill just use the old if check

* Figured out the rest of the SubscribeLocalEvents and networking BloodData cause its fully in Shared

---------

Co-authored-by: Whatstone <166147148+whatston3@users.noreply.github.com>
2026-08-08 22:48:36 +00:00

348 lines
14 KiB
C#

using Content.Shared.NPC.Prototypes;
using Content.Server.Actions;
using Content.Server.Body.Systems;
using Content.Server.Chat;
using Content.Server.Chat.Systems;
using Content.Server.Emoting.Systems;
using Content.Shared.Speech.EntitySystems;
using Content.Shared.Anomaly.Components;
using Content.Shared.Armor;
using Content.Shared.Bed.Sleep;
using Content.Shared.Body.Systems;
using Content.Shared.Cloning.Events;
using Content.Shared.Chat;
using Content.Shared.Damage.Systems;
using Content.Shared.Inventory;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Revolutionary;
using Content.Shared.Roles;
using Content.Shared.Roles.Components;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Zombies;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Content.Server.Ghost.Roles.Components;
namespace Content.Server.Zombies
{
public sealed partial class ZombieSystem : SharedZombieSystem
{
[Dependency] private IGameTiming _timing = default!;
[Dependency] private IRobustRandom _random = default!;
[Dependency] private BloodstreamSystem _bloodstream = default!;
[Dependency] private DamageableSystem _damageable = default!;
[Dependency] private ChatSystem _chat = default!;
[Dependency] private ActionsSystem _actions = default!;
[Dependency] private AutoEmoteSystem _autoEmote = default!;
[Dependency] private EmoteOnDamageSystem _emoteOnDamage = default!;
[Dependency] private MobStateSystem _mobState = default!;
[Dependency] private SharedPopupSystem _popup = default!;
[Dependency] private SharedRoleSystem _role = default!;
public readonly ProtoId<NpcFactionPrototype> Faction = "Zombie";
public const SlotFlags ProtectiveSlots =
SlotFlags.FEET |
SlotFlags.HEAD |
SlotFlags.EYES |
SlotFlags.GLOVES |
SlotFlags.MASK |
SlotFlags.NECK |
SlotFlags.INNERCLOTHING |
SlotFlags.OUTERCLOTHING;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ZombieComponent, EmoteEvent>(OnEmote, before:
new[] { typeof(VocalSystem), typeof(BodyEmotesSystem) });
SubscribeLocalEvent<ZombieComponent, MeleeHitEvent>(OnMeleeHit);
SubscribeLocalEvent<ZombieComponent, MobStateChangedEvent>(OnMobState);
SubscribeLocalEvent<ZombieComponent, CloningEvent>(OnZombieCloning);
SubscribeLocalEvent<ZombieComponent, TryingToSleepEvent>(OnSleepAttempt);
SubscribeLocalEvent<ZombieComponent, GetCharactedDeadIcEvent>(OnGetCharacterDeadIC);
SubscribeLocalEvent<ZombieComponent, GetCharacterUnrevivableIcEvent>(OnGetCharacterUnrevivableIC);
SubscribeLocalEvent<ZombieComponent, MindAddedMessage>(OnMindAdded);
SubscribeLocalEvent<ZombieComponent, MindRemovedMessage>(OnMindRemoved);
SubscribeLocalEvent<ZombieComponent, AttemptConvertRevolutionaryEvent>(OnAttemptConvert);
SubscribeLocalEvent<PendingZombieComponent, MapInitEvent>(OnPendingMapInit);
SubscribeLocalEvent<PendingZombieComponent, BeforeRemoveAnomalyOnDeathEvent>(OnBeforeRemoveAnomalyOnDeath);
SubscribeLocalEvent<IncurableZombieComponent, MapInitEvent>(OnPendingMapInit);
SubscribeLocalEvent<ZombifyOnDeathComponent, MobStateChangedEvent>(OnDamageChanged);
}
private void OnBeforeRemoveAnomalyOnDeath(Entity<PendingZombieComponent> ent, ref BeforeRemoveAnomalyOnDeathEvent args)
{
// Pending zombies (e.g. infected non-zombies) do not remove their hosted anomaly on death.
// Current zombies DO remove the anomaly on death.
args.Cancelled = true;
}
private void OnPendingMapInit(EntityUid uid, IncurableZombieComponent component, MapInitEvent args)
{
_actions.AddAction(uid, ref component.Action, component.ZombifySelfActionPrototype);
_faction.AddFaction(uid, Faction);
if (HasComp<ZombieComponent>(uid) || HasComp<ZombieImmuneComponent>(uid))
return;
EnsureComp<PendingZombieComponent>(uid, out PendingZombieComponent pendingComp);
pendingComp.GracePeriod = _random.Next(pendingComp.MinInitialInfectedGrace, pendingComp.MaxInitialInfectedGrace);
}
private void OnPendingMapInit(EntityUid uid, PendingZombieComponent component, MapInitEvent args)
{
if (_mobState.IsDead(uid))
{
ZombifyEntity(uid);
return;
}
component.NextTick = _timing.CurTime + TimeSpan.FromSeconds(1f);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var curTime = _timing.CurTime;
// Hurt the living infected
var query = EntityQueryEnumerator<PendingZombieComponent, Shared.Damage.Components.DamageableComponent, MobStateComponent>();
while (query.MoveNext(out var uid, out var comp, out var damage, out var mobState))
{
// Process only once per second
if (comp.NextTick > curTime)
continue;
comp.NextTick = curTime + TimeSpan.FromSeconds(1f);
comp.GracePeriod -= TimeSpan.FromSeconds(1f);
if (comp.GracePeriod > TimeSpan.Zero)
continue;
if (_random.Prob(comp.InfectionWarningChance))
_popup.PopupEntity(Loc.GetString(_random.Pick(comp.InfectionWarnings)), uid, uid);
var multiplier = _mobState.IsCritical(uid, mobState)
? comp.CritDamageMultiplier
: 1f;
_damageable.ChangeDamage((uid, damage), comp.Damage * multiplier, true, false);
}
// Heal the zombified
var zombQuery = EntityQueryEnumerator<ZombieComponent, Shared.Damage.Components.DamageableComponent, MobStateComponent>();
while (zombQuery.MoveNext(out var uid, out var comp, out var damage, out var mobState))
{
// Process only once per second
if (comp.NextTick + TimeSpan.FromSeconds(1) > curTime)
continue;
comp.NextTick = curTime;
if (_mobState.IsDead(uid, mobState))
continue;
var multiplier = _mobState.IsCritical(uid, mobState)
? comp.PassiveHealingCritMultiplier
: 1f;
// Gradual healing for living zombies.
_damageable.ChangeDamage((uid, damage), comp.PassiveHealing * multiplier, true, false);
}
}
private void OnSleepAttempt(EntityUid uid, ZombieComponent component, ref TryingToSleepEvent args)
{
args.Cancelled = true;
}
private void OnGetCharacterDeadIC(EntityUid uid, ZombieComponent component, ref GetCharactedDeadIcEvent args)
{
args.Dead = true;
}
private void OnGetCharacterUnrevivableIC(EntityUid uid, ZombieComponent component, ref GetCharacterUnrevivableIcEvent args)
{
args.Unrevivable = true;
}
private void OnEmote(EntityUid uid, ZombieComponent component, ref EmoteEvent args)
{
// always play zombie emote sounds and ignore others
if (args.Handled)
return;
ProtoMan.Resolve(component.EmoteSoundsId, out var sounds);
args.Handled = _chat.TryPlayEmoteSound(uid, sounds, args.Emote);
}
private void OnMobState(EntityUid uid, ZombieComponent component, MobStateChangedEvent args)
{
if (args.NewMobState == MobState.Alive)
{
// Groaning when damaged
EnsureComp<EmoteOnDamageComponent>(uid);
_emoteOnDamage.AddEmote(uid, "Scream");
// Random groaning
EnsureComp<AutoEmoteComponent>(uid);
_autoEmote.AddEmote(uid, "ZombieGroan");
}
else
{
// Stop groaning when damaged
_emoteOnDamage.RemoveEmote(uid, "Scream");
// Stop random groaning
_autoEmote.RemoveEmote(uid, "ZombieGroan");
}
}
private float GetZombieInfectionChance(EntityUid uid, ZombieComponent zombieComponent)
{
var chance = zombieComponent.BaseZombieInfectionChance;
var armorEv = new CoefficientQueryEvent(ProtectiveSlots);
RaiseLocalEvent(uid, armorEv);
foreach (var resistanceEffectiveness in zombieComponent.ResistanceEffectiveness.DamageDict)
{
if (armorEv.DamageModifiers.Coefficients.TryGetValue(resistanceEffectiveness.Key, out var coefficient))
{
// Scale the coefficient by the resistance effectiveness, very descriptive I know
// For example. With 30% slash resist (0.7 coeff), but only a 60% resistance effectiveness for slash,
// you'll end up with 1 - (0.3 * 0.6) = 0.82 coefficient, or a 18% resistance
var adjustedCoefficient = 1 - ((1 - coefficient) * resistanceEffectiveness.Value.Float());
chance *= adjustedCoefficient;
}
}
var zombificationResistanceEv = new ZombificationResistanceQueryEvent(ProtectiveSlots);
RaiseLocalEvent(uid, zombificationResistanceEv);
chance *= zombificationResistanceEv.TotalCoefficient;
return MathF.Max(chance, zombieComponent.MinZombieInfectionChance);
}
private void OnMeleeHit(Entity<ZombieComponent> entity, ref MeleeHitEvent args)
{
if (!args.IsHit)
return;
var cannotSpread = HasComp<NonSpreaderZombieComponent>(args.User);
foreach (var uid in args.HitEntities)
{
if (args.User == uid)
continue;
if (!TryComp<MobStateComponent>(uid, out var mobState))
continue;
if (HasComp<ZombieComponent>(uid) || HasComp<IncurableZombieComponent>(uid))
{
// Don't infect, don't deal damage, do not heal from bites, don't pass go!
args.Handled = true;
continue;
}
if (_mobState.IsAlive(uid, mobState))
{
_damageable.TryChangeDamage(args.User, entity.Comp.HealingOnBite, true, false);
// If we cannot infect the living target, the zed will just heal itself.
if (HasComp<ZombieImmuneComponent>(uid) || cannotSpread || !_random.Prob(GetZombieInfectionChance(uid, entity.Comp)))
continue;
EnsureComp<PendingZombieComponent>(uid);
EnsureComp<ZombifyOnDeathComponent>(uid);
}
else
{
if (HasComp<ZombieImmuneComponent>(uid) || cannotSpread)
continue;
// If the target is dead and can be infected, infect.
ZombifyEntity(uid);
args.Handled = true;
}
}
}
/// <summary>
/// This is the function to call if you want to unzombify an entity.
/// </summary>
/// <param name="source">the entity having the ZombieComponent</param>
/// <param name="target">the entity you want to unzombify (different from source in case of cloning, for example)</param>
/// <param name="zombiecomp"></param>
/// <remarks>
/// this currently only restore the skin/eye color from before zombified
/// TODO: completely rethink how zombies are done to allow reversal.
/// </remarks>
public bool UnZombify(EntityUid source, EntityUid target, ZombieComponent? zombiecomp)
{
if (!Resolve(source, ref zombiecomp))
return false;
_visualBody.ApplyProfiles(target, zombiecomp.BeforeZombifiedProfiles);
_visualBody.ApplyMarkings(target, zombiecomp.BeforeZombifiedMarkings);
_bloodstream.ChangeBloodReagents(target, zombiecomp.BeforeZombifiedBloodReagents);
return true;
}
private void OnZombieCloning(Entity<ZombieComponent> ent, ref CloningEvent args)
{
UnZombify(ent.Owner, args.CloneUid, ent.Comp);
}
// Make sure players that enter a zombie (for example via a ghost role or the mind swap spell) count as an antagonist.
private void OnMindAdded(Entity<ZombieComponent> ent, ref MindAddedMessage args)
{
if (!_role.MindHasRole<ZombieRoleComponent>(args.Mind))
_role.MindAddRole(args.Mind, "MindRoleZombie", mind: args.Mind.Comp);
}
// Remove the role when getting cloned, getting gibbed and borged, or leaving the body via any other method.
// We also need to make sure the zombie is a ghost role because zombies with minds do not get a ghostrolecomponent
private void OnMindRemoved(Entity<ZombieComponent> ent, ref MindRemovedMessage args)
{
_role.MindRemoveRole<ZombieRoleComponent>((args.Mind.Owner, args.Mind.Comp));
MakeGhostRole(ent.Owner);
}
private void OnAttemptConvert(Entity<ZombieComponent> ent, ref AttemptConvertRevolutionaryEvent args)
{
args.Cancelled = true;
}
/// <summary>
/// Makes the target entity a zombie ghost role. Should only be fired when the entity does not have a mind.
/// </summary>
private void MakeGhostRole(EntityUid ent)
{
//yet more hardcoding. Visit zombie.ftl for more information.
var ghostRole = EnsureComp<GhostRoleComponent>(ent);
EnsureComp<GhostTakeoverAvailableComponent>(ent);
ghostRole.RoleName = Loc.GetString("zombie-generic");
ghostRole.RoleDescription = Loc.GetString("zombie-role-desc");
ghostRole.RoleRules = Loc.GetString("zombie-role-rules");
ghostRole.MindRoles.Add(MindRoleZombie);
}
}
}