using System.Diagnostics.CodeAnalysis; using System.Numerics; using Content.Shared.Gibbing.Components; using Content.Shared.Gibbing.Events; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Map; using Robust.Shared.Physics.Systems; using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.Shared.Gibbing.Systems; public sealed class GibbingSystem : EntitySystem { [Dependency] private readonly SharedContainerSystem _containerSystem = default!; [Dependency] private readonly SharedTransformSystem _transformSystem = default!; [Dependency] private readonly SharedAudioSystem _audioSystem = default!; [Dependency] private readonly SharedPhysicsSystem _physicsSystem = default!; [Dependency] private readonly IRobustRandom _random = default!; //TODO: (future optimization) implement a system that "caps" giblet entities by deleting the oldest ones once we reach a certain limit, customizable via CVAR /// /// Attempt to gib a specified entity. That entity must have a gibable components. This method is NOT recursive will only /// work on the target and any entities it contains (depending on gibContentsOption) /// /// The outermost entity we care about, used to place the dropped items /// Target entity/comp we wish to gib /// What type of gibing are we performing /// What type of gibing do we perform on any container contents? /// a hashset containing all the entities that have been dropped/created /// How much to multiply the random spread on dropped giblets(if we are dropping them!) /// Should we play audio /// A list of containerIds on the target that permit gibing /// A list of containerIds on the target that DO NOT permit gibing /// The cone we are launching giblets in (if we are launching them!) /// Should we launch giblets or just drop them /// The direction to launch giblets (if we are launching them!) /// The impluse to launch giblets at(if we are launching them!) /// /// Should we log if we are missing a gibbableComp when we call this function /// The variation in giblet launch impulse (if we are launching them!) /// True if successful, false if not public bool TryGibEntity(EntityUid outerEntity, Entity gibbable, GibType gibType, GibContentsOption gibContentsOption, out HashSet droppedEntities, bool launchGibs = true, Vector2 launchDirection = default, float launchImpulse = 0f, float launchImpulseVariance = 0f, Angle launchCone = default, float randomSpreadMod = 1.0f, bool playAudio = true, List? allowedContainers = null, List? excludedContainers = null, bool logMissingGibable = false) { droppedEntities = new(); return TryGibEntityWithRef(outerEntity, gibbable, gibType, gibContentsOption, ref droppedEntities, launchGibs, launchDirection, launchImpulse, launchImpulseVariance, launchCone, randomSpreadMod, playAudio, allowedContainers, excludedContainers, logMissingGibable); } /// /// Attempt to gib a specified entity. That entity must have a gibable components. This method is NOT recursive will only /// work on the target and any entities it contains (depending on gibContentsOption) /// /// The outermost entity we care about, used to place the dropped items /// Target entity/comp we wish to gib /// What type of gibing are we performing /// What type of gibing do we perform on any container contents? /// a hashset containing all the entities that have been dropped/created /// How much to multiply the random spread on dropped giblets(if we are dropping them!) /// Should we play audio /// A list of containerIds on the target that permit gibing /// A list of containerIds on the target that DO NOT permit gibing /// The cone we are launching giblets in (if we are launching them!) /// Should we launch giblets or just drop them /// The direction to launch giblets (if we are launching them!) /// The impluse to launch giblets at(if we are launching them!) /// The variation in giblet launch impulse (if we are launching them!) /// Should we log if we are missing a gibbableComp when we call this function /// True if successful, false if not public bool TryGibEntityWithRef( EntityUid outerEntity, Entity gibbable, GibType gibType, GibContentsOption gibContentsOption, ref HashSet droppedEntities, bool launchGibs = true, Vector2? launchDirection = null, float launchImpulse = 0f, float launchImpulseVariance = 0f, Angle launchCone = default, float randomSpreadMod = 1.0f, bool playAudio = true, List? allowedContainers = null, List? excludedContainers = null, bool logMissingGibable = false) { if (!Resolve(gibbable, ref gibbable.Comp, logMissing: false)) { DropEntity(gibbable, Transform(outerEntity), randomSpreadMod, ref droppedEntities, launchGibs, launchDirection, launchImpulse, launchImpulseVariance, launchCone); if (logMissingGibable) { Log.Warning($"{ToPrettyString(gibbable)} does not have a GibbableComponent! " + $"This is not required but may cause issues contained items to not be dropped."); } return false; } if (gibType == GibType.Skip && gibContentsOption == GibContentsOption.Skip) return true; if (launchGibs) { randomSpreadMod = 0; } var parentXform = Transform(outerEntity); HashSet validContainers = new(); var gibContentsAttempt = new AttemptEntityContentsGibEvent(gibbable, gibContentsOption, allowedContainers, excludedContainers); RaiseLocalEvent(gibbable, ref gibContentsAttempt); foreach (var container in _containerSystem.GetAllContainers(gibbable)) { var valid = true; if (allowedContainers != null) valid = allowedContainers.Contains(container.ID); if (excludedContainers != null) valid = valid && !excludedContainers.Contains(container.ID); if (valid) validContainers.Add(container); } switch (gibContentsOption) { case GibContentsOption.Skip: break; case GibContentsOption.Drop: { foreach (var container in validContainers) { foreach (var ent in container.ContainedEntities) { DropEntity(new Entity(ent, null), parentXform, randomSpreadMod, ref droppedEntities, launchGibs, launchDirection, launchImpulse, launchImpulseVariance, launchCone); } } break; } case GibContentsOption.Gib: { foreach (var container in validContainers) { foreach (var ent in container.ContainedEntities) { GibEntity(new Entity(ent, null), parentXform, randomSpreadMod, ref droppedEntities, launchGibs, launchDirection, launchImpulse, launchImpulseVariance, launchCone); } } break; } } switch (gibType) { case GibType.Skip: break; case GibType.Drop: { DropEntity(gibbable, parentXform, randomSpreadMod, ref droppedEntities, launchGibs, launchDirection, launchImpulse, launchImpulseVariance, launchCone); break; } case GibType.Gib: { GibEntity(gibbable, parentXform, randomSpreadMod, ref droppedEntities, launchGibs, launchDirection, launchImpulse, launchImpulseVariance, launchCone); break; } } if (playAudio) { _audioSystem.PlayPredicted(gibbable.Comp.GibSound, parentXform.Coordinates, null); } if (gibType == GibType.Gib) QueueDel(gibbable); return true; } private void DropEntity(Entity gibbable, TransformComponent parentXform, float randomSpreadMod, ref HashSet droppedEntities, bool flingEntity, Vector2? scatterDirection, float scatterImpulse, float scatterImpulseVariance, Angle scatterCone) { var gibCount = 0; if (Resolve(gibbable, ref gibbable.Comp, logMissing: false)) { gibCount = gibbable.Comp.GibCount; } var gibAttemptEvent = new AttemptEntityGibEvent(gibbable, gibCount, GibType.Drop); RaiseLocalEvent(gibbable, ref gibAttemptEvent); switch (gibAttemptEvent.GibType) { case GibType.Skip: return; case GibType.Gib: GibEntity(gibbable, parentXform, randomSpreadMod, ref droppedEntities, flingEntity, scatterDirection, scatterImpulse, scatterImpulseVariance, scatterCone, deleteTarget: false); return; } _transformSystem.AttachToGridOrMap(gibbable); _transformSystem.SetCoordinates(gibbable, parentXform.Coordinates); _transformSystem.SetWorldRotation(gibbable, _random.NextAngle()); droppedEntities.Add(gibbable); if (flingEntity) { FlingDroppedEntity(gibbable, scatterDirection, scatterImpulse, scatterImpulseVariance, scatterCone); } var gibbedEvent = new EntityGibbedEvent(gibbable, new List {gibbable}); RaiseLocalEvent(gibbable, ref gibbedEvent); } private List GibEntity(Entity gibbable, TransformComponent parentXform, float randomSpreadMod, ref HashSet droppedEntities, bool flingEntity, Vector2? scatterDirection, float scatterImpulse, float scatterImpulseVariance, Angle scatterCone, bool deleteTarget = true) { var localGibs = new List(); var gibCount = 0; var gibProtoCount = 0; if (Resolve(gibbable, ref gibbable.Comp, logMissing: false)) { gibCount = gibbable.Comp.GibCount; gibProtoCount = gibbable.Comp.GibPrototypes.Count; } var gibAttemptEvent = new AttemptEntityGibEvent(gibbable, gibCount, GibType.Drop); RaiseLocalEvent(gibbable, ref gibAttemptEvent); switch (gibAttemptEvent.GibType) { case GibType.Skip: return localGibs; case GibType.Drop: DropEntity(gibbable, parentXform, randomSpreadMod, ref droppedEntities, flingEntity, scatterDirection, scatterImpulse, scatterImpulseVariance, scatterCone); localGibs.Add(gibbable); return localGibs; } if (gibbable.Comp != null && gibProtoCount > 0) { if (flingEntity) { for (var i = 0; i < gibAttemptEvent.GibletCount; i++) { if (!TryCreateRandomGiblet(gibbable.Comp, parentXform.Coordinates, false, out var giblet, randomSpreadMod)) continue; FlingDroppedEntity(giblet.Value, scatterDirection, scatterImpulse, scatterImpulseVariance, scatterCone); droppedEntities.Add(giblet.Value); } } else { for (var i = 0; i < gibAttemptEvent.GibletCount; i++) { if (TryCreateRandomGiblet(gibbable.Comp, parentXform.Coordinates, false, out var giblet, randomSpreadMod)) droppedEntities.Add(giblet.Value); } } } _transformSystem.AttachToGridOrMap(gibbable, Transform(gibbable)); if (flingEntity) { FlingDroppedEntity(gibbable, scatterDirection, scatterImpulse, scatterImpulseVariance, scatterCone); } var gibbedEvent = new EntityGibbedEvent(gibbable, localGibs); RaiseLocalEvent(gibbable, ref gibbedEvent); if (deleteTarget) QueueDel(gibbable); return localGibs; } public bool TryCreateRandomGiblet(Entity gibbable, [NotNullWhen(true)] out EntityUid? gibletEntity, float randomSpreadModifier = 1.0f, bool playSound = true) { gibletEntity = null; return Resolve(gibbable, ref gibbable.Comp) && TryCreateRandomGiblet(gibbable.Comp, Transform(gibbable).Coordinates, playSound, out gibletEntity, randomSpreadModifier); } public bool TryCreateAndFlingRandomGiblet(Entity gibbable, [NotNullWhen(true)] out EntityUid? gibletEntity, Vector2 scatterDirection, float force, float scatterImpulseVariance, Angle scatterCone = default, bool playSound = true) { gibletEntity = null; if (!Resolve(gibbable, ref gibbable.Comp) || !TryCreateRandomGiblet(gibbable.Comp, Transform(gibbable).Coordinates, playSound, out gibletEntity)) return false; FlingDroppedEntity(gibletEntity.Value, scatterDirection, force, scatterImpulseVariance, scatterCone); return true; } private void FlingDroppedEntity(EntityUid target, Vector2? direction, float impulse, float impulseVariance, Angle scatterConeAngle) { var scatterAngle = direction?.ToAngle() ?? _random.NextAngle(); var scatterVector = _random.NextAngle(scatterAngle - scatterConeAngle / 2, scatterAngle + scatterConeAngle / 2) .ToVec() * (impulse + _random.NextFloat(impulseVariance)); _physicsSystem.ApplyLinearImpulse(target, scatterVector); } private bool TryCreateRandomGiblet(GibbableComponent gibbable, EntityCoordinates coords, bool playSound, [NotNullWhen(true)] out EntityUid? gibletEntity, float? randomSpreadModifier = null) { gibletEntity = null; if (gibbable.GibPrototypes.Count == 0) return false; gibletEntity = Spawn(gibbable.GibPrototypes[_random.Next(0, gibbable.GibPrototypes.Count)], randomSpreadModifier == null ? coords : coords.Offset(_random.NextVector2(gibbable.GibScatterRange * randomSpreadModifier.Value))); if (playSound) _audioSystem.PlayPredicted(gibbable.GibSound, coords, null); _transformSystem.SetWorldRotation(gibletEntity.Value, _random.NextAngle()); return true; } }