Merge branch 'master' into air-alarm-fixup

This commit is contained in:
Flipp Syder
2022-08-23 13:21:05 -07:00
committed by GitHub
498 changed files with 21068 additions and 52226 deletions

View File

@@ -14,7 +14,10 @@ namespace Content.Shared.Access.Components
[Access(typeof(AccessSystem), Other = AccessPermissions.ReadExecute)] // FIXME Friends
public HashSet<string> Tags = new();
[DataField("groups", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<AccessGroupPrototype>))]
public HashSet<string> Groups = new();
/// <summary>
/// Access Groups. These are added to the tags during map init. After map init this will have no effect.
/// </summary>
[DataField("groups", readOnly: true, customTypeSerializer: typeof(PrototypeIdHashSetSerializer<AccessGroupPrototype>))]
public readonly HashSet<string> Groups = new();
}
}

View File

@@ -9,8 +9,8 @@ namespace Content.Shared.Access.Components
[Access(typeof(SharedIdCardSystem), typeof(SharedPDASystem), typeof(SharedAgentIdCardSystem))]
public sealed class IdCardComponent : Component
{
[DataField("originalOwnerName")]
public string OriginalOwnerName = default!;
[DataField("originalEntityName")]
public string OriginalEntityName = string.Empty;
[DataField("fullName")]
[Access(typeof(SharedIdCardSystem), typeof(SharedPDASystem), typeof(SharedAgentIdCardSystem),

View File

@@ -26,12 +26,14 @@ namespace Content.Shared.Access.Components
public readonly string FullName;
public readonly string JobTitle;
public readonly List<string> AccessList;
public readonly string JobPrototype;
public WriteToTargetIdMessage(string fullName, string jobTitle, List<string> accessList)
public WriteToTargetIdMessage(string fullName, string jobTitle, List<string> accessList, string jobPrototype)
{
FullName = fullName;
JobTitle = jobTitle;
AccessList = accessList;
JobPrototype = jobPrototype;
}
}
@@ -82,6 +84,7 @@ namespace Content.Shared.Access.Components
public readonly string? TargetIdFullName;
public readonly string? TargetIdJobTitle;
public readonly string[]? TargetIdAccessList;
public readonly string TargetIdJobPrototype;
public IdCardConsoleBoundUserInterfaceState(bool isPrivilegedIdPresent,
bool isPrivilegedIdAuthorized,
@@ -89,6 +92,7 @@ namespace Content.Shared.Access.Components
string? targetIdFullName,
string? targetIdJobTitle,
string[]? targetIdAccessList,
string targetIdJobPrototype,
string privilegedIdName,
string targetIdName)
{
@@ -98,6 +102,7 @@ namespace Content.Shared.Access.Components
TargetIdFullName = targetIdFullName;
TargetIdJobTitle = targetIdJobTitle;
TargetIdAccessList = targetIdAccessList;
TargetIdJobPrototype = targetIdJobPrototype;
PrivilegedIdName = privilegedIdName;
TargetIdName = targetIdName;
}

View File

@@ -12,10 +12,10 @@ namespace Content.Shared.Access.Systems
{
base.Initialize();
SubscribeLocalEvent<AccessComponent, ComponentInit>(OnAccessInit);
SubscribeLocalEvent<AccessComponent, MapInitEvent>(OnAccessInit);
}
private void OnAccessInit(EntityUid uid, AccessComponent component, ComponentInit args)
private void OnAccessInit(EntityUid uid, AccessComponent component, MapInitEvent args)
{
// Add all tags in groups to the list of tags.
foreach (var group in component.Groups)

View File

@@ -12,6 +12,7 @@ public sealed class BlockingUserSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly BlockingSystem _blockingSystem = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
public override void Initialize()
{
@@ -54,9 +55,9 @@ public sealed class BlockingUserSystem : EntitySystem
private void OnDamageChanged(EntityUid uid, BlockingUserComponent component, DamageChangedEvent args)
{
if (component.BlockingItem != null)
if (args.DamageDelta != null && args.DamageIncreased)
{
RaiseLocalEvent(component.BlockingItem.Value, args);
_damageable.TryChangeDamage(component.BlockingItem, args.DamageDelta);
}
}

View File

@@ -11,6 +11,8 @@ namespace Content.Shared.Body.Components
[NetworkedComponent()]
public abstract class SharedBodyPartComponent : Component
{
public const string ContainerId = "bodypart";
[Dependency] private readonly IEntityManager _entMan = default!;
private SharedBodyComponent? _body;
@@ -255,9 +257,6 @@ namespace Content.Shared.Body.Components
private void AddedToBody(SharedBodyComponent body)
{
var transformComponent = _entMan.GetComponent<TransformComponent>(Owner);
transformComponent.LocalRotation = 0;
transformComponent.AttachParent(body.Owner);
OnAddedToBody(body);
foreach (var mechanism in _mechanisms)

View File

@@ -5,11 +5,14 @@ using Content.Shared.Movement.Events;
using Content.Shared.Standing;
using Content.Shared.Throwing;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Timing;
namespace Content.Shared.Buckle
{
public abstract class SharedBuckleSystem : EntitySystem
{
[Dependency] protected readonly IGameTiming GameTiming = default!;
public override void Initialize()
{
base.Initialize();
@@ -28,6 +31,20 @@ namespace Content.Shared.Buckle
// TODO: This looks dirty af.
// On rotation of a strap, reattach all buckled entities.
// This fixes buckle offsets and draw depths.
// This is mega cursed. Please somebody save me from Mr Buckle's wild ride.
// Consider a chair that has a player strapped to it. Then the client receives a new server state, showing
// that the player entity has moved elsewhere, and the chair has rotated. If the client applies the player
// state, then the chairs transform comp state, and then the buckle state. The transform state will
// forcefully teleport the player back to the chair (client-side only). This causes even more issues if the
// chair was teleporting in from nullspace after having left PVS.
//
// One option is to just never trigger re-buckles during state application.
// another is to.. just not do this? Like wtf is this code. But I CBF with buckle atm.
if (GameTiming.ApplyingState)
return;
foreach (var buckledEntity in component.BuckledEntities)
{
if (!EntityManager.TryGetComponent(buckledEntity, out SharedBuckleComponent? buckled))
@@ -35,6 +52,12 @@ namespace Content.Shared.Buckle
continue;
}
if (!buckled.Buckled || buckled.LastEntityBuckledTo != uid)
{
Logger.Error($"A moving strap entity {ToPrettyString(uid)} attempted to re-parent an entity that does not 'belong' to it {ToPrettyString(buckledEntity)}");
continue;
}
buckled.ReAttach(component);
Dirty(buckled);
}

View File

@@ -8,6 +8,8 @@ namespace Content.Shared.Construction.Prototypes
[Prototype("construction")]
public sealed class ConstructionPrototype : IPrototype
{
private string _category = string.Empty;
[DataField("conditions")] private List<IConstructionCondition> _conditions = new();
/// <summary>
@@ -52,7 +54,12 @@ namespace Content.Shared.Construction.Prototypes
[DataField("canBuildInImpassable")]
public bool CanBuildInImpassable { get; private set; }
[DataField("category")] public string Category { get; private set; } = string.Empty;
[DataField("category")]
public string Category
{
get => _category;
private set => _category = Loc.GetString(value);
}
[DataField("objectType")] public ConstructionType Type { get; private set; } = ConstructionType.Structure;

View File

@@ -1,11 +1,14 @@
using Content.Shared.Examine;
using Content.Shared.Examine;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Construction.Steps
{
[DataDefinition]
public sealed class PrototypeConstructionGraphStep : ArbitraryInsertConstructionGraphStep
{
[DataField("prototype")] public string Prototype { get; } = string.Empty;
[DataField("prototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>), required:true)]
public string Prototype { get; } = string.Empty;
public override bool EntityValid(EntityUid uid, IEntityManager entityManager)
{

View File

@@ -6,6 +6,8 @@ namespace Content.Shared.Disposal.Components
[NetworkedComponent]
public abstract class SharedDisposalUnitComponent : Component
{
public const string ContainerId = "DisposalUnit";
// TODO: Could maybe turn the contact off instead far more cheaply as farseer (though not box2d) had support for it?
// Need to suss it out.
/// <summary>

View File

@@ -23,6 +23,9 @@ public abstract class SharedDoorSystem : EntitySystem
[Dependency] private readonly SharedStunSystem _stunSystem = default!;
[Dependency] protected readonly TagSystem Tags = default!;
[Dependency] protected readonly IGameTiming GameTiming = default!;
[Dependency] protected readonly SharedAudioSystem Audio = default!;
[Dependency] private readonly EntityLookupSystem _entityLookup = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
/// <summary>
/// A body must have an intersection percentage larger than this in order to be considered as colliding with a
@@ -167,7 +170,7 @@ public abstract class SharedDoorSystem : EntitySystem
if (!TryComp(uid, out AppearanceComponent? appearance))
return;
appearance.SetData(DoorVisuals.State, door.State);
_appearance.SetData(uid, DoorVisuals.State, door.State);
}
#endregion
@@ -199,7 +202,7 @@ public abstract class SharedDoorSystem : EntitySystem
SetState(uid, DoorState.Denying, door);
if (door.DenySound != null)
PlaySound(uid, door.DenySound.GetSound(), AudioParams.Default.WithVolume(-3), user, predicted);
PlaySound(uid, door.DenySound, AudioParams.Default.WithVolume(-3), user, predicted);
}
public bool TryToggleDoor(EntityUid uid, DoorComponent? door = null, EntityUid? user = null, bool predicted = false)
@@ -265,14 +268,14 @@ public abstract class SharedDoorSystem : EntitySystem
SetState(uid, DoorState.Opening, door);
if (door.OpenSound != null)
PlaySound(uid, door.OpenSound.GetSound(), AudioParams.Default.WithVolume(-5), user, predicted);
PlaySound(uid, door.OpenSound, AudioParams.Default.WithVolume(-5), user, predicted);
// I'm not sure what the intent here is/was? It plays a sound if the user is opening a door with a hands
// component, but no actual hands!? What!? Is this the sound of them head-butting the door to get it to open??
// I'm 99% sure something is wrong here, but I kind of want to keep it this way.
if (user != null && TryComp(user.Value, out SharedHandsComponent? hands) && hands.Hands.Count == 0)
PlaySound(uid, door.TryOpenDoorSound.GetSound(), AudioParams.Default.WithVolume(-2), user, predicted);
PlaySound(uid, door.TryOpenDoorSound, AudioParams.Default.WithVolume(-2), user, predicted);
}
/// <summary>
@@ -329,7 +332,7 @@ public abstract class SharedDoorSystem : EntitySystem
SetState(uid, DoorState.Closing, door);
if (door.CloseSound != null)
PlaySound(uid, door.CloseSound.GetSound(), AudioParams.Default.WithVolume(-5), user, predicted);
PlaySound(uid, door.CloseSound, AudioParams.Default.WithVolume(-5), user, predicted);
}
/// <summary>
@@ -422,7 +425,8 @@ public abstract class SharedDoorSystem : EntitySystem
yield break;
// TODO SLOTH fix electro's code.
var doorAABB = physics.GetWorldAABB();
// ReSharper disable once InconsistentNaming
var doorAABB = _entityLookup.GetWorldAABB(uid);
foreach (var otherPhysics in PhysicsSystem.GetCollidingEntities(Transform(uid).MapID, doorAABB))
{
@@ -436,7 +440,7 @@ public abstract class SharedDoorSystem : EntitySystem
&& (otherPhysics.CollisionMask & physics.CollisionLayer) == 0)
continue;
if (otherPhysics.GetWorldAABB().IntersectPercentage(doorAABB) < IntersectPercentage)
if (_entityLookup.GetWorldAABB(otherPhysics.Owner).IntersectPercentage(doorAABB) < IntersectPercentage)
continue;
yield return otherPhysics.Owner;
@@ -614,5 +618,5 @@ public abstract class SharedDoorSystem : EntitySystem
}
#endregion
protected abstract void PlaySound(EntityUid uid, string sound, AudioParams audioParams, EntityUid? predictingPlayer, bool predicted);
protected abstract void PlaySound(EntityUid uid, SoundSpecifier soundSpecifier, AudioParams audioParams, EntityUid? predictingPlayer, bool predicted);
}

View File

@@ -34,14 +34,12 @@ namespace Content.Shared.Entry
{
base.PostInit();
_initTileDefinitions();
InitTileDefinitions();
IoCManager.Resolve<SpriteAccessoryManager>().Initialize();
IoCManager.Resolve<MarkingManager>().Initialize();
var configMan = IoCManager.Resolve<IConfigurationManager>();
#if FULL_RELEASE
configMan.OverrideDefault(CVars.NetInterpRatio, 2);
#else
#if DEBUG
configMan.OverrideDefault(CVars.NetFakeLagMin, 0.075f);
configMan.OverrideDefault(CVars.NetFakeLoss, 0.005f);
configMan.OverrideDefault(CVars.NetFakeDuplicates, 0.005f);
@@ -50,10 +48,9 @@ namespace Content.Shared.Entry
// just leaving this disabled.
// configMan.OverrideDefault(CVars.NetFakeLagRand, 0.01f);
#endif
}
private void _initTileDefinitions()
private void InitTileDefinitions()
{
// Register space first because I'm a hard coding hack.
var spaceDef = _prototypeManager.Index<ContentTileDefinition>(ContentTileDefinition.SpaceID);

View File

@@ -51,6 +51,17 @@ namespace Content.Shared.Movement.Systems
Dirty(move);
}
public void ChangeBaseSpeed(EntityUid uid, float baseWalkSpeed, float baseSprintSpeed, float acceleration, MovementSpeedModifierComponent? move = null)
{
if (!Resolve(uid, ref move, false))
return;
move.BaseWalkSpeed = baseWalkSpeed;
move.BaseSprintSpeed = baseSprintSpeed;
move.Acceleration = acceleration;
Dirty(move);
}
[Serializable, NetSerializable]
private sealed class MovementSpeedModifierComponentState : ComponentState
{

View File

@@ -233,6 +233,9 @@ namespace Content.Shared.Movement.Systems
Accelerate(ref velocity, in worldTotal, accel, frameTime);
PhysicsSystem.SetLinearVelocity(physicsComponent, velocity);
// Ensures that players do not spiiiiiiin
PhysicsSystem.SetAngularVelocity(physicsComponent, 0);
}
private void Friction(float minimumFrictionSpeed, float frameTime, float friction, ref Vector2 velocity)

View File

@@ -1,17 +0,0 @@
namespace Content.Shared.PDA
{
public enum UplinkCategory
{
Weapons,
Ammo,
Explosives,
Misc,
Bundles,
Tools,
Utility,
Job,
Armor,
Pointless,
}
}

View File

@@ -1,40 +0,0 @@
using Content.Shared.Roles;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
using Robust.Shared.Utility;
namespace Content.Shared.PDA
{
[Prototype("uplinkListing")]
public sealed class UplinkStoreListingPrototype : IPrototype
{
[ViewVariables]
[IdDataFieldAttribute]
public string ID { get; } = default!;
[DataField("itemId", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
public string ItemId { get; } = string.Empty;
[DataField("price")]
public int Price { get; } = 5;
[DataField("category")]
public UplinkCategory Category { get; } = UplinkCategory.Utility;
[DataField("description")]
public string Description { get; } = string.Empty;
[DataField("listingName")]
public string ListingName { get; } = string.Empty;
[DataField("icon")]
public SpriteSpecifier? Icon { get; } = null;
[DataField("jobWhitelist", customTypeSerializer:typeof(PrototypeIdHashSetSerializer<JobPrototype>))]
public HashSet<string>? JobWhitelist;
[DataField("surplus")]
public bool CanSurplus = true;
}
}

View File

@@ -40,49 +40,6 @@ namespace Content.Shared.Pulling.Components
[ViewVariables]
public bool PrevFixedRotation;
public override ComponentState GetComponentState()
{
return new PullableComponentState(Puller);
}
public override void HandleComponentState(ComponentState? curState, ComponentState? nextState)
{
base.HandleComponentState(curState, nextState);
if (curState is not PullableComponentState state)
{
return;
}
if (!state.Puller.HasValue)
{
EntitySystem.Get<SharedPullingStateManagementSystem>().ForceDisconnectPullable(this);
return;
}
if (!state.Puller.Value.IsValid())
{
Logger.Error($"Invalid entity {state.Puller.Value} for pulling");
return;
}
if (Puller == state.Puller)
{
// don't disconnect and reconnect a puller for no reason
return;
}
if (!IoCManager.Resolve<IEntityManager>().TryGetComponent<SharedPullerComponent?>(state.Puller.Value, out var comp))
{
Logger.Error($"Entity {state.Puller.Value} for pulling had no Puller component");
// ensure it disconnects from any different puller, still
EntitySystem.Get<SharedPullingStateManagementSystem>().ForceDisconnectPullable(this);
return;
}
EntitySystem.Get<SharedPullingStateManagementSystem>().ForceRelationship(comp, this);
}
protected override void OnRemove()
{
if (Puller != null)

View File

@@ -2,6 +2,7 @@ using System.Linq;
using Content.Shared.Physics.Pull;
using Content.Shared.Pulling.Components;
using JetBrains.Annotations;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Timing;
@@ -24,6 +25,41 @@ namespace Content.Shared.Pulling
base.Initialize();
SubscribeLocalEvent<SharedPullableComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<SharedPullableComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<SharedPullableComponent, ComponentHandleState>(OnHandleState);
}
private void OnGetState(EntityUid uid, SharedPullableComponent component, ref ComponentGetState args)
{
args.State = new PullableComponentState(component.Puller);
}
private void OnHandleState(EntityUid uid, SharedPullableComponent component, ref ComponentHandleState args)
{
if (args.Current is not PullableComponentState state)
return;
if (!state.Puller.HasValue)
{
ForceDisconnectPullable(component);
return;
}
if (component.Puller == state.Puller)
{
// don't disconnect and reconnect a puller for no reason
return;
}
if (!TryComp<SharedPullerComponent?>(state.Puller.Value, out var comp))
{
Logger.Error($"Pullable state for entity {ToPrettyString(uid)} had invalid puller entity {ToPrettyString(state.Puller.Value)}");
// ensure it disconnects from any different puller, still
ForceDisconnectPullable(component);
return;
}
ForceRelationship(comp, component);
}
private void OnShutdown(EntityUid uid, SharedPullableComponent component, ComponentShutdown args)

View File

@@ -1,30 +0,0 @@
using Content.Shared.Actions.ActionTypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Utility;
namespace Content.Shared.Revenant;
[Serializable]
[Prototype("revenantListing")]
public sealed class RevenantStoreListingPrototype : IPrototype
{
[ViewVariables]
[IdDataField]
public string ID { get; } = default!;
[DataField("actionId", customTypeSerializer:typeof(PrototypeIdSerializer<InstantActionPrototype>))]
public string ActionId { get; } = string.Empty;
[DataField("price")]
public int Price { get; } = 5;
[DataField("description")]
public string Description { get; } = string.Empty;
[DataField("listingName")]
public string ListingName { get; } = string.Empty;
[DataField("icon")]
public SpriteSpecifier? Icon { get; } = null;
}

View File

@@ -16,34 +16,3 @@ public enum RevenantVisuals : byte
Stunned,
Harvesting,
}
[NetSerializable, Serializable]
public enum RevenantUiKey : byte
{
Key
}
[Serializable, NetSerializable]
public sealed class RevenantUpdateState : BoundUserInterfaceState
{
public float Essence;
public readonly List<RevenantStoreListingPrototype> Listings;
public RevenantUpdateState(float essence, List<RevenantStoreListingPrototype> listings)
{
Essence = essence;
Listings = listings;
}
}
[Serializable, NetSerializable]
public sealed class RevenantBuyListingMessage : BoundUserInterfaceMessage
{
public RevenantStoreListingPrototype Listing;
public RevenantBuyListingMessage (RevenantStoreListingPrototype listing)
{
Listing = listing;
}
}

View File

@@ -0,0 +1,43 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Store;
/// <summary>
/// Prototype used to define different types of currency for generic stores.
/// Mainly used for antags, such as traitors, nukies, and revenants
/// This is separate to the cargo ordering system.
/// </summary>
[Prototype("currency")]
[DataDefinition, Serializable, NetSerializable]
public sealed class CurrencyPrototype : IPrototype
{
[ViewVariables]
[IdDataField]
public string ID { get; } = default!;
/// <summary>
/// The Loc string used for displaying the balance of a certain currency at the top of the store ui
/// </summary>
[DataField("balanceDisplay")]
public string BalanceDisplay { get; } = string.Empty;
/// <summary>
/// The Loc string used for displaying the price of listings in store UI
/// </summary>
[DataField("priceDisplay")]
public string PriceDisplay { get; } = string.Empty;
/// <summary>
/// The physical entity of the currency
/// </summary>
[DataField("entityId", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? EntityId { get; }
/// <summary>
/// Whether or not this currency can be withdrawn from a shop by a player. Requires a valid entityId.
/// </summary>
[DataField("canWithdraw")]
public bool CanWithdraw { get; } = true;
}

View File

@@ -0,0 +1,23 @@
using JetBrains.Annotations;
using Robust.Shared.Serialization;
namespace Content.Shared.Store;
/// <summary>
/// Used to define a complicated condition that requires C#
/// </summary>
[ImplicitDataDefinitionForInheritors]
[MeansImplicitUse]
public abstract class ListingCondition
{
/// <summary>
/// Determines whether or not a certain entity can purchase a listing.
/// </summary>
/// <returns>Whether or not the listing can be purchased</returns>
public abstract bool Condition(ListingConditionArgs args);
}
/// <param name="Buyer">The person purchasing the listing</param>
/// <param name="Listing">The liting itself</param>
/// <param name="EntityManager">An entitymanager for sane coding</param>
public readonly record struct ListingConditionArgs(EntityUid Buyer, EntityUid? StoreEntity, ListingData Listing, IEntityManager EntityManager);

View File

@@ -0,0 +1,133 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Utility;
using Content.Shared.Actions.ActionTypes;
using Content.Shared.FixedPoint;
using System.Linq;
namespace Content.Shared.Store;
/// <summary>
/// This is the data object for a store listing which is passed around in code.
/// this allows for prices and features of listings to be dynamically changed in code
/// without having to modify the prototypes.
/// </summary>
[Serializable, NetSerializable]
[Virtual, DataDefinition]
public class ListingData : IEquatable<ListingData>
{
/// <summary>
/// The name of the listing. If empty, uses the entity's name (if present)
/// </summary>
[DataField("name")]
public string Name = string.Empty;
/// <summary>
/// The description of the listing. If empty, uses the entity's description (if present)
/// </summary>
[DataField("description")]
public string Description = string.Empty;
/// <summary>
/// The categories that this listing applies to. Used for filtering a listing for a store.
/// </summary>
[DataField("categories", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<StoreCategoryPrototype>))]
public List<string> Categories = new();
/// <summary>
/// The cost of the listing. String represents the currency type while the FixedPoint2 represents the amount of that currency.
/// </summary>
[DataField("cost", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<FixedPoint2, CurrencyPrototype>))]
public Dictionary<string, FixedPoint2> Cost = new();
/// <summary>
/// Specific customizeable conditions that determine whether or not the listing can be purchased.
/// </summary>
[NonSerialized]
[DataField("conditions", serverOnly: true)]
public List<ListingCondition>? Conditions;
/// <summary>
/// The icon for the listing. If null, uses the icon for the entity or action.
/// </summary>
[DataField("icon")]
public SpriteSpecifier? Icon;
/// <summary>
/// The priority for what order the listings will show up in on the menu.
/// </summary>
[DataField("priority")]
public int Priority = 0;
/// <summary>
/// The entity that is given when the listing is purchased.
/// </summary>
[DataField("productEntity", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? ProductEntity;
/// <summary>
/// The action that is given when the listing is purchased.
/// </summary>
[DataField("productAction", customTypeSerializer: typeof(PrototypeIdSerializer<InstantActionPrototype>))]
public string? ProductAction;
/// <summary>
/// The event that is broadcast when the listing is purchased.
/// </summary>
[DataField("productEvent")]
public object? ProductEvent;
/// <summary>
/// used internally for tracking how many times an item was purchased.
/// </summary>
public int PurchaseAmount = 0;
public bool Equals(ListingData? listing)
{
if (listing == null)
return false;
//simple conditions
if (Priority != listing.Priority ||
Name != listing.Name ||
Description != listing.Description ||
ProductEntity != listing.ProductEntity ||
ProductAction != listing.ProductAction ||
ProductEvent != listing.ProductEvent)
return false;
if (Icon != null && !Icon.Equals(listing.Icon))
return false;
///more complicated conditions that eat perf. these don't really matter
///as much because you will very rarely have to check these.
if (!Categories.OrderBy(x => x).SequenceEqual(listing.Categories.OrderBy(x => x)))
return false;
if (!Cost.OrderBy(x => x).SequenceEqual(listing.Cost.OrderBy(x => x)))
return false;
if ((Conditions != null && listing.Conditions != null) &&
!Conditions.OrderBy(x => x).SequenceEqual(listing.Conditions.OrderBy(x => x)))
return false;
return true;
}
}
//<inheritdoc>
/// <summary>
/// Defines a set item listing that is available in a store
/// </summary>
[Prototype("listing")]
[Serializable, NetSerializable]
[DataDefinition]
public sealed class ListingPrototype : ListingData, IPrototype
{
[ViewVariables]
[IdDataField]
public string ID { get; } = default!;
}

View File

@@ -0,0 +1,22 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Store;
/// <summary>
/// Used to define different categories for a store.
/// </summary>
[Prototype("storeCategory")]
[Serializable, NetSerializable, DataDefinition]
public sealed class StoreCategoryPrototype : IPrototype
{
[ViewVariables]
[IdDataField]
public string ID { get; } = default!;
[DataField("name")]
public string Name { get; } = string.Empty;
[DataField("priority")]
public int Priority { get; } = 0;
}

View File

@@ -0,0 +1,41 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
using Content.Shared.FixedPoint;
namespace Content.Shared.Store;
/// <summary>
/// Specifies generic info for initializing a store.
/// </summary>
[Prototype("storePreset")]
[DataDefinition]
public sealed class StorePresetPrototype : IPrototype
{
[ViewVariables] [IdDataField] public string ID { get; } = default!;
/// <summary>
/// The name displayed at the top of the store window
/// </summary>
[DataField("storeName", required: true)]
public string StoreName { get; } = string.Empty;
/// <summary>
/// The categories that this store can access
/// </summary>
[DataField("categories", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<StoreCategoryPrototype>))]
public HashSet<string> Categories { get; } = new();
/// <summary>
/// The inital balance that the store initializes with.
/// </summary>
[DataField("initialBalance",
customTypeSerializer: typeof(PrototypeIdDictionarySerializer<FixedPoint2, CurrencyPrototype>))]
public Dictionary<string, FixedPoint2>? InitialBalance { get; }
/// <summary>
/// The currencies that are accepted in the store
/// </summary>
[DataField("currencyWhitelist", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<CurrencyPrototype>))]
public HashSet<string> CurrencyWhitelist { get; } = new();
}

View File

@@ -0,0 +1,84 @@
using Content.Shared.FixedPoint;
using Content.Shared.MobState;
using Robust.Shared.Serialization;
namespace Content.Shared.Store;
[Serializable, NetSerializable]
public enum StoreUiKey : byte
{
Key
}
[Serializable, NetSerializable]
public sealed class StoreUpdateState : BoundUserInterfaceState
{
public readonly EntityUid? Buyer;
public readonly HashSet<ListingData> Listings;
public readonly Dictionary<string, FixedPoint2> Balance;
public StoreUpdateState(EntityUid? buyer, HashSet<ListingData> listings, Dictionary<string, FixedPoint2> balance)
{
Buyer = buyer;
Listings = listings;
Balance = balance;
}
}
/// <summary>
/// initializes miscellaneous data about the store.
/// </summary>
[Serializable, NetSerializable]
public sealed class StoreInitializeState : BoundUserInterfaceState
{
public readonly string Name;
public StoreInitializeState(string name)
{
Name = name;
}
}
[Serializable, NetSerializable]
public sealed class StoreRequestUpdateInterfaceMessage : BoundUserInterfaceMessage
{
public EntityUid CurrentBuyer;
public StoreRequestUpdateInterfaceMessage(EntityUid currentBuyer)
{
CurrentBuyer = currentBuyer;
}
}
[Serializable, NetSerializable]
public sealed class StoreBuyListingMessage : BoundUserInterfaceMessage
{
public EntityUid Buyer;
public ListingData Listing;
public StoreBuyListingMessage(EntityUid buyer, ListingData listing)
{
Buyer = buyer;
Listing = listing;
}
}
[Serializable, NetSerializable]
public sealed class StoreRequestWithdrawMessage : BoundUserInterfaceMessage
{
public EntityUid Buyer;
public string Currency;
public int Amount;
public StoreRequestWithdrawMessage(EntityUid buyer, string currency, int amount)
{
Buyer = buyer;
Currency = currency;
Amount = amount;
}
}

View File

@@ -1,21 +1,47 @@
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Tools.Components
{
[NetworkedComponent]
public abstract class SharedMultipleToolComponent : Component
{
[DataDefinition]
public sealed class ToolEntry
{
[DataField("behavior", required: true)]
public PrototypeFlags<ToolQualityPrototype> Behavior { get; } = new();
[DataField("useSound")]
public SoundSpecifier? Sound { get; } = null;
[DataField("changeSound")]
public SoundSpecifier? ChangeSound { get; } = null;
[DataField("sprite")]
public SpriteSpecifier? Sprite { get; } = null;
}
[DataField("entries", required: true)]
public ToolEntry[] Entries { get; } = Array.Empty<ToolEntry>();
[ViewVariables]
public uint CurrentEntry = 0;
[ViewVariables]
public string CurrentQualityName = String.Empty;
}
[NetSerializable, Serializable]
public sealed class MultipleToolComponentState : ComponentState
{
public string QualityName { get; }
public readonly uint Selected;
public MultipleToolComponentState(string qualityName)
public MultipleToolComponentState(uint selected)
{
QualityName = qualityName;
Selected = selected;
}
}
}

View File

@@ -0,0 +1,93 @@
using System.Linq;
using Content.Shared.Interaction;
using Content.Shared.Tools.Components;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Tools;
public abstract class SharedToolSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _protoMan = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
public override void Initialize()
{
SubscribeLocalEvent<SharedMultipleToolComponent, ComponentStartup>(OnMultipleToolStartup);
SubscribeLocalEvent<SharedMultipleToolComponent, ActivateInWorldEvent>(OnMultipleToolActivated);
SubscribeLocalEvent<SharedMultipleToolComponent, ComponentGetState>(OnMultipleToolGetState);
SubscribeLocalEvent<SharedMultipleToolComponent, ComponentHandleState>(OnMultipleToolHandleState);
}
private void OnMultipleToolHandleState(EntityUid uid, SharedMultipleToolComponent component, ref ComponentHandleState args)
{
if (args.Current is not MultipleToolComponentState state)
return;
component.CurrentEntry = state.Selected;
SetMultipleTool(uid, component);
}
private void OnMultipleToolStartup(EntityUid uid, SharedMultipleToolComponent multiple, ComponentStartup args)
{
// Only set the multiple tool if we have a tool component.
if(EntityManager.TryGetComponent(uid, out ToolComponent? tool))
SetMultipleTool(uid, multiple, tool);
}
private void OnMultipleToolActivated(EntityUid uid, SharedMultipleToolComponent multiple, ActivateInWorldEvent args)
{
if (args.Handled)
return;
args.Handled = CycleMultipleTool(uid, multiple, args.User);
}
private void OnMultipleToolGetState(EntityUid uid, SharedMultipleToolComponent multiple, ref ComponentGetState args)
{
args.State = new MultipleToolComponentState(multiple.CurrentEntry);
}
public bool CycleMultipleTool(EntityUid uid, SharedMultipleToolComponent? multiple = null, EntityUid? user = null)
{
if (!Resolve(uid, ref multiple))
return false;
if (multiple.Entries.Length == 0)
return false;
multiple.CurrentEntry = (uint) ((multiple.CurrentEntry + 1) % multiple.Entries.Length);
SetMultipleTool(uid, multiple, playSound: true, user: user);
return true;
}
public virtual void SetMultipleTool(EntityUid uid,
SharedMultipleToolComponent? multiple = null,
ToolComponent? tool = null,
bool playSound = false,
EntityUid? user = null)
{
if (!Resolve(uid, ref multiple, ref tool))
return;
Dirty(multiple);
if (multiple.Entries.Length <= multiple.CurrentEntry)
{
multiple.CurrentQualityName = Loc.GetString("multiple-tool-component-no-behavior");
return;
}
var current = multiple.Entries[multiple.CurrentEntry];
tool.UseSound = current.Sound;
tool.Qualities = current.Behavior;
if (playSound && current.ChangeSound != null)
_audioSystem.PlayPredicted(current.ChangeSound, uid, user);
if (_protoMan.TryIndex(current.Behavior.First(), out ToolQualityPrototype? quality))
multiple.CurrentQualityName = Loc.GetString(quality.Name);
}
}

View File

@@ -1,14 +0,0 @@
namespace Content.Shared.Traitor.Uplink
{
public sealed class UplinkAccount
{
public readonly EntityUid? AccountHolder;
public int Balance;
public UplinkAccount(int startingBalance, EntityUid? accountHolder = null)
{
AccountHolder = accountHolder;
Balance = startingBalance;
}
}
}

View File

@@ -1,17 +0,0 @@
using Content.Shared.Roles;
using Robust.Shared.Serialization;
namespace Content.Shared.Traitor.Uplink
{
[Serializable, NetSerializable]
public sealed class UplinkAccountData
{
public EntityUid? DataAccountHolder;
public int DataBalance;
public UplinkAccountData(EntityUid? dataAccountHolder, int dataBalance)
{
DataAccountHolder = dataAccountHolder;
DataBalance = dataBalance;
}
}
}

View File

@@ -1,41 +0,0 @@
using Content.Shared.PDA;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Traitor.Uplink
{
[Serializable, NetSerializable]
public sealed class UplinkListingData : ComponentState, IEquatable<UplinkListingData>
{
public readonly string ItemId;
public readonly int Price;
public readonly UplinkCategory Category;
public readonly string Description;
public readonly string ListingName;
public readonly SpriteSpecifier? Icon;
public readonly HashSet<string>? JobWhitelist;
public UplinkListingData(string listingName, string itemId,
int price, UplinkCategory category,
string description, SpriteSpecifier? icon, HashSet<string>? jobWhitelist)
{
ListingName = listingName;
Price = price;
Category = category;
Description = description;
ItemId = itemId;
Icon = icon;
JobWhitelist = jobWhitelist;
}
public bool Equals(UplinkListingData? other)
{
if (other == null)
{
return false;
}
return ItemId == other.ItemId;
}
}
}

View File

@@ -1,35 +0,0 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Traitor.Uplink
{
[Serializable, NetSerializable]
public sealed class UplinkBuyListingMessage : BoundUserInterfaceMessage
{
public string ItemId;
public UplinkBuyListingMessage(string itemId)
{
ItemId = itemId;
}
}
[Serializable, NetSerializable]
public sealed class UplinkRequestUpdateInterfaceMessage : BoundUserInterfaceMessage
{
public UplinkRequestUpdateInterfaceMessage()
{
}
}
[Serializable, NetSerializable]
public sealed class UplinkTryWithdrawTC : BoundUserInterfaceMessage
{
public int TC;
public UplinkTryWithdrawTC(int tc)
{
TC = tc;
}
}
}

View File

@@ -1,14 +0,0 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Traitor.Uplink
{
[Serializable, NetSerializable]
public sealed class UplinkBuySuccessMessage : EntityEventArgs
{
}
[Serializable, NetSerializable]
public sealed class UplinkInsufficientFundsMessage : EntityEventArgs
{
}
}

View File

@@ -1,17 +0,0 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Traitor.Uplink
{
[Serializable, NetSerializable]
public sealed class UplinkUpdateState : BoundUserInterfaceState
{
public UplinkAccountData Account;
public UplinkListingData[] Listings;
public UplinkUpdateState(UplinkAccountData account, UplinkListingData[] listings)
{
Account = account;
Listings = listings;
}
}
}

View File

@@ -1,10 +0,0 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Traitor.Uplink
{
[Serializable, NetSerializable]
public enum UplinkUiKey : byte
{
Key
}
}

View File

@@ -13,12 +13,12 @@ public sealed class FlyBySoundComponent : Component
/// Probability that the sound plays
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("prob")]
public float Prob = 0.75f;
public float Prob = 0.10f;
[ViewVariables(VVAccess.ReadWrite), DataField("sound")]
public SoundSpecifier Sound = new SoundCollectionSpecifier("BulletMiss")
{
Params = AudioParams.Default.WithVolume(5f),
Params = AudioParams.Default,
};
[ViewVariables, DataField("range")] public float Range = 1.5f;

View File

@@ -1,5 +1,6 @@
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
@@ -16,6 +17,7 @@ public abstract partial class SharedGunSystem
protected virtual void InitializeBallistic()
{
SubscribeLocalEvent<BallisticAmmoProviderComponent, ComponentInit>(OnBallisticInit);
SubscribeLocalEvent<BallisticAmmoProviderComponent, MapInitEvent>(OnBallisticMapInit);
SubscribeLocalEvent<BallisticAmmoProviderComponent, TakeAmmoEvent>(OnBallisticTakeAmmo);
SubscribeLocalEvent<BallisticAmmoProviderComponent, GetAmmoCountEvent>(OnBallisticAmmoCount);
SubscribeLocalEvent<BallisticAmmoProviderComponent, ComponentGetState>(OnBallisticGetState);
@@ -24,10 +26,10 @@ public abstract partial class SharedGunSystem
SubscribeLocalEvent<BallisticAmmoProviderComponent, ExaminedEvent>(OnBallisticExamine);
SubscribeLocalEvent<BallisticAmmoProviderComponent, GetVerbsEvent<Verb>>(OnBallisticVerb);
SubscribeLocalEvent<BallisticAmmoProviderComponent, InteractUsingEvent>(OnBallisticInteractUsing);
SubscribeLocalEvent<BallisticAmmoProviderComponent, ActivateInWorldEvent>(OnBallisticActivate);
SubscribeLocalEvent<BallisticAmmoProviderComponent, UseInHandEvent>(OnBallisticUse);
}
private void OnBallisticActivate(EntityUid uid, BallisticAmmoProviderComponent component, ActivateInWorldEvent args)
private void OnBallisticUse(EntityUid uid, BallisticAmmoProviderComponent component, UseInHandEvent args)
{
ManualCycle(component, Transform(uid).MapPosition, args.User);
args.Handled = true;
@@ -62,6 +64,9 @@ public abstract partial class SharedGunSystem
private void OnBallisticExamine(EntityUid uid, BallisticAmmoProviderComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
args.PushMarkup(Loc.GetString("gun-magazine-examine", ("color", AmmoExamineColor), ("count", GetBallisticShots(component))));
}
@@ -122,15 +127,16 @@ public abstract partial class SharedGunSystem
private void OnBallisticInit(EntityUid uid, BallisticAmmoProviderComponent component, ComponentInit args)
{
component.Container = Containers.EnsureContainer<Container>(uid, "ballistic-ammo");
component.UnspawnedCount = component.Capacity;
}
private void OnBallisticMapInit(EntityUid uid, BallisticAmmoProviderComponent component, MapInitEvent args)
{
// TODO this should be part of the prototype, not set on map init.
// Alternatively, just track spawned count, instead of unspawned count.
if (component.FillProto != null)
{
component.UnspawnedCount -= Math.Min(component.UnspawnedCount, component.Container.ContainedEntities.Count);
}
else
{
component.UnspawnedCount = 0;
component.UnspawnedCount = Math.Max(0, component.Capacity - component.Container.ContainedEntities.Count);
Dirty(component);
}
}
@@ -151,15 +157,17 @@ public abstract partial class SharedGunSystem
{
entity = component.Entities[^1];
args.Ammo.Add(EnsureComp<AmmoComponent>(entity));
// Leave the entity as is if it doesn't auto cycle
// TODO: Suss this out with NewAmmoComponent as I don't think it gets removed from container properly
if (HasComp<CartridgeAmmoComponent>(entity) && component.AutoCycle)
if (!component.AutoCycle)
{
component.Entities.RemoveAt(component.Entities.Count - 1);
component.Container.Remove(entity);
return;
}
args.Ammo.Add(EnsureComp<AmmoComponent>(entity));
component.Entities.RemoveAt(component.Entities.Count - 1);
component.Container.Remove(entity);
}
else if (component.UnspawnedCount > 0)
{

View File

@@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
@@ -18,12 +19,15 @@ public abstract partial class SharedGunSystem
SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, TakeAmmoEvent>(OnChamberMagazineTakeAmmo);
SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, GetVerbsEvent<Verb>>(OnMagazineVerb);
SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, ItemSlotChangedEvent>(OnMagazineSlotChange);
SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, ActivateInWorldEvent>(OnMagazineActivate);
SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, UseInHandEvent>(OnMagazineUse);
SubscribeLocalEvent<ChamberMagazineAmmoProviderComponent, ExaminedEvent>(OnChamberMagazineExamine);
}
private void OnChamberMagazineExamine(EntityUid uid, ChamberMagazineAmmoProviderComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
var (count, _) = GetChamberMagazineCountCapacity(component);
args.PushMarkup(Loc.GetString("gun-magazine-examine", ("color", AmmoExamineColor), ("count", count)));
}

View File

@@ -12,6 +12,9 @@ public abstract partial class SharedGunSystem
{
private void OnExamine(EntityUid uid, GunComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
args.PushMarkup(Loc.GetString("gun-selected-mode-examine", ("color", ModeExamineColor), ("mode", GetLocSelector(component.SelectedMode))));
args.PushMarkup(Loc.GetString("gun-fire-rate-examine", ("color", FireRateExamineColor), ("fireRate", component.FireRate)));
}

View File

@@ -1,6 +1,7 @@
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
@@ -17,17 +18,20 @@ public abstract partial class SharedGunSystem
SubscribeLocalEvent<MagazineAmmoProviderComponent, TakeAmmoEvent>(OnMagazineTakeAmmo);
SubscribeLocalEvent<MagazineAmmoProviderComponent, GetVerbsEvent<Verb>>(OnMagazineVerb);
SubscribeLocalEvent<MagazineAmmoProviderComponent, ItemSlotChangedEvent>(OnMagazineSlotChange);
SubscribeLocalEvent<MagazineAmmoProviderComponent, ActivateInWorldEvent>(OnMagazineActivate);
SubscribeLocalEvent<MagazineAmmoProviderComponent, UseInHandEvent>(OnMagazineUse);
SubscribeLocalEvent<MagazineAmmoProviderComponent, ExaminedEvent>(OnMagazineExamine);
}
private void OnMagazineExamine(EntityUid uid, MagazineAmmoProviderComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
var (count, _) = GetMagazineCountCapacity(component);
args.PushMarkup(Loc.GetString("gun-magazine-examine", ("color", AmmoExamineColor), ("count", count)));
}
private void OnMagazineActivate(EntityUid uid, MagazineAmmoProviderComponent component, ActivateInWorldEvent args)
private void OnMagazineUse(EntityUid uid, MagazineAmmoProviderComponent component, UseInHandEvent args)
{
var magEnt = GetMagazineEntity(uid);

View File

@@ -45,6 +45,7 @@ public abstract partial class SharedGunSystem : EntitySystem
[Dependency] protected readonly SharedPopupSystem PopupSystem = default!;
[Dependency] protected readonly ThrowingSystem ThrowingSystem = default!;
[Dependency] protected readonly TagSystem TagSystem = default!;
[Dependency] protected readonly SharedAudioSystem Audio = default!;
[Dependency] protected readonly SharedProjectileSystem Projectiles = default!;
protected ISawmill Sawmill = default!;
@@ -154,19 +155,18 @@ public abstract partial class SharedGunSystem : EntitySystem
public GunComponent? GetGun(EntityUid entity)
{
if (!EntityManager.TryGetComponent(entity, out SharedHandsComponent? hands) ||
hands.ActiveHandEntity is not { } held)
{
return null;
}
if (!EntityManager.TryGetComponent(held, out GunComponent? gun))
return null;
if (!_combatMode.IsInCombatMode(entity))
return null;
return gun;
if (EntityManager.TryGetComponent(entity, out SharedHandsComponent? hands) &&
hands.ActiveHandEntity is { } held &&
TryComp(held, out GunComponent? gun))
{
return gun;
}
// Last resort is check if the entity itself is a gun.
return !TryComp(entity, out gun) ? null : gun;
}
private void StopShooting(GunComponent gun)