using Content.Shared.Administration.Logs; using Content.Shared.Body.Systems; using Content.Shared.Damage.Systems; using Content.Shared.Database; using Content.Shared.Destructible; using Content.Shared.DoAfter; using Content.Shared.DragDrop; using Content.Shared.Examine; using Content.Shared.Hands; using Content.Shared.Humanoid; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Inventory.Events; using Content.Shared.Item; using Content.Shared.Kitchen.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Movement.Events; using Content.Shared.Nutrition.Components; using Content.Shared.Popups; using Content.Shared.Random.Helpers; using Content.Shared.Throwing; using Content.Shared.Verbs; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Serialization; using Robust.Shared.Timing; namespace Content.Shared.Kitchen; /// /// Used to butcher some entities like monkeys. /// public sealed class SharedKitchenSpikeSystem : EntitySystem { [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly ISharedAdminLogManager _logger = default!; [Dependency] private readonly DamageableSystem _damageableSystem = default!; [Dependency] private readonly ExamineSystemShared _examineSystem = default!; [Dependency] private readonly MetaDataSystem _metaDataSystem = default!; [Dependency] private readonly MobStateSystem _mobStateSystem = default!; [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!; [Dependency] private readonly SharedAudioSystem _audioSystem = default!; [Dependency] private readonly SharedBodySystem _bodySystem = default!; [Dependency] private readonly SharedContainerSystem _containerSystem = default!; [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; [Dependency] private readonly SharedInteractionSystem _interaction = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnInit); SubscribeLocalEvent(OnInsertAttempt); SubscribeLocalEvent(OnEntInsertedIntoContainer); SubscribeLocalEvent(OnEntRemovedFromContainer); SubscribeLocalEvent(OnInteractHand); SubscribeLocalEvent(OnInteractUsing); SubscribeLocalEvent(OnCanDrop); SubscribeLocalEvent(OnDragDrop); SubscribeLocalEvent(OnSpikeHookDoAfter); SubscribeLocalEvent(OnSpikeUnhookDoAfter); SubscribeLocalEvent(OnSpikeButcherDoAfter); SubscribeLocalEvent(OnSpikeExamined); SubscribeLocalEvent>(OnGetVerbs); SubscribeLocalEvent(OnDestruction); SubscribeLocalEvent(OnVictimExamined); // Prevent the victim from doing anything while on the spike. SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnAttempt); SubscribeLocalEvent(OnAttempt); // Container Jank SubscribeLocalEvent(OnAccessibleOverride); } private void OnInit(Entity ent, ref ComponentInit args) { ent.Comp.BodyContainer = _containerSystem.EnsureContainer(ent, ent.Comp.ContainerId); } private void OnInsertAttempt(Entity ent, ref ContainerIsInsertingAttemptEvent args) { if (args.Cancelled || TryComp(args.EntityUid, out var butcherable) && butcherable.Type == ButcheringType.Spike) return; args.Cancel(); } private void OnEntInsertedIntoContainer(Entity ent, ref EntInsertedIntoContainerMessage args) { if (_gameTiming.ApplyingState) return; EnsureComp(args.Entity); _damageableSystem.TryChangeDamage(args.Entity, ent.Comp.SpikeDamage, true); ent.Comp.NextDamage = _gameTiming.CurTime + ent.Comp.DamageInterval; Dirty(ent); // TODO: Add sprites for different species. _appearanceSystem.SetData(ent.Owner, KitchenSpikeVisuals.Status, KitchenSpikeStatus.Bloody); } private void OnEntRemovedFromContainer(Entity ent, ref EntRemovedFromContainerMessage args) { if (_gameTiming.ApplyingState) return; RemComp(args.Entity); _damageableSystem.TryChangeDamage(args.Entity, ent.Comp.SpikeDamage, true); _appearanceSystem.SetData(ent.Owner, KitchenSpikeVisuals.Status, KitchenSpikeStatus.Empty); } private void OnInteractHand(Entity ent, ref InteractHandEvent args) { var victim = ent.Comp.BodyContainer.ContainedEntity; if (args.Handled || !victim.HasValue) return; _popupSystem.PopupClient(Loc.GetString("butcherable-need-knife", ("target", Identity.Entity(victim.Value, EntityManager))), ent, args.User, PopupType.Medium); args.Handled = true; } private void OnInteractUsing(Entity ent, ref InteractUsingEvent args) { var victim = ent.Comp.BodyContainer.ContainedEntity; if (args.Handled || !TryComp(victim, out var butcherable) || butcherable.SpawnedEntities.Count == 0) return; args.Handled = true; if (!TryComp(args.Used, out var sharp)) { _popupSystem.PopupClient(Loc.GetString("butcherable-need-knife", ("target", Identity.Entity(victim.Value, EntityManager))), ent, args.User, PopupType.Medium); return; } var victimIdentity = Identity.Entity(victim.Value, EntityManager); _popupSystem.PopupPredicted(Loc.GetString("comp-kitchen-spike-begin-butcher-self", ("victim", victimIdentity)), Loc.GetString("comp-kitchen-spike-begin-butcher", ("user", Identity.Entity(args.User, EntityManager)), ("victim", victimIdentity)), ent, args.User, PopupType.MediumCaution); var delay = TimeSpan.FromSeconds(sharp.ButcherDelayModifier * butcherable.ButcherDelay); if (_mobStateSystem.IsAlive(victim.Value)) delay += ent.Comp.ButcherDelayAlive; else delay *= ent.Comp.ButcherModifierDead; _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, delay, new SpikeButcherDoAfterEvent(), ent, target: victim, used: args.Used) { BreakOnDamage = true, BreakOnMove = true, NeedHand = true, }); } private void OnCanDrop(Entity ent, ref CanDropTargetEvent args) { if (args.Handled) return; args.CanDrop = _containerSystem.CanInsert(args.Dragged, ent.Comp.BodyContainer); args.Handled = true; } private void OnDragDrop(Entity ent, ref DragDropTargetEvent args) { if (args.Handled) return; ShowPopups("comp-kitchen-spike-begin-hook-self", "comp-kitchen-spike-begin-hook-self-other", "comp-kitchen-spike-begin-hook-other-self", "comp-kitchen-spike-begin-hook-other", args.User, args.Dragged, ent); _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, ent.Comp.HookDelay, new SpikeHookDoAfterEvent(), ent, target: args.Dragged) { BreakOnDamage = true, BreakOnMove = true, NeedHand = true, }); args.Handled = true; } private void OnSpikeHookDoAfter(Entity ent, ref SpikeHookDoAfterEvent args) { if (args.Handled || args.Cancelled || !args.Target.HasValue) return; if (_containerSystem.Insert(args.Target.Value, ent.Comp.BodyContainer)) { ShowPopups("comp-kitchen-spike-hook-self", "comp-kitchen-spike-hook-self-other", "comp-kitchen-spike-hook-other-self", "comp-kitchen-spike-hook-other", args.User, args.Target.Value, ent); // normally medium severity, but for humanoids high severity, so new players get relay'd to admin alerts. var logSeverity = HasComp(args.Target) ? LogImpact.High : LogImpact.Medium; _logger.Add(LogType.Action, logSeverity, $"{ToPrettyString(args.User):user} put {ToPrettyString(args.Target):target} on the {ToPrettyString(ent):spike}"); _audioSystem.PlayPredicted(ent.Comp.SpikeSound, ent, args.User); } args.Handled = true; } private void OnSpikeUnhookDoAfter(Entity ent, ref SpikeUnhookDoAfterEvent args) { if (args.Handled || args.Cancelled || !args.Target.HasValue) return; if (_containerSystem.Remove(args.Target.Value, ent.Comp.BodyContainer)) { ShowPopups("comp-kitchen-spike-unhook-self", "comp-kitchen-spike-unhook-self-other", "comp-kitchen-spike-unhook-other-self", "comp-kitchen-spike-unhook-other", args.User, args.Target.Value, ent); _logger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User):user} took {ToPrettyString(args.Target):target} off the {ToPrettyString(ent):spike}"); _audioSystem.PlayPredicted(ent.Comp.SpikeSound, ent, args.User); } args.Handled = true; } private void OnSpikeButcherDoAfter(Entity ent, ref SpikeButcherDoAfterEvent args) { if (args.Handled || args.Cancelled || !args.Target.HasValue || !args.Used.HasValue || !TryComp(args.Target, out var butcherable)) return; var victimIdentity = Identity.Entity(args.Target.Value, EntityManager); _popupSystem.PopupPredicted(Loc.GetString("comp-kitchen-spike-butcher-self", ("victim", victimIdentity)), Loc.GetString("comp-kitchen-spike-butcher", ("user", Identity.Entity(args.User, EntityManager)), ("victim", victimIdentity)), ent, args.User, PopupType.MediumCaution); // Get a random entry to spawn. // TODO: Replace with RandomPredicted once the engine PR is merged var seed = SharedRandomExtensions.HashCodeCombine((int)_gameTiming.CurTick.Value, GetNetEntity(ent).Id); var rand = new System.Random(seed); var index = rand.Next(butcherable.SpawnedEntities.Count); var entry = butcherable.SpawnedEntities[index]; var uid = PredictedSpawnNextToOrDrop(entry.PrototypeId, ent); _metaDataSystem.SetEntityName(uid, Loc.GetString("comp-kitchen-spike-meat-name", ("name", Name(uid)), ("victim", args.Target))); // Decrease the amount since we spawned an entity from that entry. entry.Amount--; // Remove the entry if its new amount is zero, or update it. if (entry.Amount <= 0) butcherable.SpawnedEntities.RemoveAt(index); else butcherable.SpawnedEntities[index] = entry; Dirty(args.Target.Value, butcherable); // Gib the victim if there is nothing else to butcher. if (butcherable.SpawnedEntities.Count == 0) { _bodySystem.GibBody(args.Target.Value, true); var logSeverity = HasComp(args.Target) ? LogImpact.Extreme : LogImpact.High; _logger.Add(LogType.Gib, logSeverity, $"{ToPrettyString(args.User):user} finished butchering {ToPrettyString(args.Target):target} on the {ToPrettyString(ent):spike}"); } else { EnsureComp(args.Target.Value); _damageableSystem.ChangeDamage(args.Target.Value, ent.Comp.ButcherDamage, true); // Log severity for damaging other entities is normally medium. _logger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(args.User):user} butchered {ToPrettyString(args.Target):target} on the {ToPrettyString(ent):spike}"); } _audioSystem.PlayPredicted(ent.Comp.ButcherSound, ent, args.User); _popupSystem.PopupClient(Loc.GetString("butcherable-knife-butchered-success", ("target", Identity.Entity(args.Target.Value, EntityManager)), ("knife", args.Used.Value)), ent, args.User, PopupType.Medium); args.Handled = true; } private void OnSpikeExamined(Entity ent, ref ExaminedEvent args) { var victim = ent.Comp.BodyContainer.ContainedEntity; if (!victim.HasValue) return; // Show it at the end of the examine so it looks good. args.PushMarkup(Loc.GetString("comp-kitchen-spike-hooked", ("victim", Identity.Entity(victim.Value, EntityManager))), -1); args.PushMessage(_examineSystem.GetExamineText(victim.Value, args.Examiner), -2); } private void OnGetVerbs(Entity ent, ref GetVerbsEvent args) { var victim = ent.Comp.BodyContainer.ContainedEntity; if (!victim.HasValue || !_containerSystem.CanRemove(victim.Value, ent.Comp.BodyContainer)) return; var user = args.User; args.Verbs.Add(new Verb() { Text = Loc.GetString("comp-kitchen-spike-unhook-verb"), Act = () => TryUnhook(ent, user, victim.Value), Impact = LogImpact.Medium, }); } private void OnDestruction(Entity ent, ref DestructionEventArgs args) { _containerSystem.EmptyContainer(ent.Comp.BodyContainer, destination: Transform(ent).Coordinates); } private void OnVictimExamined(Entity ent, ref ExaminedEvent args) { args.PushMarkup(Loc.GetString("comp-kitchen-spike-victim-examine", ("target", Identity.Entity(ent, EntityManager)))); } private static void OnAttempt(EntityUid uid, KitchenSpikeHookedComponent component, CancellableEntityEventArgs args) { args.Cancel(); } private void OnAccessibleOverride(Entity ent, ref AccessibleOverrideEvent args) { // Check if the entity is the target to avoid giving the hooked entity access to everything. // If we already have access we don't need to run more code. if (args.Accessible || args.Target != ent.Owner) return; var xform = Transform(ent); if (!_interaction.CanAccess(args.User, xform.ParentUid)) return; args.Accessible = true; args.Handled = true; } public override void Update(float frameTime) { base.Update(frameTime); var query = AllEntityQuery(); while (query.MoveNext(out var uid, out var kitchenSpike)) { var contained = kitchenSpike.BodyContainer.ContainedEntity; if (!contained.HasValue) continue; if (kitchenSpike.NextDamage > _gameTiming.CurTime) continue; kitchenSpike.NextDamage += kitchenSpike.DamageInterval; Dirty(uid, kitchenSpike); _damageableSystem.ChangeDamage(contained.Value, kitchenSpike.TimeDamage, true); } } /// /// A helper method to show predicted popups that can be targeted towards yourself or somebody else. /// private void ShowPopups(string selfLocMessageSelf, string selfLocMessageOthers, string locMessageSelf, string locMessageOthers, EntityUid user, EntityUid victim, EntityUid hook) { string messageSelf, messageOthers; var victimIdentity = Identity.Entity(victim, EntityManager); if (user == victim) { messageSelf = Loc.GetString(selfLocMessageSelf, ("hook", hook)); messageOthers = Loc.GetString(selfLocMessageOthers, ("victim", victimIdentity), ("hook", hook)); } else { messageSelf = Loc.GetString(locMessageSelf, ("victim", victimIdentity), ("hook", hook)); messageOthers = Loc.GetString(locMessageOthers, ("user", Identity.Entity(user, EntityManager)), ("victim", victimIdentity), ("hook", hook)); } _popupSystem.PopupPredicted(messageSelf, messageOthers, hook, user, PopupType.MediumCaution); } /// /// Tries to unhook the victim. /// private void TryUnhook(Entity ent, EntityUid user, EntityUid target) { ShowPopups("comp-kitchen-spike-begin-unhook-self", "comp-kitchen-spike-begin-unhook-self-other", "comp-kitchen-spike-begin-unhook-other-self", "comp-kitchen-spike-begin-unhook-other", user, target, ent); _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, user, ent.Comp.UnhookDelay, new SpikeUnhookDoAfterEvent(), ent, target: target) { BreakOnDamage = user != target, BreakOnMove = true, }); } } [Serializable, NetSerializable] public sealed partial class SpikeHookDoAfterEvent : SimpleDoAfterEvent; [Serializable, NetSerializable] public sealed partial class SpikeUnhookDoAfterEvent : SimpleDoAfterEvent; [Serializable, NetSerializable] public sealed partial class SpikeButcherDoAfterEvent : SimpleDoAfterEvent;