using System.Linq; using Content.Shared.Actions; using Content.Shared.Destructible; using Content.Shared.DoAfter; 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; using Content.Shared.Vehicle.Systems; using Content.Shared.Vehicle.Components; using Content.Shared.Verbs; 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 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 SharedUserInterfaceSystem _userInterface = 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; Vehicle.TryExit(uid); } [SubscribeLocalEvent] private void OnOpenUi(EntityUid uid, MechComponent component, MechOpenUiEvent args) { args.Handled = true; ToggleMechUi(uid, component); } [SubscribeLocalEvent] private void OnAlternativeVerb(EntityUid uid, MechComponent component, GetVerbsEvent args) { if (!args.CanAccess || !args.CanInteract || component.Broken || Vehicle.HasOperator(uid)) return; var openUiVerb = new AlternativeVerb { Act = () => ToggleMechUi(uid, component, args.User), Text = Loc.GetString("mech-ui-open-verb") }; args.Verbs.Add(openUiVerb); } [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.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; Vehicle.TryExit(uid); 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); } /// /// 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) { } private void ToggleMechUi(EntityUid uid, MechComponent? component = null, EntityUid? user = null) { if (!Resolve(uid, ref component)) return; user ??= Vehicle.GetOperatorOrNull(uid); if (user == null) return; _userInterface.TryToggleUi(uid, MechUiKey.Key, user.Value); UpdateUserInterface(uid, component); } [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 OnMechEntryAttempt(Entity ent, ref ContainerVehicleEntryAttemptEvent args) { if (ent.Comp.Broken) args.Cancelled = true; } [SubscribeLocalEvent] private void OnMechExitAttempt(Entity ent, ref ContainerVehicleExitAttemptEvent args) { if (ent.Comp.Broken) args.Cancelled = true; } [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); if (args.NewOperator is null && args.OldOperator is not null) _userInterface.CloseUi(ent.Owner, MechUiKey.Key); } } /// /// Event raised when the battery is successfully removed from the mech, /// on both success and failure /// [Serializable, NetSerializable] public sealed partial class RemoveBatteryEvent : SimpleDoAfterEvent;