using System.Diagnostics.CodeAnalysis; using Content.Shared.Access.Components; using Content.Shared.ActionBlocker; using Content.Shared.Damage; using Content.Shared.Damage.Systems; using Content.Shared.DoAfter; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction.Events; using Content.Shared.Interaction.Components; using Content.Shared.Inventory.VirtualItem; using Content.Shared.Movement.Components; using Content.Shared.Movement.Events; using Content.Shared.Movement.Systems; using Content.Shared.Popups; using Content.Shared.Vehicle.Components; using Content.Shared.Whitelist; using JetBrains.Annotations; using Robust.Shared.Containers; using Robust.Shared.Timing; namespace Content.Shared.Vehicle.Systems; /// /// Handles logic relating to vehicles. /// public sealed partial class VehicleSystem : EntitySystem { [Dependency] private ActionBlockerSystem _actionBlocker = default!; [Dependency] private SharedAppearanceSystem _appearance = default!; [Dependency] private SharedContainerSystem _container = default!; [Dependency] private DamageableSystem _damageable = default!; [Dependency] private SharedDoAfterSystem _doAfter = default!; [Dependency] private SharedEyeSystem _eye = default!; [Dependency] private EntityWhitelistSystem _entityWhitelist = default!; [Dependency] private SharedHandsSystem _hands = default!; [Dependency] private SharedMoverController _mover = default!; [Dependency] private SharedPopupSystem _popup = default!; [Dependency] private SharedVirtualItemSystem _virtualItem = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private EntityQuery _appearanceQuery; [Dependency] private EntityQuery _handsQuery; [Dependency] private EntityQuery _inputMoverQuery; [Dependency] private EntityQuery _interactionRelayQuery; [Dependency] private EntityQuery _movementRelayQuery; [Dependency] private EntityQuery _relayInputMoverQuery; [Dependency] private EntityQuery _vehicleQuery; [Dependency] private EntityQuery _handBlockerQuery; [Dependency] private EntityQuery _operatorQuery; /// /// We subscribe to BeforeDamageChangedEvent so that we can access the damage value before the container is applied. /// [SubscribeLocalEvent] private void OnBeforeDamageChanged(Entity ent, ref BeforeDamageChangedEvent args) { if (!ent.Comp.TransferDamage || !args.Damage.AnyPositive() || ent.Comp.Operator is not { } operatorUid) return; var damage = DamageSpecifier.GetPositive(args.Damage); if (ent.Comp.TransferDamageModifier is { } modifierSet) { // Reduce damage to the operator via the specified modifier, if provided. damage = DamageSpecifier.ApplyModifierSet(damage, modifierSet); } _damageable.TryChangeDamage(operatorUid, damage, origin: args.Origin); } [SubscribeLocalEvent] private void OnVehicleUpdateCanMove(Entity ent, ref UpdateCanMoveEvent args) { var ev = new VehicleCanRunEvent(ent); RaiseLocalEvent(ent, ref ev); if (!ev.CanRun) args.Cancel(); } [SubscribeLocalEvent] private void OnOperatorUpdateCanMove(Entity ent, ref UpdateCanMoveEvent args) { if (ent.Comp.Vehicle is not { } vehicleUid || !_vehicleQuery.TryComp(vehicleUid, out var vehicle)) return; if (!CanOperate((vehicleUid, vehicle), ent.Owner)) args.Cancel(); } [SubscribeLocalEvent] private void OnOperatorCanAttackFromContainer(Entity ent, ref CanAttackFromContainerEvent args) { if (ent.Comp.Vehicle is not { } vehicleUid || !_vehicleQuery.TryComp(vehicleUid, out var vehicle) || !vehicle.CanAttack) return; args.CanAttack = true; } [SubscribeLocalEvent] private void OnOperatorAttackAttempt(Entity ent, ref AttackAttemptEvent args) { if (ent.Comp.Vehicle is not { } vehicleUid || !_vehicleQuery.TryComp(vehicleUid, out var vehicle) || !vehicle.CanAttack || args.Target != vehicleUid) return; args.Cancel(); } [SubscribeLocalEvent] private void OnVehicleShutdown(Entity ent, ref ComponentShutdown args) { if (_timing.ApplyingState) return; TryRemoveOperator(ent); } [SubscribeLocalEvent] private void OnVehicleGetAdditionalAccess(Entity ent, ref GetAdditionalAccessEvent args) { // Vehicles inherit access from whoever is driving them if (ent.Comp.Operator is { } operatorUid) args.Entities.Add(operatorUid); } [SubscribeLocalEvent] private void OnOperatorShutdown(Entity ent, ref ComponentShutdown args) { if (_timing.ApplyingState) return; TryRemoveOperator((ent, ent)); } /// /// Set the operator for a given vehicle /// /// The vehicle /// The new operator. /// If the new operator was successfully able to be set public bool TrySetOperator(Entity entity, EntityUid operatorUid) { // Early exit if setting the same operator that's already present. if (entity.Comp.Operator == operatorUid) return _operatorQuery.TryComp(operatorUid, out var existingOperator) && existingOperator.Vehicle == entity.Owner; if (entity.Comp.Operator is not null) return false; _operatorQuery.TryComp(operatorUid, out var vehicleOperator); if (vehicleOperator?.Vehicle is not null) return false; if (!CanOperate(entity.AsNullable(), operatorUid)) return false; if (!TryBlockHands(entity, operatorUid)) return false; entity.Comp.Operator = operatorUid; vehicleOperator ??= AddComp(operatorUid); vehicleOperator.Vehicle = entity.Owner; Dirty(operatorUid, vehicleOperator); _mover.SetRelay(operatorUid, entity); _eye.SetTarget(operatorUid, entity.Owner); var enterEvent = new OnVehicleEnteredEvent(entity, operatorUid); RaiseLocalEvent(operatorUid, ref enterEvent); RefreshCanRun((entity, entity.Comp)); var setEvent = new VehicleOperatorSetEvent(operatorUid, null); RaiseLocalEvent(entity, ref setEvent); DirtyFields(entity.Owner, entity.Comp, null, nameof(VehicleComponent.Operator)); return true; } /// /// Attempts to remove the current operator from a vehicle /// /// The vehicle whose operator is being removed. /// If the operator was removed successfully [PublicAPI] public bool TryRemoveOperator(Entity entity) { if (entity.Comp.Operator is not { } currentOperator) return false; _eye.SetTarget(currentOperator, null); if (_operatorQuery.TryComp(currentOperator, out var currentOperatorComponent)) { var exitEvent = new OnVehicleExitedEvent(entity, currentOperator); RaiseLocalEvent(currentOperator, ref exitEvent); currentOperatorComponent.Vehicle = null; RemCompDeferred(currentOperator); } UnblockHands(entity.Owner, currentOperator); entity.Comp.Operator = null; ClearOperatorRelays(currentOperator, entity); RefreshCanRun((entity, entity.Comp)); var setEvent = new VehicleOperatorSetEvent(null, currentOperator); RaiseLocalEvent(entity, ref setEvent); DirtyFields(entity.Owner, entity.Comp, null, nameof(VehicleComponent.Operator)); return true; } private void ClearOperatorRelays(EntityUid operatorUid, EntityUid vehicleUid) { if (_relayInputMoverQuery.TryComp(operatorUid, out var relayMover) && relayMover.RelayEntity == vehicleUid) { RemCompDeferred(operatorUid); } if (_interactionRelayQuery.TryComp(operatorUid, out var interactionRelay) && interactionRelay.RelayEntity == vehicleUid) { RemCompDeferred(operatorUid); } if (_movementRelayQuery.TryComp(vehicleUid, out var relayTarget) && relayTarget.Source == operatorUid) { RemCompDeferred(vehicleUid); } } /// /// From an operator, removes it from the vehicle /// /// The operator who is riding a vehicle /// If the operator was removed successfully, or if the entity was not operating a vehicle. [PublicAPI] public bool TryRemoveOperator(Entity operatorEntity) { if (!Resolve(operatorEntity, ref operatorEntity.Comp, false)) return true; var vehicleUid = operatorEntity.Comp.Vehicle; if (vehicleUid is null) return true; if (_vehicleQuery.TryComp(vehicleUid, out var vehicle)) return TryRemoveOperator((vehicleUid.Value, vehicle)); _eye.SetTarget(operatorEntity.Owner, null); UnblockHands(vehicleUid.Value, operatorEntity.Owner); ClearOperatorRelays(operatorEntity.Owner, vehicleUid.Value); operatorEntity.Comp.Vehicle = null; RemCompDeferred(operatorEntity.Owner); return true; } /// /// Attempts to get the current operator of a vehicle /// /// /// [PublicAPI] public bool TryGetOperator(Entity entity, [NotNullWhen(true)] out Entity? operatorEnt) { operatorEnt = null; if (!Resolve(entity, ref entity.Comp)) return false; if (entity.Comp.Operator is not { } operatorUid) return false; if (!_operatorQuery.TryComp(operatorUid, out var operatorComponent)) return false; operatorEnt = (operatorUid, operatorComponent); return true; } /// /// Returns the operator of the vehicle or none if there isn't one present /// public EntityUid? GetOperatorOrNull(Entity entity) { TryGetOperator(entity, out var operatorEnt); return operatorEnt; } /// /// Checks if the current vehicle has an operator. /// [PublicAPI] public bool HasOperator(Entity entity) { return TryGetOperator(entity, out _); } /// /// Checks if a given entity is capable of operating a vehicle. /// Note that the general ability for a vehicle to run (keys, fuel, etc.) is not checked here. /// This is *only* for checks on the user. /// public bool CanOperate(Entity entity, EntityUid uid) { if (!Exists(uid)) return false; if (!Resolve(entity, ref entity.Comp)) return false; if (_entityWhitelist.IsWhitelistFail(entity.Comp.OperatorWhitelist, uid)) return false; if (entity.Comp.RequiresHands && (!_handsQuery.HasComp(uid) || !_actionBlocker.CanInteract(uid, entity))) return false; if (_handBlockerQuery.TryComp(entity, out var handBlocker) && (!_handsQuery.TryComp(uid, out var hands) || _hands.GetHandCount((uid, hands)) < handBlocker.BlockedHands)) return false; return _actionBlocker.CanConsciouslyPerformAction(uid); } /// /// Attempts to occupy the configured number of the operator's hands. /// private bool TryBlockHands(Entity vehicle, EntityUid operatorUid) { if (!_handBlockerQuery.TryComp(vehicle, out var handBlocker)) return true; if (_virtualItem.TrySpawnUnremoveableVirtualItemInHand( vehicle.Owner, operatorUid, handBlocker.BlockedHands, dropOthers: true)) return true; UnblockHands(vehicle.Owner, operatorUid); return false; } /// /// Removes hand blockers owned by the specified vehicle. /// private void UnblockHands(EntityUid vehicleUid, EntityUid operatorUid) { _virtualItem.DeleteInHandsMatching(operatorUid, vehicleUid); } /// /// Checks if the vehicle is capable of running (has keys, fuel, etc.) and caches the value. /// Updates the appearance data. /// public void RefreshCanRun(Entity entity) { if (TerminatingOrDeleted(entity)) return; if (!Resolve(entity, ref entity.Comp, false)) return; _actionBlocker.UpdateCanMove(entity); UpdateAppearance((entity, entity.Comp)); } private void UpdateAppearance(Entity entity) { if (!_appearanceQuery.TryComp(entity, out var appearance)) return; if (_inputMoverQuery.TryComp(entity, out var inputMover)) _appearance.SetData(entity, VehicleVisuals.CanRun, inputMover.CanMove, appearance); _appearance.SetData(entity, VehicleVisuals.HasOperator, entity.Comp.Operator is not null, appearance); } }