mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-10-09 18:31:17 +03:00
* Remove operator relay components immediately * Use EntityQuery for relay comp lookups * learn your ABCs with whatstone * Check component lifecycle, not immediate removal
389 lines
14 KiB
C#
389 lines
14 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Handles logic relating to vehicles.
|
|
/// </summary>
|
|
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<AppearanceComponent> _appearanceQuery;
|
|
[Dependency] private EntityQuery<HandsComponent> _handsQuery;
|
|
[Dependency] private EntityQuery<InputMoverComponent> _inputMoverQuery;
|
|
[Dependency] private EntityQuery<InteractionRelayComponent> _interactionRelayQuery;
|
|
[Dependency] private EntityQuery<MovementRelayTargetComponent> _movementRelayQuery;
|
|
[Dependency] private EntityQuery<RelayInputMoverComponent> _relayInputMoverQuery;
|
|
[Dependency] private EntityQuery<VehicleComponent> _vehicleQuery;
|
|
[Dependency] private EntityQuery<VehicleHandBlockerComponent> _handBlockerQuery;
|
|
[Dependency] private EntityQuery<VehicleOperatorComponent> _operatorQuery;
|
|
|
|
/// <remarks>
|
|
/// We subscribe to BeforeDamageChangedEvent so that we can access the damage value before the container is applied.
|
|
/// </remarks>
|
|
[SubscribeLocalEvent]
|
|
private void OnBeforeDamageChanged(Entity<VehicleComponent> 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<VehicleComponent> ent, ref UpdateCanMoveEvent args)
|
|
{
|
|
var ev = new VehicleCanRunEvent(ent);
|
|
RaiseLocalEvent(ent, ref ev);
|
|
if (!ev.CanRun)
|
|
args.Cancel();
|
|
}
|
|
|
|
[SubscribeLocalEvent]
|
|
private void OnOperatorUpdateCanMove(Entity<VehicleOperatorComponent> 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<VehicleOperatorComponent> 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<VehicleOperatorComponent> 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<VehicleComponent> ent, ref ComponentShutdown args)
|
|
{
|
|
if (_timing.ApplyingState)
|
|
return;
|
|
|
|
TryRemoveOperator(ent);
|
|
}
|
|
|
|
[SubscribeLocalEvent]
|
|
private void OnVehicleGetAdditionalAccess(Entity<VehicleComponent> 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<VehicleOperatorComponent> ent, ref ComponentShutdown args)
|
|
{
|
|
if (_timing.ApplyingState)
|
|
return;
|
|
|
|
TryRemoveOperator((ent, ent));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set the operator for a given vehicle
|
|
/// </summary>
|
|
/// <param name="entity">The vehicle</param>
|
|
/// <param name="operatorUid">The new operator.</param>
|
|
/// <returns>If the new operator was successfully able to be set</returns>
|
|
public bool TrySetOperator(Entity<VehicleComponent> 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<VehicleOperatorComponent>(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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to remove the current operator from a vehicle
|
|
/// </summary>
|
|
/// <param name="entity">The vehicle whose operator is being removed.</param>
|
|
/// <returns>If the operator was removed successfully</returns>
|
|
[PublicAPI]
|
|
public bool TryRemoveOperator(Entity<VehicleComponent> 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<VehicleOperatorComponent>(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<RelayInputMoverComponent>(operatorUid);
|
|
}
|
|
|
|
if (_interactionRelayQuery.TryComp(operatorUid, out var interactionRelay) &&
|
|
interactionRelay.RelayEntity == vehicleUid)
|
|
{
|
|
RemCompDeferred<InteractionRelayComponent>(operatorUid);
|
|
}
|
|
|
|
if (_movementRelayQuery.TryComp(vehicleUid, out var relayTarget) &&
|
|
relayTarget.Source == operatorUid)
|
|
{
|
|
RemCompDeferred<MovementRelayTargetComponent>(vehicleUid);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// From an operator, removes it from the vehicle
|
|
/// </summary>
|
|
/// <param name="operatorEntity">The operator who is riding a vehicle</param>
|
|
/// <returns>If the operator was removed successfully, or if the entity was not operating a vehicle.</returns>
|
|
[PublicAPI]
|
|
public bool TryRemoveOperator(Entity<VehicleOperatorComponent?> 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<VehicleOperatorComponent>(operatorEntity.Owner);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to get the current operator of a vehicle
|
|
/// </summary>
|
|
/// <param name="entity"></param>
|
|
/// <param name="operatorEnt"></param>
|
|
[PublicAPI]
|
|
public bool TryGetOperator(Entity<VehicleComponent?> entity, [NotNullWhen(true)] out Entity<VehicleOperatorComponent>? 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the operator of the vehicle or none if there isn't one present
|
|
/// </summary>
|
|
public EntityUid? GetOperatorOrNull(Entity<VehicleComponent?> entity)
|
|
{
|
|
TryGetOperator(entity, out var operatorEnt);
|
|
return operatorEnt;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the current vehicle has an operator.
|
|
/// </summary>
|
|
[PublicAPI]
|
|
public bool HasOperator(Entity<VehicleComponent?> entity)
|
|
{
|
|
return TryGetOperator(entity, out _);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public bool CanOperate(Entity<VehicleComponent?> 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to occupy the configured number of the operator's hands.
|
|
/// </summary>
|
|
private bool TryBlockHands(Entity<VehicleComponent> 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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes hand blockers owned by the specified vehicle.
|
|
/// </summary>
|
|
private void UnblockHands(EntityUid vehicleUid, EntityUid operatorUid)
|
|
{
|
|
_virtualItem.DeleteInHandsMatching(operatorUid, vehicleUid);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if the vehicle is capable of running (has keys, fuel, etc.) and caches the value.
|
|
/// Updates the appearance data.
|
|
/// </summary>
|
|
public void RefreshCanRun(Entity<VehicleComponent?> entity)
|
|
{
|
|
if (TerminatingOrDeleted(entity))
|
|
return;
|
|
|
|
if (!Resolve(entity, ref entity.Comp, false))
|
|
return;
|
|
|
|
_actionBlocker.UpdateCanMove(entity);
|
|
UpdateAppearance((entity, entity.Comp));
|
|
}
|
|
|
|
private void UpdateAppearance(Entity<VehicleComponent> 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);
|
|
}
|
|
}
|