using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Content.Server.Atmos.EntitySystems; using Content.Server.Chemistry.EntitySystems; using Content.Server.DoAfter; using Content.Server.Popups; using Content.Server.Tools.Components; using Content.Shared.ActionBlocker; using Content.Shared.Audio; using Robust.Shared.Audio; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Prototypes; namespace Content.Server.Tools { public partial class ToolSystem : EntitySystem { [Dependency] private readonly ITileDefinitionManager _tileDefinitionManager = default!; [Dependency] private readonly IMapManager _mapManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; [Dependency] private readonly DoAfterSystem _doAfterSystem = default!; [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!; [Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; public override void Initialize() { base.Initialize(); InitializeTilePrying(); InitializeWelders(); InitializeMultipleTools(); SubscribeLocalEvent(OnDoAfterComplete); SubscribeLocalEvent(OnDoAfterCancelled); } private void OnDoAfterComplete(ToolDoAfterComplete ev) { // Actually finish the tool use! Depending on whether that succeeds or not, either event will be broadcast. if(ToolFinishUse(ev.Uid, ev.UserUid, ev.Fuel)) { if (ev.EventTarget != null) RaiseLocalEvent(ev.EventTarget.Value, ev.CompletedEvent, false); else RaiseLocalEvent(ev.CompletedEvent); } else if(ev.CancelledEvent != null) { if (ev.EventTarget != null) RaiseLocalEvent(ev.EventTarget.Value, ev.CancelledEvent, false); else RaiseLocalEvent(ev.CancelledEvent); } } private void OnDoAfterCancelled(ToolDoAfterCancelled ev) { if (ev.EventTarget != null) RaiseLocalEvent(ev.EventTarget.Value, ev.Event, false); else RaiseLocalEvent(ev.Event); } /// /// Whether a tool entity has the specified quality or not. /// public bool HasQuality(EntityUid uid, string quality, ToolComponent? tool = null) { return Resolve(uid, ref tool, false) && tool.Qualities.Contains(quality); } /// /// Whether a tool entity has all specified qualities or not. /// public bool HasAllQualities(EntityUid uid, IEnumerable qualities, ToolComponent? tool = null) { return Resolve(uid, ref tool, false) && tool.Qualities.ContainsAll(qualities); } /// /// Sync version of UseTool. /// /// The tool entity. /// The entity using the tool. /// Optionally, a target to use the tool on. /// An optional amount of fuel or energy to consume- /// A doAfter delay in seconds. /// The tool qualities needed to use the tool. /// An event to raise once the doAfter is completed successfully. /// An event to raise once the doAfter is canceled. /// Where to direct the do-after events. If null, events are broadcast /// An optional check to perform for the doAfter. /// The tool component. /// Token to provide to do_after for cancelling /// Whether initially, using the tool succeeded. If there's a doAfter delay, you'll need to listen to /// the and being broadcast /// to see whether using the tool succeeded or not. If the is zero, /// this simply returns whether using the tool succeeded or not. public bool UseTool( EntityUid tool, EntityUid user, EntityUid? target, float fuel, float doAfterDelay, IEnumerable toolQualitiesNeeded, object? doAfterCompleteEvent = null, object? doAfterCancelledEvent = null, EntityUid? doAfterEventTarget = null, Func? doAfterCheck = null, ToolComponent? toolComponent = null, CancellationToken? cancelToken = null) { // No logging here, after all that'd mean the caller would need to check if the component is there or not. if (!Resolve(tool, ref toolComponent, false)) return false; if (!ToolStartUse(tool, user, fuel, toolQualitiesNeeded, toolComponent)) return false; if (doAfterDelay > 0f) { var doAfterArgs = new DoAfterEventArgs(user, doAfterDelay / toolComponent.SpeedModifier, cancelToken ?? default, target) { ExtraCheck = doAfterCheck, BreakOnDamage = true, BreakOnStun = true, BreakOnTargetMove = true, BreakOnUserMove = true, NeedHand = true, BroadcastFinishedEvent = doAfterCompleteEvent != null ? new ToolDoAfterComplete(doAfterCompleteEvent, doAfterCancelledEvent, tool, user, fuel, doAfterEventTarget) : null, BroadcastCancelledEvent = doAfterCancelledEvent != null ? new ToolDoAfterCancelled(doAfterCancelledEvent, doAfterEventTarget) : null, }; _doAfterSystem.DoAfter(doAfterArgs); return true; } return ToolFinishUse(tool, user, fuel, toolComponent); } // This is hilariously long. /// public bool UseTool(EntityUid tool, EntityUid user, EntityUid? target, float fuel, float doAfterDelay, string toolQualityNeeded, object doAfterCompleteEvent, object doAfterCancelledEvent, EntityUid? doAfterEventTarget = null, Func? doAfterCheck = null, ToolComponent? toolComponent = null) { return UseTool(tool, user, target, fuel, doAfterDelay, new[] { toolQualityNeeded }, doAfterCompleteEvent, doAfterCancelledEvent, doAfterEventTarget, doAfterCheck, toolComponent); } /// /// Async version of UseTool. /// /// The tool entity. /// The entity using the tool. /// Optionally, a target to use the tool on. /// An optional amount of fuel or energy to consume- /// A doAfter delay in seconds. /// The tool qualities needed to use the tool. /// An optional check to perform for the doAfter. /// The tool component. /// Whether using the tool succeeded or not. public async Task UseTool(EntityUid tool, EntityUid user, EntityUid? target, float fuel, float doAfterDelay, IEnumerable toolQualitiesNeeded, Func? doAfterCheck = null, ToolComponent? toolComponent = null) { // No logging here, after all that'd mean the caller would need to check if the component is there or not. if (!Resolve(tool, ref toolComponent, false)) return false; if (!ToolStartUse(tool, user, fuel, toolQualitiesNeeded, toolComponent)) return false; if (doAfterDelay > 0f) { var doAfterArgs = new DoAfterEventArgs(user, doAfterDelay / toolComponent.SpeedModifier, default, target) { ExtraCheck = doAfterCheck, BreakOnDamage = true, BreakOnStun = true, BreakOnTargetMove = true, BreakOnUserMove = true, NeedHand = true, }; var result = await _doAfterSystem.WaitDoAfter(doAfterArgs); if (result == DoAfterStatus.Cancelled) return false; } return ToolFinishUse(tool, user, fuel, toolComponent); } // This is hilariously long. /// public Task UseTool(EntityUid tool, EntityUid user, EntityUid? target, float fuel, float doAfterDelay, string toolQualityNeeded, Func? doAfterCheck = null, ToolComponent? toolComponent = null) { return UseTool(tool, user, target, fuel, doAfterDelay, new [] {toolQualityNeeded}, doAfterCheck, toolComponent); } private bool ToolStartUse(EntityUid tool, EntityUid user, float fuel, IEnumerable toolQualitiesNeeded, ToolComponent? toolComponent = null) { if (!Resolve(tool, ref toolComponent)) return false; if (!toolComponent.Qualities.ContainsAll(toolQualitiesNeeded) || !_actionBlockerSystem.CanInteract(user)) return false; var beforeAttempt = new ToolUseAttemptEvent(fuel, user); RaiseLocalEvent(tool, beforeAttempt, false); return !beforeAttempt.Cancelled; } private bool ToolFinishUse(EntityUid tool, EntityUid user, float fuel, ToolComponent? toolComponent = null) { if (!Resolve(tool, ref toolComponent)) return false; var afterAttempt = new ToolUseFinishAttemptEvent(fuel, user); RaiseLocalEvent(tool, afterAttempt, false); if (afterAttempt.Cancelled) return false; if (toolComponent.UseSound != null) PlayToolSound(tool, toolComponent); return true; } public void PlayToolSound(EntityUid uid, ToolComponent? tool = null) { if (!Resolve(uid, ref tool)) return; if (tool.UseSound is not {} sound) return; // Pass tool.Owner to Filter.Pvs to avoid a TryGetEntity call. SoundSystem.Play(Filter.Pvs(tool.Owner), sound.GetSound(), uid, AudioHelpers.WithVariation(0.175f).WithVolume(-5f)); } public override void Update(float frameTime) { base.Update(frameTime); UpdateWelders(frameTime); } private class ToolDoAfterComplete : EntityEventArgs { public readonly object CompletedEvent; public readonly object? CancelledEvent; public readonly EntityUid Uid; public readonly EntityUid UserUid; public readonly float Fuel; public readonly EntityUid? EventTarget; public ToolDoAfterComplete(object completedEvent, object? cancelledEvent, EntityUid uid, EntityUid userUid, float fuel, EntityUid? eventTarget = null) { CompletedEvent = completedEvent; Uid = uid; UserUid = userUid; Fuel = fuel; CancelledEvent = cancelledEvent; EventTarget = eventTarget; } } private class ToolDoAfterCancelled : EntityEventArgs { public readonly object Event; public readonly EntityUid? EventTarget; public ToolDoAfterCancelled(object @event, EntityUid? eventTarget = null) { Event = @event; EventTarget = eventTarget; } } } /// /// Attempt event called *before* any do afters to see if the tool usage should succeed or not. /// You can change the fuel consumption by changing the Fuel property. /// public class ToolUseAttemptEvent : CancellableEntityEventArgs { public float Fuel { get; set; } public EntityUid User { get; } public ToolUseAttemptEvent(float fuel, EntityUid user) { Fuel = fuel; User = user; } } /// /// Attempt event called *after* any do afters to see if the tool usage should succeed or not. /// You can use this event to consume any fuel needed. /// public class ToolUseFinishAttemptEvent : CancellableEntityEventArgs { public float Fuel { get; } public EntityUid User { get; } public ToolUseFinishAttemptEvent(float fuel, EntityUid user) { Fuel = fuel; } } }