using Content.Server.DoAfter; using Content.Server.Popups; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Audio; using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.Database; using Content.Shared.DoAfter; using Content.Shared.Examine; using Content.Shared.FixedPoint; using Content.Shared.Fluids; using Content.Shared.Fluids.Components; using Content.Shared.Interaction; using Content.Shared.Tag; using Content.Shared.Verbs; using Robust.Shared.Audio.Systems; using Robust.Shared.Prototypes; using Robust.Shared.Random; using Robust.Shared.Utility; namespace Content.Server.Fluids.EntitySystems; public sealed class DrainSystem : SharedDrainSystem { [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; [Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!; [Dependency] private readonly SharedAudioSystem _audioSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly TagSystem _tagSystem = default!; [Dependency] private readonly DoAfterSystem _doAfterSystem = default!; [Dependency] private readonly PuddleSystem _puddleSystem = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; private readonly HashSet> _puddles = new(); public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnDrainMapInit); SubscribeLocalEvent>(AddEmptyVerb); SubscribeLocalEvent(OnExamined); SubscribeLocalEvent(OnInteract); SubscribeLocalEvent(OnDoAfter); } private void OnDrainMapInit(Entity ent, ref MapInitEvent args) { // Randomise puddle drains so roundstart ones don't all dump at the same time. ent.Comp.Accumulator = _random.NextFloat(ent.Comp.DrainFrequency); } private void AddEmptyVerb(Entity entity, ref GetVerbsEvent args) { if (!args.CanAccess || !args.CanInteract || args.Using == null) return; if (!TryComp(args.Using, out SpillableComponent? spillable) || !TryComp(args.Target, out DrainComponent? drain)) return; var used = args.Using.Value; var target = args.Target; Verb verb = new() { Text = Loc.GetString("drain-component-empty-verb-inhand", ("object", Name(used))), Act = () => { Empty(used, spillable, target, drain); }, Impact = LogImpact.Low, Icon = new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/VerbIcons/eject.svg.192dpi.png")) }; args.Verbs.Add(verb); } private void Empty(EntityUid container, SpillableComponent spillable, EntityUid target, DrainComponent drain) { // Find the solution in the container that is emptied if (!_solutionContainerSystem.TryGetDrainableSolution(container, out var containerSoln, out var containerSolution) || containerSolution.Volume == FixedPoint2.Zero) { _popupSystem.PopupEntity( Loc.GetString("drain-component-empty-verb-using-is-empty-message", ("object", container)), container); return; } // try to find the drain's solution if (!_solutionContainerSystem.ResolveSolution(target, DrainComponent.SolutionName, ref drain.Solution, out var drainSolution)) { return; } // Try to transfer as much solution as possible to the drain var amountToPutInDrain = drainSolution.AvailableVolume; var amountToSpillOnGround = containerSolution.Volume - drainSolution.AvailableVolume; if (amountToPutInDrain > 0) { var solutionToPutInDrain = _solutionContainerSystem.SplitSolution(containerSoln.Value, amountToPutInDrain); _solutionContainerSystem.TryAddSolution(drain.Solution.Value, solutionToPutInDrain); _audioSystem.PlayPvs(drain.ManualDrainSound, target); _ambientSoundSystem.SetAmbience(target, true); } // Spill the remainder. if (amountToSpillOnGround > 0) { var solutionToSpill = _solutionContainerSystem.SplitSolution(containerSoln.Value, amountToSpillOnGround); _puddleSystem.TrySpillAt(Transform(target).Coordinates, solutionToSpill, out _); _popupSystem.PopupEntity( Loc.GetString("drain-component-empty-verb-target-is-full-message", ("object", target)), container); } } public override void Update(float frameTime) { base.Update(frameTime); var managerQuery = GetEntityQuery(); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var drain)) { drain.Accumulator += frameTime; if (drain.Accumulator < drain.DrainFrequency) { continue; } drain.Accumulator -= drain.DrainFrequency; if (!managerQuery.TryGetComponent(uid, out var manager)) continue; // Best to do this one every second rather than once every tick... if (!_solutionContainerSystem.ResolveSolution((uid, manager), DrainComponent.SolutionName, ref drain.Solution, out var drainSolution)) continue; if (drainSolution.Volume <= 0 && !drain.AutoDrain) { _ambientSoundSystem.SetAmbience(uid, false); continue; } // Remove a bit from the buffer _solutionContainerSystem.SplitSolution(drain.Solution.Value, (drain.UnitsDestroyedPerSecond * drain.DrainFrequency)); // This will ensure that UnitsPerSecond is per second... var amount = drain.UnitsPerSecond * drain.DrainFrequency; if (drain.AutoDrain) { _puddles.Clear(); _lookup.GetEntitiesInRange(Transform(uid).Coordinates, drain.Range, _puddles); if (_puddles.Count == 0 && drainSolution.Volume <= 0) { _ambientSoundSystem.SetAmbience(uid, false); continue; } _ambientSoundSystem.SetAmbience(uid, true); amount /= _puddles.Count; foreach (var puddle in _puddles) { // Queue the solution deletion if it's empty. EvaporationSystem might also do this // but queuedelete should be pretty safe. if (!_solutionContainerSystem.ResolveSolution(puddle.Owner, puddle.Comp.SolutionName, ref puddle.Comp.Solution, out var puddleSolution)) { QueueDel(puddle); continue; } // Removes the lowest of: // the drain component's units per second adjusted for # of puddles // the puddle's remaining volume (making it cleanly zero) // the drain's remaining volume in its buffer. var transferSolution = _solutionContainerSystem.SplitSolution(puddle.Comp.Solution.Value, FixedPoint2.Min(FixedPoint2.New(amount), puddleSolution.Volume, drainSolution.AvailableVolume)); drainSolution.AddSolution(transferSolution, _prototypeManager); if (puddleSolution.Volume <= 0) { QueueDel(puddle); } } } _solutionContainerSystem.UpdateChemicals(drain.Solution.Value); } } private void OnExamined(Entity entity, ref ExaminedEvent args) { if (!args.IsInDetailsRange || !HasComp(entity) || !_solutionContainerSystem.ResolveSolution(entity.Owner, DrainComponent.SolutionName, ref entity.Comp.Solution, out var drainSolution)) { return; } var text = drainSolution.AvailableVolume != 0 ? Loc.GetString("drain-component-examine-volume", ("volume", drainSolution.AvailableVolume)) : Loc.GetString("drain-component-examine-hint-full"); args.PushMarkup(text); } private void OnInteract(Entity entity, ref AfterInteractUsingEvent args) { if (!args.CanReach || args.Target == null || !_tagSystem.HasTag(args.Used, DrainComponent.PlungerTag) || !_solutionContainerSystem.ResolveSolution(args.Target.Value, DrainComponent.SolutionName, ref entity.Comp.Solution, out var drainSolution)) { return; } if (drainSolution.AvailableVolume > 0) { _popupSystem.PopupEntity(Loc.GetString("drain-component-unclog-notapplicable", ("object", args.Target.Value)), args.Target.Value); return; } _audioSystem.PlayPvs(entity.Comp.PlungerSound, entity); var doAfterArgs = new DoAfterArgs(EntityManager, args.User, entity.Comp.UnclogDuration, new DrainDoAfterEvent(), entity, args.Target, args.Used) { BreakOnDamage = true, BreakOnMove = true, BreakOnHandChange = true }; _doAfterSystem.TryStartDoAfter(doAfterArgs); } private void OnDoAfter(Entity entity, ref DrainDoAfterEvent args) { if (args.Target == null) return; if (!_random.Prob(entity.Comp.UnclogProbability)) { _popupSystem.PopupEntity(Loc.GetString("drain-component-unclog-fail", ("object", args.Target.Value)), args.Target.Value); return; } if (!_solutionContainerSystem.ResolveSolution(args.Target.Value, DrainComponent.SolutionName, ref entity.Comp.Solution)) { return; } _solutionContainerSystem.RemoveAllSolution(entity.Comp.Solution.Value); _audioSystem.PlayPvs(entity.Comp.UnclogSound, args.Target.Value); _popupSystem.PopupEntity(Loc.GetString("drain-component-unclog-success", ("object", args.Target.Value)), args.Target.Value); } }