using System.Linq; using Content.Shared.Administration.Logs; using Content.Shared.Audio; using Content.Shared.Body.Components; using Content.Shared.Database; using Content.Shared.Emag.Components; using Content.Shared.Emag.Systems; using Content.Shared.Examine; using Content.Shared.Mobs.Components; using Content.Shared.Stacks; using Content.Shared.Whitelist; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Map; using Robust.Shared.Physics.Events; using Robust.Shared.Timing; namespace Content.Shared.Materials; /// /// Handles interactions and logic related to , /// , and . /// public abstract class SharedMaterialReclaimerSystem : EntitySystem { [Dependency] private readonly ISharedAdminLogManager _adminLog = default!; [Dependency] protected readonly IGameTiming Timing = default!; [Dependency] protected readonly SharedAmbientSoundSystem AmbientSound = default!; [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] protected readonly SharedContainerSystem Container = default!; [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; [Dependency] private readonly EmagSystem _emag = default!; public const string ActiveReclaimerContainerId = "active-material-reclaimer-container"; /// public override void Initialize() { SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnEmagged); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnCollide); SubscribeLocalEvent(OnActiveStartup); } private void OnMapInit(EntityUid uid, MaterialReclaimerComponent component, MapInitEvent args) { component.NextSound = Timing.CurTime; } private void OnShutdown(EntityUid uid, MaterialReclaimerComponent component, ComponentShutdown args) { _audio.Stop(component.Stream); } private void OnExamined(EntityUid uid, MaterialReclaimerComponent component, ExaminedEvent args) { args.PushMarkup(Loc.GetString("recycler-count-items", ("items", component.ItemsProcessed))); } private void OnEmagged(EntityUid uid, MaterialReclaimerComponent component, ref GotEmaggedEvent args) { if (!_emag.CompareFlag(args.Type, EmagType.Interaction)) return; if (_emag.CheckFlag(uid, EmagType.Interaction)) return; args.Handled = true; } private void OnCollide(EntityUid uid, CollideMaterialReclaimerComponent component, ref StartCollideEvent args) { if (args.OurFixtureId != component.FixtureId) return; if (!TryComp(uid, out var reclaimer)) return; TryStartProcessItem(uid, args.OtherEntity, reclaimer); } private void OnActiveStartup(EntityUid uid, ActiveMaterialReclaimerComponent component, ComponentStartup args) { component.ReclaimingContainer = Container.EnsureContainer(uid, ActiveReclaimerContainerId); } /// /// Tries to start processing an item via a . /// public bool TryStartProcessItem(EntityUid uid, EntityUid item, MaterialReclaimerComponent? component = null, EntityUid? user = null) { if (!Resolve(uid, ref component)) return false; if (!CanStart(uid, component)) return false; if (HasComp(item) && !CanGib(uid, item, component)) // whitelist? We be gibbing, boy! return false; if (_whitelistSystem.IsWhitelistFail(component.Whitelist, item) || _whitelistSystem.IsBlacklistPass(component.Blacklist, item)) return false; if (Container.TryGetContainingContainer((item, null, null), out _) && !Container.TryRemoveFromContainer(item)) return false; if (user != null) { _adminLog.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(user.Value):player} destroyed {ToPrettyString(item)} in the material reclaimer, {ToPrettyString(uid)}"); } if (Timing.CurTime > component.NextSound) { component.Stream = _audio.PlayPredicted(component.Sound, uid, user)?.Entity; component.NextSound = Timing.CurTime + component.SoundCooldown; } var reclaimedEvent = new GotReclaimedEvent(Transform(uid).Coordinates); RaiseLocalEvent(item, ref reclaimedEvent); var duration = GetReclaimingDuration(uid, item, component); // if it's instant, don't bother with all the active comp stuff. if (duration == TimeSpan.Zero) { Reclaim(uid, item, 1, component); return true; } var active = EnsureComp(uid); active.Duration = duration; active.EndTime = Timing.CurTime + duration; Container.Insert(item, active.ReclaimingContainer); return true; } /// /// Finishes processing an item, freeing up the the reclaimer. /// /// /// This doesn't reclaim the entity itself, but rather ends the formal /// process started with . /// The actual reclaiming happens in /// public virtual bool TryFinishProcessItem(EntityUid uid, MaterialReclaimerComponent? component = null, ActiveMaterialReclaimerComponent? active = null) { if (!Resolve(uid, ref component, ref active, false)) return false; RemCompDeferred(uid, active); return true; } /// /// Spawns the materials and chemicals associated /// with an entity. Also deletes the item. /// public virtual void Reclaim(EntityUid uid, EntityUid item, float completion = 1f, MaterialReclaimerComponent? component = null) { if (!Resolve(uid, ref component)) return; component.ItemsProcessed++; if (component.CutOffSound) { _audio.Stop(component.Stream); } Dirty(uid, component); } /// /// Sets the Enabled field on the reclaimer. /// public bool SetReclaimerEnabled(EntityUid uid, bool enabled, MaterialReclaimerComponent? component = null) { if (!Resolve(uid, ref component, false)) return true; if (component.Broken && enabled) return false; component.Enabled = enabled; AmbientSound.SetAmbience(uid, enabled && component.Powered); Dirty(uid, component); return true; } /// /// Whether or not the specified reclaimer can currently /// begin reclaiming another entity. /// public bool CanStart(EntityUid uid, MaterialReclaimerComponent component) { if (HasComp(uid)) return false; return component.Powered && component.Enabled && !component.Broken; } /// /// Whether or not the reclaimer satisfies the conditions /// allowing it to gib/reclaim a living creature. /// public bool CanGib(EntityUid uid, EntityUid victim, MaterialReclaimerComponent component) { return component.Powered && component.Enabled && !component.Broken && HasComp(victim) && _emag.CheckFlag(uid, EmagType.Interaction); } /// /// Gets the duration of processing a specified entity. /// Processing is calculated from the sum of the materials within the entity. /// It does not regard the chemicals within it. /// public TimeSpan GetReclaimingDuration(EntityUid reclaimer, EntityUid item, MaterialReclaimerComponent? reclaimerComponent = null, PhysicalCompositionComponent? compositionComponent = null) { if (!Resolve(reclaimer, ref reclaimerComponent)) return TimeSpan.Zero; if (!reclaimerComponent.ScaleProcessSpeed || !Resolve(item, ref compositionComponent, false)) return reclaimerComponent.MinimumProcessDuration; var materialSum = compositionComponent.MaterialComposition.Values.Sum(); materialSum *= CompOrNull(item)?.Count ?? 1; var duration = TimeSpan.FromSeconds(materialSum / reclaimerComponent.MaterialProcessRate); if (duration < reclaimerComponent.MinimumProcessDuration) duration = reclaimerComponent.MinimumProcessDuration; return duration; } /// public override void Update(float frameTime) { base.Update(frameTime); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var active, out var reclaimer)) { if (Timing.CurTime < active.EndTime) continue; TryFinishProcessItem(uid, reclaimer, active); } } } [ByRefEvent] public record struct GotReclaimedEvent(EntityCoordinates ReclaimerCoordinates);