| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643 |
- using System.Diagnostics.CodeAnalysis;
- using System.Numerics;
- using Content.Shared.ActionBlocker;
- using Content.Shared.Actions;
- using Content.Shared.Administration.Logs;
- using Content.Shared.Audio;
- using Content.Shared.CombatMode;
- using Content.Shared.Containers.ItemSlots;
- using Content.Shared.Damage;
- using Content.Shared.Examine;
- using Content.Shared.Gravity;
- using Content.Shared.Hands;
- using Content.Shared.Hands.Components;
- using Content.Shared.Popups;
- using Content.Shared.Projectiles;
- using Content.Shared.Tag;
- using Content.Shared.Throwing;
- using Content.Shared.Timing;
- using Content.Shared.Verbs;
- using Content.Shared.Weapons.Melee;
- using Content.Shared.Weapons.Melee.Events;
- using Content.Shared.Weapons.Ranged.Components;
- using Content.Shared.Weapons.Ranged.Events;
- using Content.Shared.Whitelist;
- using Robust.Shared.Audio;
- using Robust.Shared.Audio.Systems;
- using Robust.Shared.Containers;
- using Robust.Shared.Map;
- using Robust.Shared.Network;
- using Robust.Shared.Physics.Components;
- using Robust.Shared.Physics.Systems;
- using Robust.Shared.Prototypes;
- using Robust.Shared.Random;
- using Robust.Shared.Serialization;
- using Robust.Shared.Timing;
- using Robust.Shared.Utility;
- namespace Content.Shared.Weapons.Ranged.Systems;
- public abstract partial class SharedGunSystem : EntitySystem
- {
- [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!;
- [Dependency] protected readonly IGameTiming Timing = default!;
- [Dependency] protected readonly IMapManager MapManager = default!;
- [Dependency] private readonly INetManager _netManager = default!;
- [Dependency] protected readonly IPrototypeManager ProtoManager = default!;
- [Dependency] protected readonly IRobustRandom Random = default!;
- [Dependency] protected readonly ISharedAdminLogManager Logs = default!;
- [Dependency] protected readonly DamageableSystem Damageable = default!;
- [Dependency] protected readonly ExamineSystemShared Examine = default!;
- [Dependency] private readonly ItemSlotsSystem _slots = default!;
- [Dependency] private readonly RechargeBasicEntityAmmoSystem _recharge = default!;
- [Dependency] protected readonly SharedActionsSystem Actions = default!;
- [Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
- [Dependency] protected readonly SharedAudioSystem Audio = default!;
- [Dependency] private readonly SharedCombatModeSystem _combatMode = default!;
- [Dependency] protected readonly SharedContainerSystem Containers = default!;
- [Dependency] private readonly SharedGravitySystem _gravity = default!;
- [Dependency] protected readonly SharedPointLightSystem Lights = default!;
- [Dependency] protected readonly SharedPopupSystem PopupSystem = default!;
- [Dependency] protected readonly SharedPhysicsSystem Physics = default!;
- [Dependency] protected readonly SharedProjectileSystem Projectiles = default!;
- [Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
- [Dependency] protected readonly TagSystem TagSystem = default!;
- [Dependency] protected readonly ThrowingSystem ThrowingSystem = default!;
- [Dependency] private readonly UseDelaySystem _useDelay = default!;
- [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
- private const float InteractNextFire = 0.3f;
- private const double SafetyNextFire = 0.5;
- private const float EjectOffset = 0.4f;
- protected const string AmmoExamineColor = "yellow";
- protected const string FireRateExamineColor = "yellow";
- public const string ModeExamineColor = "cyan";
- public override void Initialize()
- {
- SubscribeAllEvent<RequestShootEvent>(OnShootRequest);
- SubscribeAllEvent<RequestStopShootEvent>(OnStopShootRequest);
- SubscribeLocalEvent<GunComponent, MeleeHitEvent>(OnGunMelee);
- // Ammo providers
- InitializeBallistic();
- InitializeBattery();
- InitializeCartridge();
- InitializeChamberMagazine();
- InitializeMagazine();
- InitializeRevolver();
- InitializeBasicEntity();
- InitializeClothing();
- InitializeContainer();
- InitializeSolution();
- // Interactions
- SubscribeLocalEvent<GunComponent, GetVerbsEvent<AlternativeVerb>>(OnAltVerb);
- SubscribeLocalEvent<GunComponent, ExaminedEvent>(OnExamine);
- SubscribeLocalEvent<GunComponent, CycleModeEvent>(OnCycleMode);
- SubscribeLocalEvent<GunComponent, HandSelectedEvent>(OnGunSelected);
- SubscribeLocalEvent<GunComponent, MapInitEvent>(OnMapInit);
- }
- private void OnMapInit(Entity<GunComponent> gun, ref MapInitEvent args)
- {
- #if DEBUG
- if (gun.Comp.NextFire > Timing.CurTime)
- Log.Warning($"Initializing a map that contains an entity that is on cooldown. Entity: {ToPrettyString(gun)}");
- DebugTools.Assert((gun.Comp.AvailableModes & gun.Comp.SelectedMode) != 0x0);
- #endif
- RefreshModifiers((gun, gun));
- }
- private void OnGunMelee(EntityUid uid, GunComponent component, MeleeHitEvent args)
- {
- if (!TryComp<MeleeWeaponComponent>(uid, out var melee))
- return;
- if (melee.NextAttack > component.NextFire)
- {
- component.NextFire = melee.NextAttack;
- EntityManager.DirtyField(uid, component, nameof(GunComponent.NextFire));
- }
- }
- private void OnShootRequest(RequestShootEvent msg, EntitySessionEventArgs args)
- {
- var user = args.SenderSession.AttachedEntity;
- if (user == null ||
- !_combatMode.IsInCombatMode(user) ||
- !TryGetGun(user.Value, out var ent, out var gun))
- {
- return;
- }
- if (ent != GetEntity(msg.Gun))
- return;
- gun.ShootCoordinates = GetCoordinates(msg.Coordinates);
- gun.Target = GetEntity(msg.Target);
- AttemptShoot(user.Value, ent, gun);
- }
- private void OnStopShootRequest(RequestStopShootEvent ev, EntitySessionEventArgs args)
- {
- var gunUid = GetEntity(ev.Gun);
- if (args.SenderSession.AttachedEntity == null ||
- !TryComp<GunComponent>(gunUid, out var gun) ||
- !TryGetGun(args.SenderSession.AttachedEntity.Value, out _, out var userGun))
- {
- return;
- }
- if (userGun != gun)
- return;
- StopShooting(gunUid, gun);
- }
- public bool CanShoot(GunComponent component)
- {
- if (component.NextFire > Timing.CurTime)
- return false;
- return true;
- }
- public bool TryGetGun(EntityUid entity, out EntityUid gunEntity, [NotNullWhen(true)] out GunComponent? gunComp)
- {
- gunEntity = default;
- gunComp = null;
- if (EntityManager.TryGetComponent(entity, out HandsComponent? hands) &&
- hands.ActiveHandEntity is { } held &&
- TryComp(held, out GunComponent? gun))
- {
- gunEntity = held;
- gunComp = gun;
- return true;
- }
- // Last resort is check if the entity itself is a gun.
- if (TryComp(entity, out gun))
- {
- gunEntity = entity;
- gunComp = gun;
- return true;
- }
- return false;
- }
- private void StopShooting(EntityUid uid, GunComponent gun)
- {
- if (gun.ShotCounter == 0)
- return;
- gun.ShotCounter = 0;
- gun.ShootCoordinates = null;
- gun.Target = null;
- EntityManager.DirtyField(uid, gun, nameof(GunComponent.ShotCounter));
- }
- /// <summary>
- /// Attempts to shoot at the target coordinates. Resets the shot counter after every shot.
- /// </summary>
- public void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun, EntityCoordinates toCoordinates)
- {
- gun.ShootCoordinates = toCoordinates;
- AttemptShoot(user, gunUid, gun);
- gun.ShotCounter = 0;
- EntityManager.DirtyField(gunUid, gun, nameof(GunComponent.ShotCounter));
- }
- /// <summary>
- /// Shoots by assuming the gun is the user at default coordinates.
- /// </summary>
- public void AttemptShoot(EntityUid gunUid, GunComponent gun)
- {
- var coordinates = new EntityCoordinates(gunUid, gun.DefaultDirection);
- gun.ShootCoordinates = coordinates;
- AttemptShoot(gunUid, gunUid, gun);
- gun.ShotCounter = 0;
- }
- private void AttemptShoot(EntityUid user, EntityUid gunUid, GunComponent gun)
- {
- if (gun.FireRateModified <= 0f ||
- !_actionBlockerSystem.CanAttack(user))
- {
- return;
- }
- var toCoordinates = gun.ShootCoordinates;
- if (toCoordinates == null)
- return;
- var curTime = Timing.CurTime;
- // check if anything wants to prevent shooting
- var prevention = new ShotAttemptedEvent
- {
- User = user,
- Used = (gunUid, gun)
- };
- RaiseLocalEvent(gunUid, ref prevention);
- if (prevention.Cancelled)
- return;
- RaiseLocalEvent(user, ref prevention);
- if (prevention.Cancelled)
- return;
- // Need to do this to play the clicking sound for empty automatic weapons
- // but not play anything for burst fire.
- if (gun.NextFire > curTime)
- return;
- var fireRate = TimeSpan.FromSeconds(1f / gun.FireRateModified);
- if (gun.SelectedMode == SelectiveFire.Burst || gun.BurstActivated)
- fireRate = TimeSpan.FromSeconds(1f / gun.BurstFireRate);
- // First shot
- // Previously we checked shotcounter but in some cases all the bullets got dumped at once
- // curTime - fireRate is insufficient because if you time it just right you can get a 3rd shot out slightly quicker.
- if (gun.NextFire < curTime - fireRate || gun.ShotCounter == 0 && gun.NextFire < curTime)
- gun.NextFire = curTime;
- var shots = 0;
- var lastFire = gun.NextFire;
- while (gun.NextFire <= curTime)
- {
- gun.NextFire += fireRate;
- shots++;
- }
- // NextFire has been touched regardless so need to dirty the gun.
- EntityManager.DirtyField(gunUid, gun, nameof(GunComponent.NextFire));
- // Get how many shots we're actually allowed to make, due to clip size or otherwise.
- // Don't do this in the loop so we still reset NextFire.
- if (!gun.BurstActivated)
- {
- switch (gun.SelectedMode)
- {
- case SelectiveFire.SemiAuto:
- shots = Math.Min(shots, 1 - gun.ShotCounter);
- break;
- case SelectiveFire.Burst:
- shots = Math.Min(shots, gun.ShotsPerBurstModified - gun.ShotCounter);
- break;
- case SelectiveFire.FullAuto:
- break;
- default:
- throw new ArgumentOutOfRangeException($"No implemented shooting behavior for {gun.SelectedMode}!");
- }
- } else
- {
- shots = Math.Min(shots, gun.ShotsPerBurstModified - gun.ShotCounter);
- }
- var attemptEv = new AttemptShootEvent(user, null);
- RaiseLocalEvent(gunUid, ref attemptEv);
- if (attemptEv.Cancelled)
- {
- if (attemptEv.Message != null)
- {
- PopupSystem.PopupClient(attemptEv.Message, gunUid, user);
- }
- gun.BurstActivated = false;
- gun.BurstShotsCount = 0;
- gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds));
- return;
- }
- var fromCoordinates = Transform(user).Coordinates;
- // Remove ammo
- var ev = new TakeAmmoEvent(shots, new List<(EntityUid? Entity, IShootable Shootable)>(), fromCoordinates, user);
- // Listen it just makes the other code around it easier if shots == 0 to do this.
- if (shots > 0)
- RaiseLocalEvent(gunUid, ev);
- DebugTools.Assert(ev.Ammo.Count <= shots);
- DebugTools.Assert(shots >= 0);
- UpdateAmmoCount(gunUid);
- // Even if we don't actually shoot update the ShotCounter. This is to avoid spamming empty sounds
- // where the gun may be SemiAuto or Burst.
- gun.ShotCounter += shots;
- EntityManager.DirtyField(gunUid, gun, nameof(GunComponent.ShotCounter));
- if (ev.Ammo.Count <= 0)
- {
- // triggers effects on the gun if it's empty
- var emptyGunShotEvent = new OnEmptyGunShotEvent();
- RaiseLocalEvent(gunUid, ref emptyGunShotEvent);
- gun.BurstActivated = false;
- gun.BurstShotsCount = 0;
- gun.NextFire += TimeSpan.FromSeconds(gun.BurstCooldown);
- // Play empty gun sounds if relevant
- // If they're firing an existing clip then don't play anything.
- if (shots > 0)
- {
- PopupSystem.PopupCursor(ev.Reason ?? Loc.GetString("gun-magazine-fired-empty"));
- // Don't spam safety sounds at gun fire rate, play it at a reduced rate.
- // May cause prediction issues? Needs more tweaking
- gun.NextFire = TimeSpan.FromSeconds(Math.Max(lastFire.TotalSeconds + SafetyNextFire, gun.NextFire.TotalSeconds));
- Audio.PlayPredicted(gun.SoundEmpty, gunUid, user);
- return;
- }
- return;
- }
- // Handle burstfire
- if (gun.SelectedMode == SelectiveFire.Burst)
- {
- gun.BurstActivated = true;
- }
- if (gun.BurstActivated)
- {
- gun.BurstShotsCount += shots;
- if (gun.BurstShotsCount >= gun.ShotsPerBurstModified)
- {
- gun.NextFire += TimeSpan.FromSeconds(gun.BurstCooldown);
- gun.BurstActivated = false;
- gun.BurstShotsCount = 0;
- }
- }
- // Shoot confirmed - sounds also played here in case it's invalid (e.g. cartridge already spent).
- Shoot(gunUid, gun, ev.Ammo, fromCoordinates, toCoordinates.Value, out var userImpulse, user, throwItems: attemptEv.ThrowItems);
- var shotEv = new GunShotEvent(user, ev.Ammo);
- RaiseLocalEvent(gunUid, ref shotEv);
- if (userImpulse && TryComp<PhysicsComponent>(user, out var userPhysics))
- {
- if (_gravity.IsWeightless(user, userPhysics))
- CauseImpulse(fromCoordinates, toCoordinates.Value, user, userPhysics);
- }
- }
- public void Shoot(
- EntityUid gunUid,
- GunComponent gun,
- EntityUid ammo,
- EntityCoordinates fromCoordinates,
- EntityCoordinates toCoordinates,
- out bool userImpulse,
- EntityUid? user = null,
- bool throwItems = false)
- {
- var shootable = EnsureShootable(ammo);
- Shoot(gunUid, gun, new List<(EntityUid? Entity, IShootable Shootable)>(1) { (ammo, shootable) }, fromCoordinates, toCoordinates, out userImpulse, user, throwItems);
- }
- public abstract void Shoot(
- EntityUid gunUid,
- GunComponent gun,
- List<(EntityUid? Entity, IShootable Shootable)> ammo,
- EntityCoordinates fromCoordinates,
- EntityCoordinates toCoordinates,
- out bool userImpulse,
- EntityUid? user = null,
- bool throwItems = false);
- public void ShootProjectile(EntityUid uid, Vector2 direction, Vector2 gunVelocity, EntityUid gunUid, EntityUid? user = null, float speed = 20f)
- {
- var physics = EnsureComp<PhysicsComponent>(uid);
- Physics.SetBodyStatus(uid, physics, BodyStatus.InAir);
- var targetMapVelocity = gunVelocity + direction.Normalized() * speed;
- var currentMapVelocity = Physics.GetMapLinearVelocity(uid, physics);
- var finalLinear = physics.LinearVelocity + targetMapVelocity - currentMapVelocity;
- Physics.SetLinearVelocity(uid, finalLinear, body: physics);
- var projectile = EnsureComp<ProjectileComponent>(uid);
- Projectiles.SetShooter(uid, projectile, user ?? gunUid);
- projectile.Weapon = gunUid;
- TransformSystem.SetWorldRotation(uid, direction.ToWorldAngle() + projectile.Angle);
- }
- protected abstract void Popup(string message, EntityUid? uid, EntityUid? user);
- /// <summary>
- /// Call this whenever the ammo count for a gun changes.
- /// </summary>
- protected virtual void UpdateAmmoCount(EntityUid uid, bool prediction = true) {}
- protected void SetCartridgeSpent(EntityUid uid, CartridgeAmmoComponent cartridge, bool spent)
- {
- if (cartridge.Spent != spent)
- DirtyField(uid, cartridge, nameof(CartridgeAmmoComponent.Spent));
- cartridge.Spent = spent;
- Appearance.SetData(uid, AmmoVisuals.Spent, spent);
- }
- /// <summary>
- /// Drops a single cartridge / shell
- /// </summary>
- protected void EjectCartridge(
- EntityUid entity,
- Angle? angle = null,
- bool playSound = true)
- {
- // TODO: Sound limit version.
- var offsetPos = Random.NextVector2(EjectOffset);
- var xform = Transform(entity);
- var coordinates = xform.Coordinates;
- coordinates = coordinates.Offset(offsetPos);
- TransformSystem.SetLocalRotation(xform, Random.NextAngle());
- TransformSystem.SetCoordinates(entity, xform, coordinates);
- // decides direction the casing ejects and only when not cycling
- if (angle != null)
- {
- Angle ejectAngle = angle.Value;
- ejectAngle += 3.7f; // 212 degrees; casings should eject slightly to the right and behind of a gun
- ThrowingSystem.TryThrow(entity, ejectAngle.ToVec().Normalized() / 100, 5f);
- }
- if (playSound && TryComp<CartridgeAmmoComponent>(entity, out var cartridge))
- {
- Audio.PlayPvs(cartridge.EjectSound, entity, AudioParams.Default.WithVariation(SharedContentAudioSystem.DefaultVariation).WithVolume(-1f));
- }
- }
- protected IShootable EnsureShootable(EntityUid uid)
- {
- if (TryComp<CartridgeAmmoComponent>(uid, out var cartridge))
- return cartridge;
- return EnsureComp<AmmoComponent>(uid);
- }
- protected void RemoveShootable(EntityUid uid)
- {
- RemCompDeferred<CartridgeAmmoComponent>(uid);
- RemCompDeferred<AmmoComponent>(uid);
- }
- protected void MuzzleFlash(EntityUid gun, AmmoComponent component, Angle worldAngle, EntityUid? user = null)
- {
- var attemptEv = new GunMuzzleFlashAttemptEvent();
- RaiseLocalEvent(gun, ref attemptEv);
- if (attemptEv.Cancelled)
- return;
- var sprite = component.MuzzleFlash;
- if (sprite == null)
- return;
- var ev = new MuzzleFlashEvent(GetNetEntity(gun), sprite, worldAngle);
- CreateEffect(gun, ev, user);
- }
- public void CauseImpulse(EntityCoordinates fromCoordinates, EntityCoordinates toCoordinates, EntityUid user, PhysicsComponent userPhysics)
- {
- var fromMap = fromCoordinates.ToMapPos(EntityManager, TransformSystem);
- var toMap = toCoordinates.ToMapPos(EntityManager, TransformSystem);
- var shotDirection = (toMap - fromMap).Normalized();
- const float impulseStrength = 25.0f;
- var impulseVector = shotDirection * impulseStrength;
- Physics.ApplyLinearImpulse(user, -impulseVector, body: userPhysics);
- }
- public void RefreshModifiers(Entity<GunComponent?> gun)
- {
- if (!Resolve(gun, ref gun.Comp))
- return;
- var comp = gun.Comp;
- var ev = new GunRefreshModifiersEvent(
- (gun, comp),
- comp.SoundGunshot,
- comp.CameraRecoilScalar,
- comp.AngleIncrease,
- comp.AngleDecay,
- comp.MaxAngle,
- comp.MinAngle,
- comp.ShotsPerBurst,
- comp.FireRate,
- comp.ProjectileSpeed
- );
- RaiseLocalEvent(gun, ref ev);
- if (comp.SoundGunshotModified != ev.SoundGunshot)
- {
- comp.SoundGunshotModified = ev.SoundGunshot;
- DirtyField(gun, nameof(GunComponent.SoundGunshotModified));
- }
- if (!MathHelper.CloseTo(comp.CameraRecoilScalarModified, ev.CameraRecoilScalar))
- {
- comp.CameraRecoilScalarModified = ev.CameraRecoilScalar;
- DirtyField(gun, nameof(GunComponent.CameraRecoilScalarModified));
- }
- if (!comp.AngleIncreaseModified.EqualsApprox(ev.AngleIncrease))
- {
- comp.AngleIncreaseModified = ev.AngleIncrease;
- DirtyField(gun, nameof(GunComponent.AngleIncreaseModified));
- }
- if (!comp.AngleDecayModified.EqualsApprox(ev.AngleDecay))
- {
- comp.AngleDecayModified = ev.AngleDecay;
- DirtyField(gun, nameof(GunComponent.AngleDecayModified));
- }
- if (!comp.MaxAngleModified.EqualsApprox(ev.MinAngle))
- {
- comp.MaxAngleModified = ev.MaxAngle;
- DirtyField(gun, nameof(GunComponent.MaxAngleModified));
- }
- if (!comp.MinAngleModified.EqualsApprox(ev.MinAngle))
- {
- comp.MinAngleModified = ev.MinAngle;
- DirtyField(gun, nameof(GunComponent.MinAngleModified));
- }
- if (comp.ShotsPerBurstModified != ev.ShotsPerBurst)
- {
- comp.ShotsPerBurstModified = ev.ShotsPerBurst;
- DirtyField(gun, nameof(GunComponent.ShotsPerBurstModified));
- }
- if (!MathHelper.CloseTo(comp.FireRateModified, ev.FireRate))
- {
- comp.FireRateModified = ev.FireRate;
- DirtyField(gun, nameof(GunComponent.FireRateModified));
- }
- if (!MathHelper.CloseTo(comp.ProjectileSpeedModified, ev.ProjectileSpeed))
- {
- comp.ProjectileSpeedModified = ev.ProjectileSpeed;
- DirtyField(gun, nameof(GunComponent.ProjectileSpeedModified));
- }
- }
- protected abstract void CreateEffect(EntityUid gunUid, MuzzleFlashEvent message, EntityUid? user = null);
- /// <summary>
- /// Used for animated effects on the client.
- /// </summary>
- [Serializable, NetSerializable]
- public sealed class HitscanEvent : EntityEventArgs
- {
- public List<(NetCoordinates coordinates, Angle angle, SpriteSpecifier Sprite, float Distance)> Sprites = new();
- }
- }
- /// <summary>
- /// Raised directed on the gun before firing to see if the shot should go through.
- /// </summary>
- /// <remarks>
- /// Handling this in server exclusively will lead to mispredicts.
- /// </remarks>
- /// <param name="User">The user that attempted to fire this gun.</param>
- /// <param name="Cancelled">Set this to true if the shot should be cancelled.</param>
- /// <param name="ThrowItems">Set this to true if the ammo shouldn't actually be fired, just thrown.</param>
- [ByRefEvent]
- public record struct AttemptShootEvent(EntityUid User, string? Message, bool Cancelled = false, bool ThrowItems = false);
- /// <summary>
- /// Raised directed on the gun after firing.
- /// </summary>
- /// <param name="User">The user that fired this gun.</param>
- [ByRefEvent]
- public record struct GunShotEvent(EntityUid User, List<(EntityUid? Uid, IShootable Shootable)> Ammo);
- public enum EffectLayers : byte
- {
- Unshaded,
- }
- [Serializable, NetSerializable]
- public enum AmmoVisuals : byte
- {
- Spent,
- AmmoCount,
- AmmoMax,
- HasAmmo, // used for generic visualizers. c# stuff can just check ammocount != 0
- MagLoaded,
- BoltClosed,
- }
|