using System.Linq; using Content.Shared.ActionBlocker; using Content.Shared.Actions; using Content.Shared.Destructible; using Content.Shared.DoAfter; using Content.Shared.DragDrop; using Content.Shared.FixedPoint; using Content.Shared.Interaction; using Content.Shared.Interaction.Components; using Content.Shared.Mech.Components; using Content.Shared.Mech.Equipment.Components; using Content.Shared.Popups; using Content.Shared.Storage.Components; using Content.Shared.Vehicle.Systems; using Content.Shared.Vehicle.Components; using Content.Shared.Weapons.Melee; 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 partial class SharedMechSystem : EntitySystem { [Dependency] private IGameTiming _timing = default!; [Dependency] private INetManager _net = default!; [Dependency] private ActionBlockerSystem _actionBlocker = default!; [Dependency] private SharedActionsSystem _actions = default!; [Dependency] private SharedAppearanceSystem _appearance = default!; [Dependency] private SharedContainerSystem _container = default!; [Dependency] private SharedInteractionSystem _interaction = default!; [Dependency] private SharedPopupSystem _popup = default!; [Dependency] private SharedDoAfterSystem _doAfter = default!; [Dependency] protected VehicleSystem Vehicle = default!; [Dependency] private EntityWhitelistSystem _whitelistSystem = default!; [SubscribeLocalEvent] private void OnToggleEquipmentAction(EntityUid uid, MechComponent component, MechToggleEquipmentEvent args) { if (args.Handled) return; args.Handled = true; CycleEquipment(uid); } [SubscribeLocalEvent] private void OnEjectPilotEvent(EntityUid uid, MechComponent component, MechEjectPilotEvent args) { if (args.Handled) return; args.Handled = true; TryEject(uid, component); } [SubscribeLocalEvent] private void RelayInteractionEvent(EntityUid uid, MechComponent component, UserActivateInWorldEvent args) { if (!Vehicle.HasOperator(uid)) return; // TODO why is this being blocked? if (!_timing.IsFirstTimePredicted) return; if (component.CurrentSelectedEquipment != null) { RaiseLocalEvent(component.CurrentSelectedEquipment.Value, args); } } [SubscribeLocalEvent] 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); } [SubscribeLocalEvent] private void OnDestruction(EntityUid uid, MechComponent component, DestructionEventArgs args) { BreakMech(uid, component); } [SubscribeLocalEvent] private void OnEntityStorageDump(Entity entity, ref EntityStorageIntoContainerAttemptEvent args) { // There's no reason we should dump into /any/ of the mech's containers. args.Cancelled = true; } private void SetupUser(EntityUid mech, EntityUid pilot, MechComponent? component = null) { if (!Resolve(mech, ref component)) return; // Warning: this bypasses most normal interaction blocking components on the user, like drone laws and the like. var irelay = EnsureComp(pilot); _interaction.SetRelay(pilot, mech, irelay); 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) { RemComp(pilot); _actions.RemoveProvidedActions(pilot, mech); } /// /// Destroys the mech, removing the user and ejecting anything contained. /// /// /// 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, and if non-mech equipment should be ejected. /// public void RemoveEquipment(EntityUid uid, EntityUid toRemove, MechComponent? component = null, MechEquipmentComponent? equipmentComponent = null, bool forced = false) { if (!Resolve(uid, ref component)) return; // When forced, we also want to handle the possibility that the "equipment" isn't actually equipment. // This /shouldn't/ be possible thanks to OnEntityStorageDump, but there's been quite a few regressions // with entities being hardlock stuck inside mechs. if (!Resolve(toRemove, ref equipmentComponent) && !forced) 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); if (forced && equipmentComponent != null) equipmentComponent.EquipmentOwner = null; _container.Remove(toRemove, component.EquipmentContainer); UpdateUserInterface(uid, component); } /// /// Attempts to change the amount of energy in the mech. /// TODO: Power cells are predicted now, so no need to duplicate the charge level /// /// 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 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; if (!_actionBlocker.CanMove(toInsert)) return false; if (Vehicle.GetOperatorOrNull(uid) == toInsert) return false; if (!_container.CanInsert(toInsert, component.PilotSlot)) return false; return true; } /// /// 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 (!CanInsert(uid, toInsert, component)) return false; _container.Insert(toInsert, component.PilotSlot); 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 (!Vehicle.TryGetOperator(uid, out var operatorEnt)) return false; return _container.RemoveEntity(uid, operatorEnt.Value); } [SubscribeLocalEvent] private void OnGetMeleeWeapon(Entity ent, ref GetMeleeWeaponEvent args) { if (args.Handled) return; if (ent.Comp.Vehicle is not { } vehicle) return; if (!TryComp(vehicle, out var mech)) return; var weapon = mech.CurrentSelectedEquipment ?? vehicle; args.Weapon = weapon; args.Handled = true; } 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, !Vehicle.HasOperator(uid), appearance); _appearance.SetData(uid, MechVisuals.Broken, component.Broken, appearance); } [SubscribeLocalEvent] 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); } [SubscribeLocalEvent] private void OnCanDragDrop(EntityUid uid, MechComponent component, ref CanDropTargetEvent args) { args.Handled = true; args.CanDrop |= !component.Broken && CanInsert(uid, args.Dragged, component); } [SubscribeLocalEvent] private void OnOperatorSet(Entity ent, ref VehicleOperatorSetEvent args) { if (args.OldOperator is { } oldOperator) { RemoveUser(ent, oldOperator); } if (args.NewOperator is { } newOperator) { SetupUser(ent, newOperator, ent); } UpdateAppearance(ent); UpdateUserInterface(ent, ent); } } /// /// 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;