diff --git a/Content.Shared/Mech/EntitySystem/SharedMechSystem.cs b/Content.Shared/Mech/EntitySystem/SharedMechSystem.cs new file mode 100644 index 00000000000..97242973152 --- /dev/null +++ b/Content.Shared/Mech/EntitySystem/SharedMechSystem.cs @@ -0,0 +1,490 @@ +using System.Linq; +using Content.Shared.Access.Components; +using Content.Shared.ActionBlocker; +using Content.Shared.Actions; +using Content.Shared.Destructible; +using Content.Shared.DoAfter; +using Content.Shared.DragDrop; +using Content.Shared.Emag.Components; +using Content.Shared.Emag.Systems; +using Content.Shared.FixedPoint; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Components; +using Content.Shared.Interaction.Events; +using Content.Shared.Mech.Components; +using Content.Shared.Mech.Equipment.Components; +using Content.Shared.Movement.Components; +using Content.Shared.Movement.Systems; +using Content.Shared.Popups; +using Content.Shared.Weapons.Melee; +using Content.Shared.Weapons.Ranged.Events; +using Content.Shared.Whitelist; +using Robust.Shared.Containers; +using Robust.Shared.Network; +using Robust.Shared.Serialization; +using Robust.Shared.Timing; + +namespace Content.Shared.Mech.EntitySystems; + +/// +/// Handles all of the interactions, UI handling, and items shennanigans for +/// +public abstract class SharedMechSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly SharedAppearanceSystem _appearance = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly SharedInteractionSystem _interaction = default!; + [Dependency] private readonly SharedMoverController _mover = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; + + /// + public override void Initialize() + { + SubscribeLocalEvent(OnToggleEquipmentAction); + SubscribeLocalEvent(OnEjectPilotEvent); + SubscribeLocalEvent(RelayInteractionEvent); + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnDestruction); + SubscribeLocalEvent(OnGetAdditionalAccess); + SubscribeLocalEvent(OnDragDrop); + SubscribeLocalEvent(OnCanDragDrop); + SubscribeLocalEvent(OnEmagged); + + SubscribeLocalEvent(OnGetMeleeWeapon); + SubscribeLocalEvent(OnCanAttackFromContainer); + SubscribeLocalEvent(OnAttackAttempt); + } + + private void OnToggleEquipmentAction(EntityUid uid, MechComponent component, MechToggleEquipmentEvent args) + { + if (args.Handled) + return; + args.Handled = true; + CycleEquipment(uid); + } + + private void OnEjectPilotEvent(EntityUid uid, MechComponent component, MechEjectPilotEvent args) + { + if (args.Handled) + return; + args.Handled = true; + TryEject(uid, component); + } + + private void RelayInteractionEvent(EntityUid uid, MechComponent component, UserActivateInWorldEvent args) + { + var pilot = component.PilotSlot.ContainedEntity; + if (pilot == null) + return; + + // TODO why is this being blocked? + if (!_timing.IsFirstTimePredicted) + return; + + if (component.CurrentSelectedEquipment != null) + { + RaiseLocalEvent(component.CurrentSelectedEquipment.Value, args); + } + } + + private void OnStartup(EntityUid uid, MechComponent component, ComponentStartup args) + { + component.PilotSlot = _container.EnsureContainer(uid, component.PilotSlotId); + component.EquipmentContainer = _container.EnsureContainer(uid, component.EquipmentContainerId); + component.BatterySlot = _container.EnsureContainer(uid, component.BatterySlotId); + UpdateAppearance(uid, component); + } + + private void OnDestruction(EntityUid uid, MechComponent component, DestructionEventArgs args) + { + BreakMech(uid, component); + } + + private void OnGetAdditionalAccess(EntityUid uid, MechComponent component, ref GetAdditionalAccessEvent args) + { + var pilot = component.PilotSlot.ContainedEntity; + if (pilot == null) + return; + + args.Entities.Add(pilot.Value); + } + + private void SetupUser(EntityUid mech, EntityUid pilot, MechComponent? component = null) + { + if (!Resolve(mech, ref component)) + return; + + var rider = EnsureComp(pilot); + + // Warning: this bypasses most normal interaction blocking components on the user, like drone laws and the like. + var irelay = EnsureComp(pilot); + + _mover.SetRelay(pilot, mech); + _interaction.SetRelay(pilot, mech, irelay); + rider.Mech = mech; + Dirty(pilot, rider); + + if (_net.IsClient) + return; + + _actions.AddAction(pilot, ref component.MechCycleActionEntity, component.MechCycleAction, mech); + _actions.AddAction(pilot, ref component.MechUiActionEntity, component.MechUiAction, mech); + _actions.AddAction(pilot, ref component.MechEjectActionEntity, component.MechEjectAction, mech); + } + + private void RemoveUser(EntityUid mech, EntityUid pilot) + { + if (!RemComp(pilot)) + return; + RemComp(pilot); + RemComp(pilot); + + _actions.RemoveProvidedActions(pilot, mech); + } + + /// + /// Destroys the mech, removing the user and ejecting all installed equipment. + /// + /// + /// + public virtual void BreakMech(EntityUid uid, MechComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + TryEject(uid, component); + var equipment = new List(component.EquipmentContainer.ContainedEntities); + foreach (var ent in equipment) + { + RemoveEquipment(uid, ent, component, forced: true); + } + + component.Broken = true; + UpdateAppearance(uid, component); + } + + /// + /// Cycles through the currently selected equipment. + /// + /// + /// + public void CycleEquipment(EntityUid uid, MechComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + var allEquipment = component.EquipmentContainer.ContainedEntities.ToList(); + + var equipmentIndex = -1; + if (component.CurrentSelectedEquipment != null) + { + bool StartIndex(EntityUid u) => u == component.CurrentSelectedEquipment; + equipmentIndex = allEquipment.FindIndex(StartIndex); + } + + equipmentIndex++; + component.CurrentSelectedEquipment = equipmentIndex >= allEquipment.Count + ? null + : allEquipment[equipmentIndex]; + + var popupString = component.CurrentSelectedEquipment != null + ? Loc.GetString("mech-equipment-select-popup", ("item", component.CurrentSelectedEquipment)) + : Loc.GetString("mech-equipment-select-none-popup"); + + if (_net.IsServer) + _popup.PopupEntity(popupString, uid); + + Dirty(uid, component); + } + + /// + /// Inserts an equipment item into the mech. + /// + /// + /// + /// + /// + public void InsertEquipment(EntityUid uid, EntityUid toInsert, MechComponent? component = null, + MechEquipmentComponent? equipmentComponent = null) + { + if (!Resolve(uid, ref component)) + return; + + if (!Resolve(toInsert, ref equipmentComponent)) + return; + + if (component.EquipmentContainer.ContainedEntities.Count >= component.MaxEquipmentAmount) + return; + + if (_whitelistSystem.IsWhitelistFail(component.EquipmentWhitelist, toInsert)) + return; + + equipmentComponent.EquipmentOwner = uid; + _container.Insert(toInsert, component.EquipmentContainer); + var ev = new MechEquipmentInsertedEvent(uid); + RaiseLocalEvent(toInsert, ref ev); + UpdateUserInterface(uid, component); + } + + /// + /// Removes an equipment item from a mech. + /// + /// + /// + /// + /// + /// Whether or not the removal can be cancelled + public void RemoveEquipment(EntityUid uid, EntityUid toRemove, MechComponent? component = null, + MechEquipmentComponent? equipmentComponent = null, bool forced = false) + { + if (!Resolve(uid, ref component)) + return; + + if (!Resolve(toRemove, ref equipmentComponent)) + return; + + if (!forced) + { + var attemptev = new AttemptRemoveMechEquipmentEvent(); + RaiseLocalEvent(toRemove, ref attemptev); + if (attemptev.Cancelled) + return; + } + + var ev = new MechEquipmentRemovedEvent(uid); + RaiseLocalEvent(toRemove, ref ev); + + if (component.CurrentSelectedEquipment == toRemove) + CycleEquipment(uid, component); + + equipmentComponent.EquipmentOwner = null; + _container.Remove(toRemove, component.EquipmentContainer); + UpdateUserInterface(uid, component); + } + + /// + /// Attempts to change the amount of energy in the mech. + /// + /// The mech itself + /// The change in energy + /// + /// If the energy was successfully changed. + public virtual bool TryChangeEnergy(EntityUid uid, FixedPoint2 delta, MechComponent? component = null) + { + if (!Resolve(uid, ref component)) + return false; + + if (component.Energy + delta < 0) + return false; + + component.Energy = FixedPoint2.Clamp(component.Energy + delta, 0, component.MaxEnergy); + Dirty(uid, component); + UpdateUserInterface(uid, component); + return true; + } + + /// + /// Sets the integrity of the mech. + /// + /// The mech itself + /// The value the integrity will be set at + /// + public void SetIntegrity(EntityUid uid, FixedPoint2 value, MechComponent? component = null) + { + if (!Resolve(uid, ref component)) + return; + + component.Integrity = FixedPoint2.Clamp(value, 0, component.MaxIntegrity); + + if (component.Integrity <= 0) + { + BreakMech(uid, component); + } + else if (component.Broken) + { + component.Broken = false; + UpdateAppearance(uid, component); + } + + Dirty(uid, component); + UpdateUserInterface(uid, component); + } + + /// + /// Checks if the pilot is present + /// + /// + /// Whether or not the pilot is present + public bool IsEmpty(MechComponent component) + { + return component.PilotSlot.ContainedEntity == null; + } + + /// + /// Checks if an entity can be inserted into the mech. + /// + /// + /// + /// + /// + public bool CanInsert(EntityUid uid, EntityUid toInsert, MechComponent? component = null) + { + if (!Resolve(uid, ref component)) + return false; + + return IsEmpty(component) && _actionBlocker.CanMove(toInsert); + } + + /// + /// Updates the user interface + /// + /// + /// This is defined here so that UI updates can be accessed from shared. + /// + public virtual void UpdateUserInterface(EntityUid uid, MechComponent? component = null) + { + } + + /// + /// Attempts to insert a pilot into the mech. + /// + /// + /// + /// + /// Whether or not the entity was inserted + public bool TryInsert(EntityUid uid, EntityUid? toInsert, MechComponent? component = null) + { + if (!Resolve(uid, ref component)) + return false; + + if (toInsert == null || component.PilotSlot.ContainedEntity == toInsert) + return false; + + if (!CanInsert(uid, toInsert.Value, component)) + return false; + + SetupUser(uid, toInsert.Value); + _container.Insert(toInsert.Value, component.PilotSlot); + UpdateAppearance(uid, component); + return true; + } + + /// + /// Attempts to eject the current pilot from the mech + /// + /// + /// + /// Whether or not the pilot was ejected. + public bool TryEject(EntityUid uid, MechComponent? component = null) + { + if (!Resolve(uid, ref component)) + return false; + + if (component.PilotSlot.ContainedEntity == null) + return false; + + var pilot = component.PilotSlot.ContainedEntity.Value; + + RemoveUser(uid, pilot); + _container.RemoveEntity(uid, pilot); + UpdateAppearance(uid, component); + return true; + } + + private void OnGetMeleeWeapon(EntityUid uid, MechPilotComponent component, GetMeleeWeaponEvent args) + { + if (args.Handled) + return; + + if (!TryComp(component.Mech, out var mech)) + return; + + var weapon = mech.CurrentSelectedEquipment ?? component.Mech; + args.Weapon = weapon; + args.Handled = true; + } + + private void OnCanAttackFromContainer(EntityUid uid, MechPilotComponent component, CanAttackFromContainerEvent args) + { + args.CanAttack = true; + } + + private void OnAttackAttempt(EntityUid uid, MechPilotComponent component, AttackAttemptEvent args) + { + if (args.Target == component.Mech) + args.Cancel(); + } + + private void UpdateAppearance(EntityUid uid, MechComponent? component = null, + AppearanceComponent? appearance = null) + { + if (!Resolve(uid, ref component, ref appearance, false)) + return; + + _appearance.SetData(uid, MechVisuals.Open, IsEmpty(component), appearance); + _appearance.SetData(uid, MechVisuals.Broken, component.Broken, appearance); + } + + private void OnDragDrop(EntityUid uid, MechComponent component, ref DragDropTargetEvent args) + { + if (args.Handled) + return; + + args.Handled = true; + + var doAfterEventArgs = new DoAfterArgs(EntityManager, args.Dragged, component.EntryDelay, new MechEntryEvent(), uid, target: uid) + { + BreakOnMove = true, + }; + + _doAfter.TryStartDoAfter(doAfterEventArgs); + } + + private void OnCanDragDrop(EntityUid uid, MechComponent component, ref CanDropTargetEvent args) + { + args.Handled = true; + + args.CanDrop |= !component.Broken && CanInsert(uid, args.Dragged, component); + } + + private void OnEmagged(EntityUid uid, MechComponent component, ref GotEmaggedEvent args) + { + if (!component.BreakOnEmag) + return; + args.Handled = true; + component.EquipmentWhitelist = null; + Dirty(uid, component); + } +} + +/// +/// Event raised when the battery is successfully removed from the mech, +/// on both success and failure +/// +[Serializable, NetSerializable] +public sealed partial class RemoveBatteryEvent : SimpleDoAfterEvent +{ +} + +/// +/// Event raised when a person removes someone from a mech, +/// on both success and failure +/// +[Serializable, NetSerializable] +public sealed partial class MechExitEvent : SimpleDoAfterEvent +{ +} + +/// +/// Event raised when a person enters a mech, on both success and failure +/// +[Serializable, NetSerializable] +public sealed partial class MechEntryEvent : SimpleDoAfterEvent +{ +}