using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Vehicle.Components;
using Content.Shared.Actions;
using Content.Shared.Buckle.Components;
using Content.Shared.Item;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Robust.Shared.Serialization;
using Robust.Shared.Containers;
using Content.Shared.Tag;
using Content.Shared.Audio;
using Content.Shared.Buckle;
using Content.Shared.Hands;
using Content.Shared.Light.Component;
using Content.Shared.Popups;
using Robust.Shared.Network;
using Robust.Shared.Physics.Systems;
namespace Content.Shared.Vehicle;
///
/// Stores the VehicleVisuals and shared event
/// Nothing for a system but these need to be put somewhere in
/// Content.Shared
///
public abstract partial class SharedVehicleSystem : EntitySystem
{
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly MovementSpeedModifierSystem _modifier = default!;
[Dependency] private readonly SharedAmbientSoundSystem _ambientSound = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly AccessReaderSystem _access = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedHandVirtualItemSystem _virtualItemSystem = default!;
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
[Dependency] private readonly SharedJointSystem _joints = default!;
[Dependency] private readonly SharedBuckleSystem _buckle = default!;
[Dependency] private readonly SharedMoverController _mover = default!;
private const string KeySlot = "key_slot";
///
public override void Initialize()
{
base.Initialize();
InitializeRider();
SubscribeLocalEvent(OnVehicleStartup);
SubscribeLocalEvent(OnBuckleChange);
SubscribeLocalEvent(OnHonkAction);
SubscribeLocalEvent(OnEntInserted);
SubscribeLocalEvent(OnEntRemoved);
SubscribeLocalEvent(OnRefreshMovementSpeedModifiers);
SubscribeLocalEvent(OnMoveEvent);
SubscribeLocalEvent(OnGetAdditionalAccess);
SubscribeLocalEvent(OnGettingPickedUpAttempt);
}
///
/// This just controls whether the wheels are turning.
///
public override void Update(float frameTime)
{
var vehicleQuery = EntityQueryEnumerator();
while (vehicleQuery.MoveNext(out var uid, out var vehicle, out var mover))
{
if (!vehicle.AutoAnimate)
continue;
if (_mover.GetVelocityInput(mover).Sprinting == Vector2.Zero)
{
UpdateAutoAnimate(uid, false);
continue;
}
UpdateAutoAnimate(uid, true);
}
}
private void OnVehicleStartup(EntityUid uid, VehicleComponent component, ComponentStartup args)
{
UpdateDrawDepth(uid, 2);
// This code should be purged anyway but with that being said this doesn't handle components being changed.
if (TryComp(uid, out var strap))
{
component.BaseBuckleOffset = strap.BuckleOffset;
strap.BuckleOffsetUnclamped = Vector2.Zero;
}
_modifier.RefreshMovementSpeedModifiers(uid);
}
///
/// Give the user the rider component if they're buckling to the vehicle,
/// otherwise remove it.
///
private void OnBuckleChange(EntityUid uid, VehicleComponent component, ref BuckleChangeEvent args)
{
// Add Rider
if (args.Buckling)
{
// Add a virtual item to rider's hand, unbuckle if we can't.
if (!_virtualItemSystem.TrySpawnVirtualItemInHand(uid, args.BuckledEntity))
{
_buckle.TryUnbuckle(uid, uid, true);
return;
}
// Set up the rider and vehicle with each other
EnsureComp(uid);
var rider = EnsureComp(args.BuckledEntity);
component.Rider = args.BuckledEntity;
component.LastRider = component.Rider;
Dirty(component);
Appearance.SetData(uid, VehicleVisuals.HideRider, true);
_mover.SetRelay(args.BuckledEntity, uid);
rider.Vehicle = uid;
// Update appearance stuff, add actions
UpdateBuckleOffset(uid, Transform(uid), component);
if (TryComp(uid, out var mover))
UpdateDrawDepth(uid, GetDrawDepth(Transform(uid), component, mover.RelativeRotation.Degrees));
if (TryComp(args.BuckledEntity, out var actions) && TryComp(uid, out var flashlight))
{
_actionsSystem.AddAction(args.BuckledEntity, flashlight.ToggleAction, uid, actions);
}
if (component.HornSound != null)
{
_actionsSystem.AddAction(args.BuckledEntity, component.HornAction, uid, actions);
}
_joints.ClearJoints(args.BuckledEntity);
return;
}
// Remove rider
// Clean up actions and virtual items
_actionsSystem.RemoveProvidedActions(args.BuckledEntity, uid);
_virtualItemSystem.DeleteInHandsMatching(args.BuckledEntity, uid);
// Entity is no longer riding
RemComp(args.BuckledEntity);
RemComp(args.BuckledEntity);
Appearance.SetData(uid, VehicleVisuals.HideRider, false);
// Reset component
component.Rider = null;
Dirty(component);
}
///
/// This fires when the rider presses the honk action
///
private void OnHonkAction(EntityUid uid, VehicleComponent vehicle, HonkActionEvent args)
{
if (args.Handled || vehicle.HornSound == null)
return;
// TODO: Need audio refactor maybe, just some way to null it when the stream is over.
// For now better to just not loop to keep the code much cleaner.
vehicle.HonkPlayingStream?.Stop();
vehicle.HonkPlayingStream = _audioSystem.PlayPredicted(vehicle.HornSound, uid, uid);
args.Handled = true;
}
///
/// Handle adding keys to the ignition, give stuff the InVehicleComponent so it can't be picked
/// up by people not in the vehicle.
///
private void OnEntInserted(EntityUid uid, VehicleComponent component, EntInsertedIntoContainerMessage args)
{
if (args.Container.ID != KeySlot ||
!_tagSystem.HasTag(args.Entity, "VehicleKey"))
return;
// Enable vehicle
var inVehicle = EnsureComp(args.Entity);
inVehicle.Vehicle = component;
component.HasKey = true;
var msg = Loc.GetString("vehicle-use-key",
("keys", args.Entity), ("vehicle", uid));
if (_netManager.IsServer)
_popupSystem.PopupEntity(msg, uid, args.OldParent, PopupType.Medium);
// Audiovisual feedback
_ambientSound.SetAmbience(uid, true);
_tagSystem.AddTag(uid, "DoorBumpOpener");
_modifier.RefreshMovementSpeedModifiers(uid);
}
///
/// Turn off the engine when key is removed.
///
private void OnEntRemoved(EntityUid uid, VehicleComponent component, EntRemovedFromContainerMessage args)
{
if (args.Container.ID != KeySlot || !RemComp(args.Entity))
return;
// Disable vehicle
component.HasKey = false;
_ambientSound.SetAmbience(uid, false);
_tagSystem.RemoveTag(uid, "DoorBumpOpener");
_modifier.RefreshMovementSpeedModifiers(uid);
}
private void OnRefreshMovementSpeedModifiers(EntityUid uid, VehicleComponent component, RefreshMovementSpeedModifiersEvent args)
{
if (!component.HasKey)
{
args.ModifySpeed(0f, 0f);
}
}
// TODO: Shitcode, needs to use sprites instead of actual offsets.
private void OnMoveEvent(EntityUid uid, VehicleComponent component, ref MoveEvent args)
{
if (args.NewRotation == args.OldRotation)
return;
// This first check is just for safety
if (component.AutoAnimate && !HasComp(uid))
{
UpdateAutoAnimate(uid, false);
return;
}
UpdateBuckleOffset(uid, args.Component, component);
if (TryComp(uid, out var mover))
UpdateDrawDepth(uid, GetDrawDepth(args.Component, component, mover.RelativeRotation));
}
private void OnGettingPickedUpAttempt(EntityUid uid, InVehicleComponent component, GettingPickedUpAttemptEvent args)
{
if (component.Vehicle == null || component.Vehicle.Rider != null && component.Vehicle.Rider != args.User)
args.Cancel();
}
///
/// Depending on which direction the vehicle is facing,
/// change its draw depth. Vehicles can choose between special drawdetph
/// when facing north or south. East and west are easy.
///
private int GetDrawDepth(TransformComponent xform, VehicleComponent component, Angle cameraAngle)
{
var itemDirection = cameraAngle.GetDir() switch
{
Direction.South => xform.LocalRotation.GetDir(),
Direction.North => xform.LocalRotation.RotateDir(Direction.North),
Direction.West => xform.LocalRotation.RotateDir(Direction.East),
Direction.East => xform.LocalRotation.RotateDir(Direction.West),
_ => Direction.South
};
return itemDirection switch
{
Direction.North => component.NorthOver
? (int) DrawDepth.DrawDepth.Doors
: (int) DrawDepth.DrawDepth.WallMountedItems,
Direction.South => component.SouthOver
? (int) DrawDepth.DrawDepth.Doors
: (int) DrawDepth.DrawDepth.WallMountedItems,
Direction.West => component.WestOver
? (int) DrawDepth.DrawDepth.Doors
: (int) DrawDepth.DrawDepth.WallMountedItems,
Direction.East => component.EastOver
? (int) DrawDepth.DrawDepth.Doors
: (int) DrawDepth.DrawDepth.WallMountedItems,
_ => (int) DrawDepth.DrawDepth.WallMountedItems
};
}
///
/// Change the buckle offset based on what direction the vehicle is facing and
/// teleport any buckled entities to it. This is the most crucial part of making
/// buckled vehicles work.
///
private void UpdateBuckleOffset(EntityUid uid, TransformComponent xform, VehicleComponent component)
{
if (!TryComp(uid, out var strap))
return;
// TODO: Strap should handle this but buckle E/C moment.
var oldOffset = strap.BuckleOffsetUnclamped;
strap.BuckleOffsetUnclamped = xform.LocalRotation.Degrees switch
{
< 45f => (0, component.SouthOverride),
<= 135f => component.BaseBuckleOffset,
< 225f => (0, component.NorthOverride),
<= 315f => (component.BaseBuckleOffset.X * -1, component.BaseBuckleOffset.Y),
_ => (0, component.SouthOverride)
};
if (!oldOffset.Equals(strap.BuckleOffsetUnclamped))
Dirty(strap);
foreach (var buckledEntity in strap.BuckledEntities)
{
var buckleXform = Transform(buckledEntity);
_transform.SetLocalPositionNoLerp(buckleXform, strap.BuckleOffset);
}
}
private void OnGetAdditionalAccess(EntityUid uid, VehicleComponent component, ref GetAdditionalAccessEvent args)
{
if (component.Rider == null)
return;
var rider = component.Rider.Value;
args.Entities.Add(rider);
_access.FindAccessItemsInventory(rider, out var items);
args.Entities.UnionWith(items);
}
///
/// Set the draw depth for the sprite.
///
private void UpdateDrawDepth(EntityUid uid, int drawDepth)
{
Appearance.SetData(uid, VehicleVisuals.DrawDepth, drawDepth);
}
///
/// Set whether the vehicle's base layer is animating or not.
///
private void UpdateAutoAnimate(EntityUid uid, bool autoAnimate)
{
Appearance.SetData(uid, VehicleVisuals.AutoAnimate, autoAnimate);
}
}
///
/// Stores the vehicle's draw depth mostly
///
[Serializable, NetSerializable]
public enum VehicleVisuals : byte
{
///
/// What layer the vehicle should draw on (assumed integer)
///
DrawDepth,
///
/// Whether the wheels should be turning
///
AutoAnimate,
HideRider
}
///
/// Raised when someone honks a vehicle horn
///
public sealed class HonkActionEvent : InstantActionEvent
{
}