Merge branch 'master' of https://github.com/space-wizards/space-station-14 into map-load-refactor

This commit is contained in:
ElectroJr
2025-02-16 16:52:51 +13:00
1624 changed files with 385006 additions and 263837 deletions

View File

@@ -76,7 +76,7 @@ public sealed partial class AccessReaderComponent : Component
/// Whether or not emag interactions have an effect on this.
/// </summary>
[DataField]
public bool BreakOnEmag = true;
public bool BreakOnAccessBreaker = true;
}
[DataDefinition, Serializable, NetSerializable]

View File

@@ -22,6 +22,8 @@ public sealed partial class IdCardComponent : Component
[Access(typeof(SharedIdCardSystem), typeof(SharedPdaSystem), typeof(SharedAgentIdCardSystem), Other = AccessPermissions.ReadWrite)]
public LocId? JobTitle;
[DataField]
[AutoNetworkedField]
private string? _jobTitle;
[Access(typeof(SharedIdCardSystem), typeof(SharedPdaSystem), typeof(SharedAgentIdCardSystem), Other = AccessPermissions.ReadWriteExecute)]

View File

@@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.Access.Components;
using Content.Shared.DeviceLinking.Events;
using Content.Shared.Emag.Components;
using Content.Shared.Emag.Systems;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Inventory;
@@ -24,6 +23,7 @@ public sealed class AccessReaderSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly SharedGameTicker _gameTicker = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
@@ -71,17 +71,28 @@ public sealed class AccessReaderSystem : EntitySystem
{
if (args.User == null) // AutoLink (and presumably future external linkers) have no user.
return;
if (!HasComp<EmaggedComponent>(uid) && !IsAllowed(args.User.Value, uid, component))
if (!IsAllowed(args.User.Value, uid, component))
args.Cancel();
}
private void OnEmagged(EntityUid uid, AccessReaderComponent reader, ref GotEmaggedEvent args)
{
if (!reader.BreakOnEmag)
if (!_emag.CompareFlag(args.Type, EmagType.Access))
return;
if (!reader.BreakOnAccessBreaker)
return;
if (!GetMainAccessReader(uid, out var accessReader))
return;
if (accessReader.Value.Comp.AccessLists.Count < 1)
return;
args.Repeatable = true;
args.Handled = true;
reader.Enabled = false;
reader.AccessLog.Clear();
accessReader.Value.Comp.AccessLists.Clear();
accessReader.Value.Comp.AccessLog.Clear();
Dirty(uid, reader);
}
@@ -135,6 +146,7 @@ public sealed class AccessReaderSystem : EntitySystem
return true;
}
}
return true;
}

View File

@@ -45,3 +45,6 @@ namespace Content.Shared.Access.Systems
}
}
}
[ByRefEvent]
public record struct OnAccessOverriderAccessUpdatedEvent(EntityUid UserUid, bool Handled = false);

View File

@@ -199,7 +199,8 @@ namespace Content.Shared.ActionBlocker
{
var containerEv = new CanAttackFromContainerEvent(uid, target);
RaiseLocalEvent(uid, containerEv);
return containerEv.CanAttack;
if (!containerEv.CanAttack)
return false;
}
var ev = new AttackAttemptEvent(uid, target, weapon, disarm);

View File

@@ -167,6 +167,14 @@ public abstract partial class BaseActionComponent : Component
[DataField]
public bool RaiseOnUser;
/// <summary>
/// If true, this will cause the the action event to always be raised directed at the action itself instead of the action's container/provider.
/// Takes priority over RaiseOnUser.
/// </summary>
[DataField]
[Obsolete("This datafield will be reworked in an upcoming action refactor")]
public bool RaiseOnAction;
/// <summary>
/// Whether or not to automatically add this action to the action bar when it becomes available.
/// </summary>
@@ -212,6 +220,7 @@ public abstract class BaseActionComponentState : ComponentState
public int Priority;
public NetEntity? AttachedEntity;
public bool RaiseOnUser;
public bool RaiseOnAction;
public bool AutoPopulate;
public bool Temporary;
public ItemActionIconStyle ItemIconStyle;
@@ -223,6 +232,7 @@ public abstract class BaseActionComponentState : ComponentState
EntityIcon = entManager.GetNetEntity(component.EntIcon);
AttachedEntity = entManager.GetNetEntity(component.AttachedEntity);
RaiseOnUser = component.RaiseOnUser;
RaiseOnAction = component.RaiseOnAction;
Icon = component.Icon;
IconOn = component.IconOn;
IconColor = component.IconColor;

View File

@@ -1,4 +1,4 @@
using Content.Shared.Whitelist;
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
@@ -25,6 +25,12 @@ public sealed partial class EntityTargetActionComponent : BaseTargetActionCompon
/// <remarks>No whitelist check when null.</remarks>
[DataField("whitelist")] public EntityWhitelist? Whitelist;
/// <summary>
/// Determines which entities are NOT valid targets for this action.
/// </summary>
/// <remarks>No blacklist check when null.</remarks>
[DataField] public EntityWhitelist? Blacklist;
/// <summary>
/// Whether this action considers the user as a valid target entity when using this action.
/// </summary>
@@ -35,11 +41,13 @@ public sealed partial class EntityTargetActionComponent : BaseTargetActionCompon
public sealed class EntityTargetActionComponentState : BaseActionComponentState
{
public EntityWhitelist? Whitelist;
public EntityWhitelist? Blacklist;
public bool CanTargetSelf;
public EntityTargetActionComponentState(EntityTargetActionComponent component, IEntityManager entManager) : base(component, entManager)
{
Whitelist = component.Whitelist;
Blacklist = component.Blacklist;
CanTargetSelf = component.CanTargetSelf;
}
}

View File

@@ -538,6 +538,7 @@ public abstract class SharedActionsSystem : EntitySystem
if (!ValidateEntityTargetBase(user,
target,
comp.Whitelist,
comp.Blacklist,
comp.CheckCanInteract,
comp.CanTargetSelf,
comp.CheckCanAccess,
@@ -552,6 +553,7 @@ public abstract class SharedActionsSystem : EntitySystem
private bool ValidateEntityTargetBase(EntityUid user,
EntityUid? targetEntity,
EntityWhitelist? whitelist,
EntityWhitelist? blacklist,
bool checkCanInteract,
bool canTargetSelf,
bool checkCanAccess,
@@ -563,6 +565,9 @@ public abstract class SharedActionsSystem : EntitySystem
if (_whitelistSystem.IsWhitelistFail(whitelist, target))
return false;
if (_whitelistSystem.IsBlacklistPass(blacklist, target))
return false;
if (checkCanInteract && !_actionBlockerSystem.CanInteract(user, target))
return false;
@@ -637,6 +642,7 @@ public abstract class SharedActionsSystem : EntitySystem
var entityValidated = ValidateEntityTargetBase(user,
entity,
comp.Whitelist,
null,
comp.CheckCanInteract,
comp.CanTargetSelf,
comp.CheckCanAccess,
@@ -679,6 +685,9 @@ public abstract class SharedActionsSystem : EntitySystem
if (!action.RaiseOnUser && action.Container != null && !HasComp<MindComponent>(action.Container))
target = action.Container.Value;
if (action.RaiseOnAction)
target = actionId;
RaiseLocalEvent(target, (object) actionEvent, broadcast: true);
handled = actionEvent.Handled;
}

View File

@@ -1,7 +1,6 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.IdentityManagement;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition.Components;
@@ -32,7 +31,6 @@ public sealed class UdderSystem : EntitySystem
SubscribeLocalEvent<UdderComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<UdderComponent, GetVerbsEvent<AlternativeVerb>>(AddMilkVerb);
SubscribeLocalEvent<UdderComponent, MilkingDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<UdderComponent, ExaminedEvent>(OnExamine);
}
private void OnMapInit(EntityUid uid, UdderComponent component, MapInitEvent args)
@@ -140,50 +138,4 @@ public sealed class UdderSystem : EntitySystem
};
args.Verbs.Add(verb);
}
/// <summary>
/// Defines the text provided on examine.
/// Changes depending on the amount of hunger the target has.
/// </summary>
private void OnExamine(Entity<UdderComponent> entity, ref ExaminedEvent args)
{
var entityIdentity = Identity.Entity(args.Examined, EntityManager);
string message;
// Check if the target has hunger, otherwise return not hungry.
if (!TryComp<HungerComponent>(entity, out var hunger))
{
message = Loc.GetString("udder-system-examine-none", ("entity", entityIdentity));
args.PushMarkup(message);
return;
}
// Choose the correct examine string based on HungerThreshold.
switch (_hunger.GetHungerThreshold(hunger))
{
case >= HungerThreshold.Overfed:
message = Loc.GetString("udder-system-examine-overfed", ("entity", entityIdentity));
break;
case HungerThreshold.Okay:
message = Loc.GetString("udder-system-examine-okay", ("entity", entityIdentity));
break;
case HungerThreshold.Peckish:
message = Loc.GetString("udder-system-examine-hungry", ("entity", entityIdentity));
break;
// There's a final hunger threshold called "dead" but animals don't actually die so we'll re-use this.
case <= HungerThreshold.Starving:
message = Loc.GetString("udder-system-examine-starved", ("entity", entityIdentity));
break;
default:
return;
}
args.PushMarkup(message);
}
}

View File

@@ -1,4 +1,5 @@
using Content.Shared.Damage;
using Content.Shared.Inventory;
using Robust.Shared.GameStates;
using Robust.Shared.Utility;
@@ -30,3 +31,24 @@ public sealed partial class ArmorComponent : Component
/// <param name="Msg"></param>
[ByRefEvent]
public record struct ArmorExamineEvent(FormattedMessage Msg);
/// <summary>
/// A Relayed inventory event, gets the total Armor for all Inventory slots defined by the Slotflags in TargetSlots
/// </summary>
public sealed class CoefficientQueryEvent : EntityEventArgs, IInventoryRelayEvent
{
/// <summary>
/// All slots to relay to
/// </summary>
public SlotFlags TargetSlots { get; set; }
/// <summary>
/// The Total of all Coefficients.
/// </summary>
public DamageModifierSet DamageModifiers { get; set; } = new DamageModifierSet();
public CoefficientQueryEvent(SlotFlags slots)
{
TargetSlots = slots;
}
}

View File

@@ -19,11 +19,25 @@ public abstract class SharedArmorSystem : EntitySystem
{
base.Initialize();
SubscribeLocalEvent<ArmorComponent, InventoryRelayedEvent<CoefficientQueryEvent>>(OnCoefficientQuery);
SubscribeLocalEvent<ArmorComponent, InventoryRelayedEvent<DamageModifyEvent>>(OnDamageModify);
SubscribeLocalEvent<ArmorComponent, BorgModuleRelayedEvent<DamageModifyEvent>>(OnBorgDamageModify);
SubscribeLocalEvent<ArmorComponent, GetVerbsEvent<ExamineVerb>>(OnArmorVerbExamine);
}
/// <summary>
/// Get the total Damage reduction value of all equipment caught by the relay.
/// </summary>
/// <param name="ent">The item that's being relayed to</param>
/// <param name="args">The event, contains the running count of armor percentage as a coefficient</param>
private void OnCoefficientQuery(Entity<ArmorComponent> ent, ref InventoryRelayedEvent<CoefficientQueryEvent> args)
{
foreach (var armorCoefficient in ent.Comp.Modifiers.Coefficients)
{
args.Args.DamageModifiers.Coefficients[armorCoefficient.Key] = args.Args.DamageModifiers.Coefficients.TryGetValue(armorCoefficient.Key, out var coefficient) ? coefficient * armorCoefficient.Value : armorCoefficient.Value;
}
}
private void OnDamageModify(EntityUid uid, ArmorComponent component, InventoryRelayedEvent<DamageModifyEvent> args)
{
args.Args.Damage = DamageSpecifier.ApplyModifierSet(args.Args.Damage, component.Modifiers);

View File

@@ -40,6 +40,12 @@ namespace Content.Shared.Atmos
/// </summary>
public const float T20C = 293.15f;
/// <summary>
/// -38.15ºC in K.
/// This is used to initialize roundstart freezer rooms.
/// </summary>
public const float FreezerTemp = 235f;
/// <summary>
/// Do not allow any gas mixture temperatures to exceed this number. It is occasionally possible
/// to have very small heat capacity (e.g. room that was just unspaced) and for large amounts of
@@ -65,6 +71,12 @@ namespace Content.Shared.Atmos
/// </summary>
public const float MolesCellStandard = (OneAtmosphere * CellVolume / (T20C * R));
/// <summary>
/// Moles in a 2.5 m^3 cell at 101.325 kPa and -38.15ºC.
/// This is used in fix atmos freezer markers to ensure the air is at the correct atmospheric pressure while still being cold.
/// </summary>
public const float MolesCellFreezer = (OneAtmosphere * CellVolume / (FreezerTemp * R));
/// <summary>
/// Moles in a 2.5 m^3 cell at GasMinerDefaultMaxExternalPressure kPa and 20ºC
/// </summary>
@@ -81,6 +93,9 @@ namespace Content.Shared.Atmos
public const float OxygenMolesStandard = MolesCellStandard * OxygenStandard;
public const float NitrogenMolesStandard = MolesCellStandard * NitrogenStandard;
public const float OxygenMolesFreezer = MolesCellFreezer * OxygenStandard;
public const float NitrogenMolesFreezer = MolesCellFreezer * NitrogenStandard;
#endregion
/// <summary>

View File

@@ -1,3 +1,4 @@
using Content.Shared.Guidebook;
using Robust.Shared.GameStates;
namespace Content.Shared.Atmos.Components;
@@ -21,5 +22,6 @@ public sealed partial class GasPressurePumpComponent : Component
/// Max pressure of the target gas (NOT relative to source).
/// </summary>
[DataField]
[GuidebookData]
public float MaxTargetPressure = Atmospherics.MaxOutputPressure;
}

View File

@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Collections;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.CompilerServices;
using Content.Shared.Atmos.EntitySystems;
@@ -13,12 +14,12 @@ namespace Content.Shared.Atmos
/// </summary>
[Serializable]
[DataDefinition]
public sealed partial class GasMixture : IEquatable<GasMixture>, ISerializationHooks
public sealed partial class GasMixture : IEquatable<GasMixture>, ISerializationHooks, IEnumerable<(Gas gas, float moles)>
{
public static GasMixture SpaceGas => new() {Volume = Atmospherics.CellVolume, Temperature = Atmospherics.TCMB, Immutable = true};
// No access, to ensure immutable mixtures are never accidentally mutated.
[Access(typeof(SharedAtmosphereSystem), typeof(SharedAtmosDebugOverlaySystem), Other = AccessPermissions.None)]
[Access(typeof(SharedAtmosphereSystem), typeof(SharedAtmosDebugOverlaySystem), typeof(GasEnumerator), Other = AccessPermissions.None)]
[DataField]
public float[] Moles = new float[Atmospherics.AdjustedNumberOfGases];
@@ -32,10 +33,9 @@ namespace Content.Shared.Atmos
public bool Immutable { get; private set; }
[ViewVariables]
public readonly Dictionary<GasReaction, float> ReactionResults = new()
public readonly float[] ReactionResults =
{
// We initialize the dictionary here.
{ GasReaction.Fire, 0f }
0f,
};
[ViewVariables]
@@ -249,6 +249,16 @@ namespace Content.Shared.Atmos
return new GasMixtureStringRepresentation(TotalMoles, Temperature, Pressure, molesPerGas);
}
GasEnumerator GetEnumerator()
{
return new GasEnumerator(this);
}
IEnumerator<(Gas gas, float moles)> IEnumerable<(Gas gas, float moles)>.GetEnumerator()
{
return GetEnumerator();
}
public override bool Equals(object? obj)
{
if (obj is GasMixture mix)
@@ -289,6 +299,11 @@ namespace Content.Shared.Atmos
return hashCode.ToHashCode();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public GasMixture Clone()
{
if (Immutable)
@@ -302,5 +317,28 @@ namespace Content.Shared.Atmos
};
return newMixture;
}
public struct GasEnumerator(GasMixture mixture) : IEnumerator<(Gas gas, float moles)>
{
private int _idx = -1;
public void Dispose()
{
// Nada.
}
public bool MoveNext()
{
return ++_idx < Atmospherics.TotalNumberOfGases;
}
public void Reset()
{
_idx = -1;
}
public (Gas gas, float moles) Current => ((Gas)_idx, mixture.Moles[_idx]);
object? IEnumerator.Current => Current;
}
}
}

View File

@@ -253,10 +253,57 @@ public sealed partial class AtmosAlarmThreshold
break;
}
}
/// <summary>
/// Iterates through the changes that these threshold settings would make from a
/// previous instance. Basically, diffs the two settings.
/// </summary>
public IEnumerable<AtmosAlarmThresholdChange> GetChanges(AtmosAlarmThreshold previous)
{
if (LowerBound != previous.LowerBound)
yield return new AtmosAlarmThresholdChange(AtmosMonitorLimitType.LowerDanger, previous.LowerBound, LowerBound);
if (LowerWarningBound != previous.LowerWarningBound)
yield return new AtmosAlarmThresholdChange(AtmosMonitorLimitType.LowerWarning, previous.LowerWarningBound, LowerWarningBound);
if (UpperBound != previous.UpperBound)
yield return new AtmosAlarmThresholdChange(AtmosMonitorLimitType.UpperDanger, previous.UpperBound, UpperBound);
if (UpperWarningBound != previous.UpperWarningBound)
yield return new AtmosAlarmThresholdChange(AtmosMonitorLimitType.UpperWarning, previous.UpperWarningBound, UpperWarningBound);
}
}
/// <summary>
/// A change of a single value between two AtmosAlarmThreshold, for a given AtmosMonitorLimitType
/// </summary>
public readonly struct AtmosAlarmThresholdChange
{
/// <summary>
/// The type of change between the two threshold sets
/// </summary>
public readonly AtmosMonitorLimitType Type;
/// <summary>
/// The value in the old threshold set
/// </summary>
public readonly AlarmThresholdSetting? Previous;
/// <summary>
/// The value in the new threshold set
/// </summary>
public readonly AlarmThresholdSetting Current;
public AtmosAlarmThresholdChange(AtmosMonitorLimitType type, AlarmThresholdSetting? previous, AlarmThresholdSetting current)
{
Type = type;
Previous = previous;
Current = current;
}
}
[DataDefinition, Serializable]
public readonly partial struct AlarmThresholdSetting
public readonly partial struct AlarmThresholdSetting: IEquatable<AlarmThresholdSetting>
{
[DataField("enabled")]
public bool Enabled { get; init; } = true;
@@ -289,6 +336,32 @@ public readonly partial struct AlarmThresholdSetting
{
return this with {Enabled = enabled};
}
public bool Equals(AlarmThresholdSetting other)
{
if (Enabled != other.Enabled)
return false;
if (Value != other.Value)
return false;
return true;
}
public static bool operator ==(AlarmThresholdSetting lhs, AlarmThresholdSetting rhs)
{
return lhs.Equals(rhs);
}
public static bool operator !=(AlarmThresholdSetting lhs, AlarmThresholdSetting rhs)
{
return !lhs.Equals(rhs);
}
public override int GetHashCode()
{
return HashCode.Combine(Enabled, Value);
}
}
public enum AtmosMonitorThresholdBound

View File

@@ -1,6 +1,8 @@
using Content.Shared.Dataset;
using Content.Shared.FixedPoint;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Bed.Sleep;
@@ -39,4 +41,11 @@ public sealed partial class SleepingComponent : Component
{
Params = AudioParams.Default.WithVariation(0.05f)
};
/// <summary>
/// The fluent string prefix to use when picking a random suffix
/// This is only active for those who have the sleeping component
/// </summary>
[DataField]
public ProtoId<LocalizedDatasetPrototype> ForceSaySleepDataset = "ForceSaySleepDataset";
}

View File

@@ -1,6 +1,7 @@
using Content.Shared.Actions;
using Content.Shared.Buckle.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.Events;
using Content.Shared.Damage.ForceSay;
using Content.Shared.Emoting;
using Content.Shared.Examine;
@@ -18,6 +19,7 @@ using Content.Shared.Sound.Components;
using Content.Shared.Speech;
using Content.Shared.StatusEffect;
using Content.Shared.Stunnable;
using Content.Shared.Traits.Assorted;
using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Prototypes;
@@ -63,6 +65,8 @@ public sealed partial class SleepingSystem : EntitySystem
SubscribeLocalEvent<ForcedSleepingComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<SleepingComponent, UnbuckleAttemptEvent>(OnUnbuckleAttempt);
SubscribeLocalEvent<SleepingComponent, EmoteAttemptEvent>(OnEmoteAttempt);
SubscribeLocalEvent<SleepingComponent, BeforeForceSayEvent>(OnChangeForceSay, after: new []{typeof(PainNumbnessSystem)});
}
private void OnUnbuckleAttempt(Entity<SleepingComponent> ent, ref UnbuckleAttemptEvent args)
@@ -317,6 +321,11 @@ public sealed partial class SleepingSystem : EntitySystem
{
args.Cancel();
}
private void OnChangeForceSay(Entity<SleepingComponent> ent, ref BeforeForceSayEvent args)
{
args.Prefix = ent.Comp.ForceSaySleepDataset;
}
}

View File

@@ -89,6 +89,12 @@ public sealed partial class CCVars
public static readonly CVarDef<bool> ServerBanErasePlayer =
CVarDef.Create("admin.server_ban_erase_player", false, CVar.ARCHIVE | CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// If true, will reset the last time the player has read the rules. This will mean on their next login they will be shown the rules again.
/// </summary>
public static readonly CVarDef<bool> ServerBanResetLastReadRules =
CVarDef.Create("admin.server_ban_reset_last_read_rules", true, CVar.ARCHIVE | CVar.SERVER);
/// <summary>
/// Minimum players sharing a connection required to create an alert. -1 to disable the alert.
/// </summary>
@@ -176,4 +182,11 @@ public sealed partial class CCVars
public static readonly CVarDef<bool> BanHardwareIds =
CVarDef.Create("ban.hardware_ids", true, CVar.SERVERONLY);
/// <summary>
/// If true, players are allowed to connect to multiple game servers at once.
/// If false, they will be kicked from the first when connecting to another.
/// </summary>
public static readonly CVarDef<bool> AdminAllowMultiServerPlay =
CVarDef.Create("admin.allow_multi_server_play", true, CVar.SERVERONLY);
}

View File

@@ -51,4 +51,23 @@ public sealed partial class CCVars
/// </summary>
public static readonly CVarDef<bool> OpaqueStorageWindow =
CVarDef.Create("control.opaque_storage_background", false, CVar.CLIENTONLY | CVar.ARCHIVE);
/// <summary>
/// Whether or not the storage window has a title of the entity name.
/// </summary>
public static readonly CVarDef<bool> StorageWindowTitle =
CVarDef.Create("control.storage_window_title", false, CVar.CLIENTONLY | CVar.ARCHIVE);
/// <summary>
/// How many storage windows are allowed to be open at once.
/// Recommended that you utilise this in conjunction with <see cref="StaticStorageUI"/>
/// </summary>
public static readonly CVarDef<int> StorageLimit =
CVarDef.Create("control.storage_limit", 1, CVar.REPLICATED | CVar.SERVER);
/// <summary>
/// Whether or not storage can be opened recursively.
/// </summary>
public static readonly CVarDef<bool> NestedStorage =
CVarDef.Create("control.nested_storage", true, CVar.REPLICATED | CVar.SERVER);
}

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using Content.Shared.Inventory;
using Content.Shared.Movement.Systems;
namespace Content.Shared.Camera;
@@ -17,3 +18,15 @@ namespace Content.Shared.Camera;
/// </remarks>
[ByRefEvent]
public record struct GetEyeOffsetEvent(Vector2 Offset);
/// <summary>
/// Raised on any equipped and in-hand items that may modify the eye offset.
/// Pockets and suitstorage are excluded.
/// </summary>
[ByRefEvent]
public sealed class GetEyeOffsetRelayedEvent : EntityEventArgs, IInventoryRelayEvent
{
public SlotFlags TargetSlots { get; } = ~(SlotFlags.POCKET & SlotFlags.SUITSTORAGE);
public Vector2 Offset;
}

View File

@@ -0,0 +1,33 @@
using System.Numerics;
using Content.Shared.Inventory;
using Content.Shared.Movement.Systems;
namespace Content.Shared.Camera;
/// <summary>
/// Raised directed by-ref when <see cref="SharedContentEyeSystem.UpdatePvsScale"/> is called.
/// Should be subscribed to by any systems that want to modify an entity's eye PVS scale,
/// so that they do not override each other. Keep in mind that this should be done serverside;
/// the client may set a new PVS scale, but the server won't provide the data if it isn't done on the server.
/// </summary>
/// <param name="Scale">
/// The total scale to apply.
/// </param>
/// <remarks>
/// Note that in most cases <see cref="Scale"/> should be incremented or decremented by subscribers, not set.
/// Otherwise, any offsets applied by previous subscribing systems will be overridden.
/// </remarks>
[ByRefEvent]
public record struct GetEyePvsScaleEvent(float Scale);
/// <summary>
/// Raised on any equipped and in-hand items that may modify the eye offset.
/// Pockets and suitstorage are excluded.
/// </summary>
[ByRefEvent]
public sealed class GetEyePvsScaleRelayedEvent : EntityEventArgs, IInventoryRelayEvent
{
public SlotFlags TargetSlots { get; } = ~(SlotFlags.POCKET & SlotFlags.SUITSTORAGE);
public float Scale;
}

View File

@@ -1,4 +1,6 @@
using System.Numerics;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using JetBrains.Annotations;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
@@ -28,7 +30,7 @@ public abstract class SharedCameraRecoilSystem : EntitySystem
/// </summary>
protected const float KickMagnitudeMax = 1f;
[Dependency] private readonly SharedEyeSystem _eye = default!;
[Dependency] private readonly SharedContentEyeSystem _eye = default!;
[Dependency] private readonly INetManager _net = default!;
public override void Initialize()
@@ -81,9 +83,7 @@ public abstract class SharedCameraRecoilSystem : EntitySystem
continue;
recoil.LastKick = recoil.CurrentKick;
var ev = new GetEyeOffsetEvent();
RaiseLocalEvent(uid, ref ev);
_eye.SetOffset(uid, ev.Offset, eye);
_eye.UpdateEyeOffset((uid, eye));
}
}

View File

@@ -0,0 +1,67 @@
using Content.Shared.Cargo.Prototypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Cargo;
/// <summary>
/// A data structure for storing historical information about bounties.
/// </summary>
[DataDefinition, NetSerializable, Serializable]
public readonly partial record struct CargoBountyHistoryData
{
/// <summary>
/// A unique id used to identify the bounty
/// </summary>
[DataField]
public string Id { get; init; } = string.Empty;
/// <summary>
/// Whether this bounty was completed or skipped.
/// </summary>
[DataField]
public BountyResult Result { get; init; } = BountyResult.Completed;
/// <summary>
/// Optional name of the actor that completed/skipped the bounty.
/// </summary>
[DataField]
public string? ActorName { get; init; } = default;
/// <summary>
/// Time when this bounty was completed or skipped
/// </summary>
[DataField]
public TimeSpan Timestamp { get; init; } = TimeSpan.MinValue;
/// <summary>
/// The prototype containing information about the bounty.
/// </summary>
[DataField(required: true)]
public ProtoId<CargoBountyPrototype> Bounty { get; init; } = string.Empty;
public CargoBountyHistoryData(CargoBountyData bounty, BountyResult result, TimeSpan timestamp, string? actorName)
{
Bounty = bounty.Bounty;
Result = result;
Id = bounty.Id;
ActorName = actorName;
Timestamp = timestamp;
}
/// <summary>
/// Covers how a bounty was actually finished.
/// </summary>
public enum BountyResult
{
/// <summary>
/// Bounty was actually fulfilled and the goods sold
/// </summary>
Completed = 0,
/// <summary>
/// Bounty was explicitly skipped by some actor
/// </summary>
Skipped = 1,
}
}

View File

@@ -50,11 +50,13 @@ public sealed partial class CargoBountyConsoleComponent : Component
public sealed class CargoBountyConsoleState : BoundUserInterfaceState
{
public List<CargoBountyData> Bounties;
public List<CargoBountyHistoryData> History;
public TimeSpan UntilNextSkip;
public CargoBountyConsoleState(List<CargoBountyData> bounties, TimeSpan untilNextSkip)
public CargoBountyConsoleState(List<CargoBountyData> bounties, List<CargoBountyHistoryData> history, TimeSpan untilNextSkip)
{
Bounties = bounties;
History = history;
UntilNextSkip = untilNextSkip;
}
}

View File

@@ -26,7 +26,8 @@ public sealed class SolutionSpikerSystem : EntitySystem
private void OnInteractUsing(Entity<RefillableSolutionComponent> entity, ref InteractUsingEvent args)
{
TrySpike(args.Used, args.Target, args.User, entity.Comp);
if (TrySpike(args.Used, args.Target, args.User, entity.Comp))
args.Handled = true;
}
/// <summary>
@@ -36,7 +37,7 @@ public sealed class SolutionSpikerSystem : EntitySystem
/// <param name="source">Source of the solution.</param>
/// <param name="target">Target to spike with the solution from source.</param>
/// <param name="user">User spiking the target solution.</param>
private void TrySpike(EntityUid source, EntityUid target, EntityUid user, RefillableSolutionComponent? spikableTarget = null,
private bool TrySpike(EntityUid source, EntityUid target, EntityUid user, RefillableSolutionComponent? spikableTarget = null,
SolutionSpikerComponent? spikableSource = null,
SolutionContainerManagerComponent? managerSource = null,
SolutionContainerManagerComponent? managerTarget = null)
@@ -46,21 +47,23 @@ public sealed class SolutionSpikerSystem : EntitySystem
|| !_solution.TryGetRefillableSolution((target, spikableTarget, managerTarget), out var targetSoln, out var targetSolution)
|| !_solution.TryGetSolution((source, managerSource), spikableSource.SourceSolution, out _, out var sourceSolution))
{
return;
return false;
}
if (targetSolution.Volume == 0 && !spikableSource.IgnoreEmpty)
{
_popup.PopupClient(Loc.GetString(spikableSource.PopupEmpty, ("spiked-entity", target), ("spike-entity", source)), user, user);
return;
return false;
}
if (!_solution.ForceAddSolution(targetSoln.Value, sourceSolution))
return;
return false;
_popup.PopupClient(Loc.GetString(spikableSource.Popup, ("spiked-entity", target), ("spike-entity", source)), user, user);
sourceSolution.RemoveAllSolution();
if (spikableSource.Delete)
QueueDel(source);
return true;
}
}

View File

@@ -46,15 +46,6 @@ public sealed partial class CloningPodComponent : Component
[DataField("mobSpawnId"), ViewVariables(VVAccess.ReadWrite)]
public EntProtoId MobSpawnId = "MobAbomination";
/// <summary>
/// Emag sound effects.
/// </summary>
[DataField("sparkSound")]
public SoundSpecifier SparkSound = new SoundCollectionSpecifier("sparks")
{
Params = AudioParams.Default.WithVolume(8),
};
// TODO: Remove this from here when cloning and/or zombies are refactored
[DataField("screamSound")]
public SoundSpecifier ScreamSound = new SoundCollectionSpecifier("ZombieScreams")

View File

@@ -20,8 +20,11 @@ public sealed partial class MaskComponent : Component
[DataField, AutoNetworkedField]
public bool IsToggled;
/// <summary>
/// Equipped prefix to use after the mask was pulled down.
/// </summary>
[DataField, AutoNetworkedField]
public string EquippedPrefix = "toggled";
public string EquippedPrefix = "up";
/// <summary>
/// When <see langword="true"/> will function normally, otherwise will not react to events

View File

@@ -5,8 +5,9 @@ using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
using Content.Shared.Item;
using Content.Shared.Tag;
using Content.Shared.Verbs;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Utility;
namespace Content.Shared.Clothing.EntitySystems;
@@ -14,19 +15,20 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
{
[Dependency] private readonly IComponentFactory _factory = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly ISerializationManager _serialization = default!;
[Dependency] private readonly ClothingSystem _clothingSystem = default!;
[Dependency] private readonly ContrabandSystem _contraband = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly SharedItemSystem _itemSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] protected readonly SharedUserInterfaceSystem UI = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ChameleonClothingComponent, GotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<ChameleonClothingComponent, GotUnequippedEvent>(OnGotUnequipped);
SubscribeLocalEvent<ChameleonClothingComponent, GetVerbsEvent<InteractionVerb>>(OnVerb);
}
private void OnGotEquipped(EntityUid uid, ChameleonClothingComponent component, GotEquippedEvent args)
@@ -94,6 +96,22 @@ public abstract class SharedChameleonClothingSystem : EntitySystem
}
}
private void OnVerb(Entity<ChameleonClothingComponent> ent, ref GetVerbsEvent<InteractionVerb> args)
{
if (!args.CanAccess || !args.CanInteract || ent.Comp.User != args.User)
return;
// Can't pass args from a ref event inside of lambdas
var user = args.User;
args.Verbs.Add(new InteractionVerb()
{
Text = Loc.GetString("chameleon-component-verb-text"),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")),
Act = () => UI.TryToggleUi(ent.Owner, ChameleonUiKey.Key, user)
});
}
protected virtual void UpdateSprite(EntityUid uid, EntityPrototype proto) { }
/// <summary>

View File

@@ -10,6 +10,7 @@ namespace Content.Shared.Construction
{
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] protected readonly IPrototypeManager PrototypeManager = default!;
[Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
/// <summary>
/// Get predicate for construction obstruction checks.

View File

@@ -40,7 +40,7 @@ public sealed class ContainerFillSystem : EntitySystem
if (!_containerSystem.Insert(ent, container, containerXform: xform))
{
Log.Error($"Entity {ToPrettyString(uid)} with a {nameof(ContainerFillComponent)} failed to insert an entity: {ToPrettyString(ent)}.");
Transform(ent).AttachToGridOrMap();
_transform.AttachToGridOrMap(ent);
break;
}
}

View File

@@ -17,4 +17,16 @@ public sealed partial class DragInsertContainerComponent : Component
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public bool UseVerbs = true;
/// <summary>
/// The delay in seconds before a drag will be completed.
/// </summary>
[DataField]
public TimeSpan EntryDelay = TimeSpan.Zero;
/// <summary>
/// If entry delay isn't zero, this sets whether an entity dragging itself into the container should be delayed.
/// </summary>
[DataField]
public bool DelaySelfEntry = false;
}

View File

@@ -2,24 +2,28 @@ using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Logs;
using Content.Shared.Climbing.Systems;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.DragDrop;
using Content.Shared.Verbs;
using Robust.Shared.Containers;
using Robust.Shared.Serialization;
namespace Content.Shared.Containers;
public sealed class DragInsertContainerSystem : EntitySystem
public sealed partial class DragInsertContainerSystem : EntitySystem
{
[Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly ClimbSystem _climb = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DragInsertContainerComponent, DragDropTargetEvent>(OnDragDropOn, before: new []{ typeof(ClimbSystem)});
SubscribeLocalEvent<DragInsertContainerComponent, DragInsertContainerDoAfterEvent>(OnDragFinished);
SubscribeLocalEvent<DragInsertContainerComponent, CanDropTargetEvent>(OnCanDragDropOn);
SubscribeLocalEvent<DragInsertContainerComponent, GetVerbsEvent<AlternativeVerb>>(OnGetAlternativeVerb);
}
@@ -33,7 +37,34 @@ public sealed class DragInsertContainerSystem : EntitySystem
if (!_container.TryGetContainer(ent, comp.ContainerId, out var container))
return;
args.Handled = Insert(args.Dragged, args.User, ent, container);
if (comp.EntryDelay <= TimeSpan.Zero ||
!comp.DelaySelfEntry && args.User == args.Dragged)
{
//instant insertion
args.Handled = Insert(args.Dragged, args.User, ent, container);
return;
}
//delayed insertion
var doAfterArgs = new DoAfterArgs(EntityManager, args.User, comp.EntryDelay, new DragInsertContainerDoAfterEvent(), ent, args.Dragged, ent)
{
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = false,
};
_doAfter.TryStartDoAfter(doAfterArgs);
args.Handled = true;
}
private void OnDragFinished(Entity<DragInsertContainerComponent> ent, ref DragInsertContainerDoAfterEvent args)
{
if (args.Handled || args.Cancelled || args.Args.Target == null)
return;
if (!_container.TryGetContainer(ent, ent.Comp.ContainerId, out var container))
return;
Insert(args.Args.Target.Value, args.User, ent, container);
}
private void OnCanDragDropOn(Entity<DragInsertContainerComponent> ent, ref CanDropTargetEvent args)
@@ -117,4 +148,9 @@ public sealed class DragInsertContainerSystem : EntitySystem
_adminLog.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(user):player} inserted {ToPrettyString(target):player} into container {ToPrettyString(containerEntity)}");
return true;
}
[Serializable, NetSerializable]
public sealed partial class DragInsertContainerDoAfterEvent : SimpleDoAfterEvent
{
}
}

View File

@@ -24,5 +24,13 @@ public sealed partial class ContrabandComponent : Component
/// </summary>
[DataField]
[AutoNetworkedField]
public HashSet<ProtoId<DepartmentPrototype>>? AllowedDepartments = ["Security"];
public HashSet<ProtoId<DepartmentPrototype>> AllowedDepartments = new();
/// <summary>
/// Which jobs is this item restricted to?
/// If empty, no jobs are allowed to use this beyond the allowed departments.
/// </summary>
[DataField]
[AutoNetworkedField]
public HashSet<ProtoId<JobPrototype>> AllowedJobs = new();
}

View File

@@ -19,8 +19,8 @@ public sealed partial class ContrabandSeverityPrototype : IPrototype
public LocId ExamineText;
/// <summary>
/// When examining the contraband, should this take into account the viewer's departments?
/// When examining the contraband, should this take into account the viewer's departments and job?
/// </summary>
[DataField]
public bool ShowDepartments;
public bool ShowDepartmentsAndJobs;
}

View File

@@ -4,8 +4,10 @@ using Content.Shared.CCVar;
using Content.Shared.Examine;
using Content.Shared.Localizations;
using Content.Shared.Roles;
using Content.Shared.Verbs;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Contraband;
@@ -17,22 +19,18 @@ public sealed class ContrabandSystem : EntitySystem
[Dependency] private readonly IConfigurationManager _configuration = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly SharedIdCardSystem _id = default!;
[Dependency] private readonly ExamineSystemShared _examine = default!;
private bool _contrabandExamineEnabled;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<ContrabandComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<ContrabandComponent, GetVerbsEvent<ExamineVerb>>(OnDetailedExamine);
Subs.CVar(_configuration, CCVars.ContrabandExamine, SetContrabandExamine, true);
}
private void SetContrabandExamine(bool val)
{
_contrabandExamineEnabled = val;
}
public void CopyDetails(EntityUid uid, ContrabandComponent other, ContrabandComponent? contraband = null)
{
if (!Resolve(uid, ref contraband))
@@ -40,52 +38,84 @@ public sealed class ContrabandSystem : EntitySystem
contraband.Severity = other.Severity;
contraband.AllowedDepartments = other.AllowedDepartments;
contraband.AllowedJobs = other.AllowedJobs;
Dirty(uid, contraband);
}
private void OnExamined(Entity<ContrabandComponent> ent, ref ExaminedEvent args)
private void OnDetailedExamine(EntityUid ent,ContrabandComponent component, ref GetVerbsEvent<ExamineVerb> args)
{
if (!_contrabandExamineEnabled)
return;
// CanAccess is not used here, because we want people to be able to examine legality in strip menu.
if (!args.CanInteract)
return;
// two strings:
// one, the actual informative 'this is restricted'
// then, the 'you can/shouldn't carry this around' based on the ID the user is wearing
using (args.PushGroup(nameof(ContrabandComponent)))
var localizedDepartments = component.AllowedDepartments.Select(p => Loc.GetString("contraband-department-plural", ("department", Loc.GetString(_proto.Index(p).Name))));
var localizedJobs = component.AllowedJobs.Select(p => Loc.GetString("contraband-job-plural", ("job", _proto.Index(p).LocalizedName)));
var severity = _proto.Index(component.Severity);
String departmentExamineMessage;
if (severity.ShowDepartmentsAndJobs)
{
var severity = _proto.Index(ent.Comp.Severity);
if (severity.ShowDepartments && ent.Comp is { AllowedDepartments: not null })
{
// TODO shouldn't department prototypes have a localized name instead of just using the ID for this?
var list = ContentLocalizationManager.FormatList(ent.Comp.AllowedDepartments.Select(p => Loc.GetString($"department-{p.Id}")).ToList());
// department restricted text
args.PushMarkup(Loc.GetString("contraband-examine-text-Restricted-department", ("departments", list)));
}
else
{
args.PushMarkup(Loc.GetString(severity.ExamineText));
}
// text based on ID card
List<ProtoId<DepartmentPrototype>>? departments = null;
if (_id.TryFindIdCard(args.Examiner, out var id))
{
departments = id.Comp.JobDepartments;
}
// either its fully restricted, you have no departments, or your departments dont intersect with the restricted departments
if (ent.Comp.AllowedDepartments is null
|| departments is null
|| !departments.Intersect(ent.Comp.AllowedDepartments).Any())
{
args.PushMarkup(Loc.GetString("contraband-examine-text-avoid-carrying-around"));
return;
}
// otherwise fine to use :tm:
args.PushMarkup(Loc.GetString("contraband-examine-text-in-the-clear"));
//creating a combined list of jobs and departments for the restricted text
var list = ContentLocalizationManager.FormatList(localizedDepartments.Concat(localizedJobs).ToList());
// department restricted text
departmentExamineMessage = Loc.GetString("contraband-examine-text-Restricted-department", ("departments", list));
}
else
{
departmentExamineMessage = Loc.GetString(severity.ExamineText);
}
// text based on ID card
List<ProtoId<DepartmentPrototype>> departments = new();
var jobId = "";
if (_id.TryFindIdCard(args.User, out var id))
{
departments = id.Comp.JobDepartments;
if (id.Comp.LocalizedJobTitle is not null)
{
jobId = id.Comp.LocalizedJobTitle;
}
}
String carryingMessage;
// either its fully restricted, you have no departments, or your departments dont intersect with the restricted departments
if (departments.Intersect(component.AllowedDepartments).Any()
|| localizedJobs.Contains(jobId))
{
carryingMessage = Loc.GetString("contraband-examine-text-in-the-clear");
}
else
{
// otherwise fine to use :tm:
carryingMessage = Loc.GetString("contraband-examine-text-avoid-carrying-around");
}
var examineMarkup = GetContrabandExamine(departmentExamineMessage, carryingMessage);
_examine.AddDetailedExamineVerb(args,
component,
examineMarkup,
Loc.GetString("contraband-examinable-verb-text"),
"/Textures/Interface/VerbIcons/lock.svg.192dpi.png",
Loc.GetString("contraband-examinable-verb-message"));
}
private FormattedMessage GetContrabandExamine(String deptMessage, String carryMessage)
{
var msg = new FormattedMessage();
msg.AddMarkupOrThrow(deptMessage);
msg.PushNewline();
msg.AddMarkupOrThrow(carryMessage);
return msg;
}
private void SetContrabandExamine(bool val)
{
_contrabandExamineEnabled = val;
}
}

View File

@@ -1,7 +1,10 @@
using Content.Shared.CriminalRecords.Systems;
using Content.Shared.CriminalRecords.Components;
using Content.Shared.CriminalRecords;
using Content.Shared.Radio;
using Content.Shared.StationRecords;
using Robust.Shared.Prototypes;
using Content.Shared.Security;
namespace Content.Shared.CriminalRecords.Components;
@@ -31,6 +34,12 @@ public sealed partial class CriminalRecordsConsoleComponent : Component
[DataField]
public StationRecordsFilter? Filter;
/// <summary>
/// Current seleced security status for the filter by criminal status dropdown.
/// </summary>
[DataField]
public SecurityStatus FilterStatus;
/// <summary>
/// Channel to send messages to when someone's status gets changed.
/// </summary>

View File

@@ -35,9 +35,9 @@ public sealed class CriminalRecordsConsoleState : BoundUserInterfaceState
/// Currently selected crewmember record key.
/// </summary>
public uint? SelectedKey = null;
public CriminalRecord? CriminalRecord = null;
public GeneralStationRecord? StationRecord = null;
public SecurityStatus FilterStatus = SecurityStatus.None;
public readonly Dictionary<uint, string>? RecordListing;
public readonly StationRecordsFilter? Filter;
@@ -100,3 +100,20 @@ public sealed class CriminalRecordDeleteHistory : BoundUserInterfaceMessage
Index = index;
}
}
/// <summary>
/// Used to set what status to filter by index.
///
/// </summary>
///
[Serializable, NetSerializable]
public sealed class CriminalRecordSetStatusFilter : BoundUserInterfaceMessage
{
public readonly SecurityStatus FilterStatus;
public CriminalRecordSetStatusFilter(SecurityStatus newFilterStatus)
{
FilterStatus = newFilterStatus;
}
}

View File

@@ -84,15 +84,18 @@ namespace Content.Shared.Damage
public sealed class DamageableComponentState : ComponentState
{
public readonly Dictionary<string, FixedPoint2> DamageDict;
public readonly string? DamageContainerId;
public readonly string? ModifierSetId;
public readonly FixedPoint2? HealthBarThreshold;
public DamageableComponentState(
Dictionary<string, FixedPoint2> damageDict,
string? damageContainerId,
string? modifierSetId,
FixedPoint2? healthBarThreshold)
{
DamageDict = damageDict;
DamageContainerId = damageContainerId;
ModifierSetId = modifierSetId;
HealthBarThreshold = healthBarThreshold;
}

View File

@@ -0,0 +1,14 @@
using Content.Shared.Dataset;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Damage.Events;
/// <summary>
/// Event for interrupting and changing the prefix for when an entity is being forced to say something
/// </summary>
[Serializable, NetSerializable]
public sealed class BeforeForceSayEvent(ProtoId<LocalizedDatasetPrototype> prefixDataset) : EntityEventArgs
{
public ProtoId<LocalizedDatasetPrototype> Prefix = prefixDataset;
}

View File

@@ -1,8 +1,8 @@
using Content.Shared.Damage.Prototypes;
using Content.Shared.Dataset;
using Content.Shared.FixedPoint;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
namespace Content.Shared.Damage.ForceSay;
@@ -30,14 +30,7 @@ public sealed partial class DamageForceSayComponent : Component
/// The fluent string prefix to use when picking a random suffix
/// </summary>
[DataField]
public string ForceSayStringPrefix = "damage-force-say-";
/// <summary>
/// The number of suffixes that exist for use with <see cref="ForceSayStringPrefix"/>.
/// i.e. (prefix)-1 through (prefix)-(count)
/// </summary>
[DataField]
public int ForceSayStringCount = 7;
public ProtoId<LocalizedDatasetPrototype> ForceSayStringDataset = "ForceSayStringDataset";
/// <summary>
/// The amount of total damage between <see cref="ValidDamageGroups"/> that needs to be taken before

View File

@@ -228,12 +228,12 @@ namespace Content.Shared.Damage
{
if (_netMan.IsServer)
{
args.State = new DamageableComponentState(component.Damage.DamageDict, component.DamageModifierSetId, component.HealthBarThreshold);
args.State = new DamageableComponentState(component.Damage.DamageDict, component.DamageContainerID, component.DamageModifierSetId, component.HealthBarThreshold);
}
else
{
// avoid mispredicting damage on newly spawned entities.
args.State = new DamageableComponentState(component.Damage.DamageDict.ShallowClone(), component.DamageModifierSetId, component.HealthBarThreshold);
args.State = new DamageableComponentState(component.Damage.DamageDict.ShallowClone(), component.DamageContainerID, component.DamageModifierSetId, component.HealthBarThreshold);
}
}
@@ -266,6 +266,7 @@ namespace Content.Shared.Damage
return;
}
component.DamageContainerID = state.DamageContainerId;
component.DamageModifierSetId = state.ModifierSetId;
component.HealthBarThreshold = state.HealthBarThreshold;

View File

@@ -32,6 +32,8 @@ namespace Content.Shared.Decals
node.TryGetValue(new ValueDataNode("version"), out var versionNode);
var version = ((ValueDataNode?) versionNode)?.AsInt() ?? 1;
Dictionary<Vector2i, DecalChunk> dictionary;
uint nextIndex = 0;
var ids = new HashSet<uint>();
// TODO: Dump this when we don't need support anymore.
if (version > 1)
@@ -53,22 +55,31 @@ namespace Content.Shared.Decals
var chunkOrigin = SharedMapSystem.GetChunkIndices(coords, SharedDecalSystem.ChunkSize);
var chunk = dictionary.GetOrNew(chunkOrigin);
var decal = new Decal(coords, data.Id, data.Color, data.Angle, data.ZIndex, data.Cleanable);
chunk.Decals.Add(dUid, decal);
nextIndex = Math.Max(nextIndex, dUid);
// Re-used ID somehow
// This will bump all IDs by up to 1 but will ensure the map is still readable.
if (!ids.Add(dUid))
{
dUid = nextIndex++;
ids.Add(dUid);
}
chunk.Decals[dUid] = decal;
}
}
}
else
{
dictionary = serializationManager.Read<Dictionary<Vector2i, DecalChunk>>(node, hookCtx, context, notNullableOverride: true);
}
uint nextIndex = 0;
foreach (var decals in dictionary.Values)
{
foreach (var uid in decals.Decals.Keys)
foreach (var decals in dictionary.Values)
{
nextIndex = Math.Max(uid, nextIndex);
foreach (var uid in decals.Decals.Keys)
{
nextIndex = Math.Max(uid, nextIndex);
}
}
}

View File

@@ -1,6 +1,5 @@
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Dice;

View File

@@ -1,12 +1,18 @@
using Content.Shared.Examine;
using Content.Shared.Interaction.Events;
using Content.Shared.Popups;
using Content.Shared.Throwing;
using Robust.Shared.GameStates;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Timing;
namespace Content.Shared.Dice;
public abstract class SharedDiceSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
public override void Initialize()
{
base.Initialize();
@@ -14,76 +20,67 @@ public abstract class SharedDiceSystem : EntitySystem
SubscribeLocalEvent<DiceComponent, UseInHandEvent>(OnUseInHand);
SubscribeLocalEvent<DiceComponent, LandEvent>(OnLand);
SubscribeLocalEvent<DiceComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<DiceComponent, AfterAutoHandleStateEvent>(OnDiceAfterHandleState);
}
private void OnDiceAfterHandleState(EntityUid uid, DiceComponent component, ref AfterAutoHandleStateEvent args)
{
UpdateVisuals(uid, component);
}
private void OnUseInHand(EntityUid uid, DiceComponent component, UseInHandEvent args)
private void OnUseInHand(Entity<DiceComponent> entity, ref UseInHandEvent args)
{
if (args.Handled)
return;
Roll(entity, args.User);
args.Handled = true;
Roll(uid, component);
}
private void OnLand(EntityUid uid, DiceComponent component, ref LandEvent args)
private void OnLand(Entity<DiceComponent> entity, ref LandEvent args)
{
Roll(uid, component);
Roll(entity);
}
private void OnExamined(EntityUid uid, DiceComponent dice, ExaminedEvent args)
private void OnExamined(Entity<DiceComponent> entity, ref ExaminedEvent args)
{
//No details check, since the sprite updates to show the side.
using (args.PushGroup(nameof(DiceComponent)))
{
args.PushMarkup(Loc.GetString("dice-component-on-examine-message-part-1", ("sidesAmount", dice.Sides)));
args.PushMarkup(Loc.GetString("dice-component-on-examine-message-part-1", ("sidesAmount", entity.Comp.Sides)));
args.PushMarkup(Loc.GetString("dice-component-on-examine-message-part-2",
("currentSide", dice.CurrentValue)));
("currentSide", entity.Comp.CurrentValue)));
}
}
public void SetCurrentSide(EntityUid uid, int side, DiceComponent? die = null)
private void SetCurrentSide(Entity<DiceComponent> entity, int side)
{
if (!Resolve(uid, ref die))
return;
if (side < 1 || side > die.Sides)
if (side < 1 || side > entity.Comp.Sides)
{
Log.Error($"Attempted to set die {ToPrettyString(uid)} to an invalid side ({side}).");
Log.Error($"Attempted to set die {ToPrettyString(entity)} to an invalid side ({side}).");
return;
}
die.CurrentValue = (side - die.Offset) * die.Multiplier;
Dirty(uid, die);
UpdateVisuals(uid, die);
entity.Comp.CurrentValue = (side - entity.Comp.Offset) * entity.Comp.Multiplier;
Dirty(entity);
}
public void SetCurrentValue(EntityUid uid, int value, DiceComponent? die = null)
public void SetCurrentValue(Entity<DiceComponent> entity, int value)
{
if (!Resolve(uid, ref die))
return;
if (value % die.Multiplier != 0 || value/ die.Multiplier + die.Offset < 1)
if (value % entity.Comp.Multiplier != 0 || value / entity.Comp.Multiplier + entity.Comp.Offset < 1)
{
Log.Error($"Attempted to set die {ToPrettyString(uid)} to an invalid value ({value}).");
Log.Error($"Attempted to set die {ToPrettyString(entity)} to an invalid value ({value}).");
return;
}
SetCurrentSide(uid, value / die.Multiplier + die.Offset, die);
SetCurrentSide(entity, value / entity.Comp.Multiplier + entity.Comp.Offset);
}
protected virtual void UpdateVisuals(EntityUid uid, DiceComponent? die = null)
private void Roll(Entity<DiceComponent> entity, EntityUid? user = null)
{
// See client system.
}
var rand = new System.Random((int)_timing.CurTick.Value);
public virtual void Roll(EntityUid uid, DiceComponent? die = null)
{
// See the server system, client cannot predict rolling.
var roll = rand.Next(1, entity.Comp.Sides + 1);
SetCurrentSide(entity, roll);
var popupString = Loc.GetString("dice-component-on-roll-land",
("die", entity),
("currentSide", entity.Comp.CurrentValue));
_popup.PopupPredicted(popupString, entity, user);
_audio.PlayPredicted(entity.Comp.Sound, entity, user);
}
}

View File

@@ -24,6 +24,7 @@ public sealed partial class DisposalDoAfterEvent : SimpleDoAfterEvent
public abstract class SharedDisposalUnitSystem : EntitySystem
{
[Dependency] protected readonly IGameTiming GameTiming = default!;
[Dependency] protected readonly EmagSystem _emag = default!;
[Dependency] protected readonly MetaDataSystem Metadata = default!;
[Dependency] protected readonly SharedJointSystem Joints = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
@@ -102,6 +103,12 @@ public abstract class SharedDisposalUnitSystem : EntitySystem
protected void OnEmagged(EntityUid uid, SharedDisposalUnitComponent component, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (component.DisablePressure == true)
return;
component.DisablePressure = true;
args.Handled = true;
}

View File

@@ -314,7 +314,6 @@ public enum DoorState : byte
public enum DoorVisuals : byte
{
State,
Powered,
BoltLights,
EmergencyLights,
ClosedLights,

View File

@@ -1,3 +1,4 @@
using Content.Shared.Guidebook;
using Robust.Shared.GameStates;
namespace Content.Shared.Doors.Components
@@ -23,12 +24,14 @@ namespace Content.Shared.Doors.Components
/// Maximum pressure difference before the firelock will refuse to open, in kPa.
/// </summary>
[DataField("pressureThreshold"), ViewVariables(VVAccess.ReadWrite)]
[GuidebookData]
public float PressureThreshold = 20;
/// <summary>
/// Maximum temperature difference before the firelock will refuse to open, in k.
/// </summary>
[DataField("temperatureThreshold"), ViewVariables(VVAccess.ReadWrite)]
[GuidebookData]
public float TemperatureThreshold = 330;
// this used to check for hot-spots, but because accessing that data is a a mess this now just checks
// temperature. This does mean a cold room will trigger hot-air pop-ups

View File

@@ -23,7 +23,6 @@ using Robust.Shared.Timing;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Network;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
namespace Content.Shared.Doors.Systems;
@@ -34,6 +33,7 @@ public abstract partial class SharedDoorSystem : EntitySystem
[Dependency] private readonly INetManager _net = default!;
[Dependency] protected readonly SharedPhysicsSystem PhysicsSystem = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly SharedStunSystem _stunSystem = default!;
[Dependency] protected readonly TagSystem Tags = default!;
[Dependency] protected readonly SharedAudioSystem Audio = default!;
@@ -77,8 +77,6 @@ public abstract partial class SharedDoorSystem : EntitySystem
SubscribeLocalEvent<DoorComponent, WeldableAttemptEvent>(OnWeldAttempt);
SubscribeLocalEvent<DoorComponent, WeldableChangedEvent>(OnWeldChanged);
SubscribeLocalEvent<DoorComponent, GetPryTimeModifierEvent>(OnPryTimeModifier);
SubscribeLocalEvent<DoorComponent, OnAttemptEmagEvent>(OnAttemptEmag);
SubscribeLocalEvent<DoorComponent, GotEmaggedEvent>(OnEmagged);
}
@@ -118,31 +116,24 @@ public abstract partial class SharedDoorSystem : EntitySystem
_activeDoors.Remove(door);
}
private void OnAttemptEmag(EntityUid uid, DoorComponent door, ref OnAttemptEmagEvent args)
{
if (!TryComp<AirlockComponent>(uid, out var airlock))
{
args.Handled = true;
return;
}
if (IsBolted(uid) || !airlock.Powered)
{
args.Handled = true;
return;
}
if (door.State != DoorState.Closed)
{
args.Handled = true;
}
}
private void OnEmagged(EntityUid uid, DoorComponent door, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Access))
return;
if (!TryComp<AirlockComponent>(uid, out var airlock))
return;
if (IsBolted(uid) || !airlock.Powered)
return;
if (door.State != DoorState.Closed)
return;
if (!SetState(uid, DoorState.Emagging, door))
return;
Audio.PlayPredicted(door.SparkSound, uid, args.UserUid, AudioParams.Default.WithVolume(8));
args.Repeatable = true;
args.Handled = true;
}

View File

@@ -9,32 +9,42 @@ namespace Content.Shared.DrawDepth
/// <summary>
/// This is for sub-floors, the floors you see after prying off a tile.
/// </summary>
LowFloors = DrawDepthTag.Default - 11,
LowFloors = DrawDepthTag.Default - 14,
// various entity types that require different
// draw depths, as to avoid hiding
#region SubfloorEntities
ThickPipe = DrawDepthTag.Default - 10,
ThickWire = DrawDepthTag.Default - 9,
ThinPipe = DrawDepthTag.Default - 8,
ThinWire = DrawDepthTag.Default - 7,
ThickPipe = DrawDepthTag.Default - 13,
ThickWire = DrawDepthTag.Default - 12,
ThinPipe = DrawDepthTag.Default - 11,
ThinWire = DrawDepthTag.Default - 10,
#endregion
/// <summary>
/// Things that are beneath regular floors.
/// </summary>
BelowFloor = DrawDepthTag.Default - 7,
BelowFloor = DrawDepthTag.Default - 9,
/// <summary>
/// Used for entities like carpets.
/// </summary>
FloorTiles = DrawDepthTag.Default - 6,
FloorTiles = DrawDepthTag.Default - 8,
/// <summary>
/// Things that are actually right on the floor, like puddles. This does not mean objects like
/// Things that are actually right on the floor, like ice crust or atmos devices. This does not mean objects like
/// tables, even though they are technically "on the floor".
/// </summary>
FloorObjects = DrawDepthTag.Default - 5,
FloorObjects = DrawDepthTag.Default - 7,
/// <summary>
// Discrete drawdepth to avoid z-fighting with other FloorObjects but also above floor entities.
/// </summary>
Puddles = DrawDepthTag.Default - 6,
/// <summary>
// Objects that are on the floor, but should render above puddles. This includes kudzu, holopads, telepads and levers.
/// </summary>
HighFloorObjects = DrawDepthTag.Default - 5,
DeadMobs = DrawDepthTag.Default - 4,

View File

@@ -1,6 +1,8 @@
using Content.Shared.Emag.Systems;
using Content.Shared.Tag;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Serialization;
@@ -14,7 +16,21 @@ public sealed partial class EmagComponent : Component
/// <summary>
/// The tag that marks an entity as immune to emags
/// </summary>
[DataField("emagImmuneTag", customTypeSerializer: typeof(PrototypeIdSerializer<TagPrototype>)), ViewVariables(VVAccess.ReadWrite)]
[DataField]
[AutoNetworkedField]
public string EmagImmuneTag = "EmagImmune";
public ProtoId<TagPrototype> EmagImmuneTag = "EmagImmune";
/// <summary>
/// What type of emag effect this device will do
/// </summary>
[DataField]
[AutoNetworkedField]
public EmagType EmagType = EmagType.Interaction;
/// <summary>
/// What sound should the emag play when used
/// </summary>
[DataField]
[AutoNetworkedField]
public SoundSpecifier EmagSound = new SoundCollectionSpecifier("sparks");
}

View File

@@ -1,3 +1,4 @@
using Content.Shared.Emag.Systems;
using Robust.Shared.GameStates;
namespace Content.Shared.Emag.Components;
@@ -5,7 +6,12 @@ namespace Content.Shared.Emag.Components;
/// <summary>
/// Marker component for emagged entities
/// </summary>
[RegisterComponent, NetworkedComponent]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class EmaggedComponent : Component
{
/// <summary>
/// The EmagType flags that were used to emag this device
/// </summary>
[DataField, AutoNetworkedField]
public EmagType EmagType = EmagType.None;
}

View File

@@ -6,8 +6,9 @@ using Content.Shared.Emag.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Silicons.Laws.Components;
using Content.Shared.Tag;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Serialization;
namespace Content.Shared.Emag.Systems;
@@ -23,88 +24,124 @@ public sealed class EmagSystem : EntitySystem
[Dependency] private readonly SharedChargesSystem _charges = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EmagComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<EmaggedComponent, OnAccessOverriderAccessUpdatedEvent>(OnAccessOverriderAccessUpdated);
}
private void OnAccessOverriderAccessUpdated(Entity<EmaggedComponent> entity, ref OnAccessOverriderAccessUpdatedEvent args)
{
if (!CompareFlag(entity.Comp.EmagType, EmagType.Access))
return;
entity.Comp.EmagType &= ~EmagType.Access;
Dirty(entity);
}
private void OnAfterInteract(EntityUid uid, EmagComponent comp, AfterInteractEvent args)
{
if (!args.CanReach || args.Target is not { } target)
return;
args.Handled = TryUseEmag(uid, args.User, target, comp);
}
/// <summary>
/// Tries to use the emag on a target entity
/// </summary>
public bool TryUseEmag(EntityUid uid, EntityUid user, EntityUid target, EmagComponent? comp = null)
{
if (!Resolve(uid, ref comp, false))
return false;
if (_tag.HasTag(target, comp.EmagImmuneTag))
return false;
TryComp<LimitedChargesComponent>(uid, out var charges);
if (_charges.IsEmpty(uid, charges))
{
_popup.PopupClient(Loc.GetString("emag-no-charges"), user, user);
return false;
}
var handled = DoEmagEffect(user, target);
if (!handled)
return false;
_popup.PopupClient(Loc.GetString("emag-success", ("target", Identity.Entity(target, EntityManager))), user,
user, PopupType.Medium);
_adminLogger.Add(LogType.Emag, LogImpact.High, $"{ToPrettyString(user):player} emagged {ToPrettyString(target):target}");
if (charges != null)
_charges.UseCharge(uid, charges);
return true;
args.Handled = TryEmagEffect((uid, comp), args.User, target);
}
/// <summary>
/// Does the emag effect on a specified entity
/// </summary>
public bool DoEmagEffect(EntityUid user, EntityUid target)
public bool TryEmagEffect(Entity<EmagComponent?> ent, EntityUid user, EntityUid target)
{
// prevent emagging twice
if (HasComp<EmaggedComponent>(target))
if (!Resolve(ent, ref ent.Comp, false))
return false;
var onAttemptEmagEvent = new OnAttemptEmagEvent(user);
RaiseLocalEvent(target, ref onAttemptEmagEvent);
// prevent emagging if attempt fails
if (onAttemptEmagEvent.Handled)
if (_tag.HasTag(target, ent.Comp.EmagImmuneTag))
return false;
var emaggedEvent = new GotEmaggedEvent(user);
TryComp<LimitedChargesComponent>(ent, out var charges);
if (_charges.IsEmpty(ent, charges))
{
_popup.PopupClient(Loc.GetString("emag-no-charges"), user, user);
return false;
}
var emaggedEvent = new GotEmaggedEvent(user, ent.Comp.EmagType);
RaiseLocalEvent(target, ref emaggedEvent);
if (emaggedEvent.Handled && !emaggedEvent.Repeatable)
EnsureComp<EmaggedComponent>(target);
if (!emaggedEvent.Handled)
return false;
_popup.PopupPredicted(Loc.GetString("emag-success", ("target", Identity.Entity(target, EntityManager))), user, user, PopupType.Medium);
_audio.PlayPredicted(ent.Comp.EmagSound, ent, ent);
_adminLogger.Add(LogType.Emag, LogImpact.High, $"{ToPrettyString(user):player} emagged {ToPrettyString(target):target} with flag(s): {ent.Comp.EmagType}");
if (charges != null && emaggedEvent.Handled)
_charges.UseCharge(ent, charges);
if (!emaggedEvent.Repeatable)
{
EnsureComp<EmaggedComponent>(target, out var emaggedComp);
emaggedComp.EmagType |= ent.Comp.EmagType;
Dirty(target, emaggedComp);
}
return emaggedEvent.Handled;
}
/// <summary>
/// Checks whether an entity has the EmaggedComponent with a set flag.
/// </summary>
/// <param name="target">The target entity to check for the flag.</param>
/// <param name="flag">The EmagType flag to check for.</param>
/// <returns>True if entity has EmaggedComponent and the provided flag. False if the entity lacks EmaggedComponent or provided flag.</returns>
public bool CheckFlag(EntityUid target, EmagType flag)
{
if (!TryComp<EmaggedComponent>(target, out var comp))
return false;
if ((comp.EmagType & flag) == flag)
return true;
return false;
}
/// <summary>
/// Compares a flag to the target.
/// </summary>
/// <param name="target">The target flag to check.</param>
/// <param name="flag">The flag to check for within the target.</param>
/// <returns>True if target contains flag. Otherwise false.</returns>
public bool CompareFlag(EmagType target, EmagType flag)
{
if ((target & flag) == flag)
return true;
return false;
}
}
[Flags]
[Serializable, NetSerializable]
public enum EmagType : byte
{
None = 0,
Interaction = 1 << 1,
Access = 1 << 2
}
/// <summary>
/// Shows a popup to emag user (client side only!) and adds <see cref="EmaggedComponent"/> to the entity when handled
/// </summary>
/// <param name="UserUid">Emag user</param>
/// <param name="Type">The emag type to use</param>
/// <param name="Handled">Did the emagging succeed? Causes a user-only popup to show on client side</param>
/// <param name="Repeatable">Can the entity be emagged more than once? Prevents adding of <see cref="EmaggedComponent"/></param>
/// <remarks>Needs to be handled in shared/client, not just the server, to actually show the emagging popup</remarks>
[ByRefEvent]
public record struct GotEmaggedEvent(EntityUid UserUid, bool Handled = false, bool Repeatable = false);
[ByRefEvent]
public record struct OnAttemptEmagEvent(EntityUid UserUid, bool Handled = false);
public record struct GotEmaggedEvent(EntityUid UserUid, EmagType Type, bool Handled = false, bool Repeatable = false);

View File

@@ -13,13 +13,13 @@ public sealed partial class EnsnareableComponent : Component
/// <summary>
/// How much should this slow down the entities walk?
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public float WalkSpeed = 1.0f;
/// <summary>
/// How much should this slow down the entities sprint?
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public float SprintSpeed = 1.0f;
/// <summary>

View File

@@ -38,6 +38,12 @@ public sealed partial class EnsnaringComponent : Component
[DataField]
public float StaminaDamage = 55f;
/// <summary>
/// How many times can the ensnare be applied to the same target?
/// </summary>
[DataField]
public float MaxEnsnares = 1;
/// <summary>
/// Should this ensnare someone when thrown?
/// </summary>

View File

@@ -256,23 +256,18 @@ public abstract class SharedEnsnareableSystem : EntitySystem
if (!TryComp<EnsnareableComponent>(target, out var ensnareable))
return false;
// Need to insert before free legs check.
Container.Insert(ensnare, ensnareable.Container);
var numEnsnares = ensnareable.Container.ContainedEntities.Count;
var legs = _body.GetBodyChildrenOfType(target, BodyPartType.Leg).Count();
var ensnaredLegs = (2 * ensnareable.Container.ContainedEntities.Count);
var freeLegs = legs - ensnaredLegs;
if (freeLegs > 0)
//Don't do anything if the maximum number of ensnares is applied.
if (numEnsnares >= component.MaxEnsnares)
return false;
// Apply stamina damage to target if they weren't ensnared before.
if (ensnareable.IsEnsnared != true)
Container.Insert(ensnare, ensnareable.Container);
// Apply stamina damage to target
if (TryComp<StaminaComponent>(target, out var stamina))
{
if (TryComp<StaminaComponent>(target, out var stamina))
{
_stamina.TakeStaminaDamage(target, component.StaminaDamage, with: ensnare, component: stamina);
}
_stamina.TakeStaminaDamage(target, component.StaminaDamage, with: ensnare, component: stamina);
}
component.Ensnared = target;

View File

@@ -27,7 +27,7 @@ public sealed partial class ScatteringGrenadeComponent : Component
/// <summary>
/// If we have a pre-fill how many more can we spawn.
/// </summary>
[AutoNetworkedField]
[ViewVariables(VVAccess.ReadOnly), AutoNetworkedField]
public int UnspawnedCount;
/// <summary>
@@ -36,6 +36,12 @@ public sealed partial class ScatteringGrenadeComponent : Component
[DataField]
public int Capacity = 3;
/// <summary>
/// Number of grenades currently contained in the cluster (both spawned and unspawned)
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
public int Count => UnspawnedCount + Container.ContainedEntities.Count;
/// <summary>
/// Decides if contained entities trigger after getting launched
/// </summary>

View File

@@ -49,6 +49,10 @@ public abstract class SharedScatteringGrenadeSystem : EntitySystem
if (entity.Comp.Whitelist == null)
return;
// Make sure there's room for another grenade to be added
if (entity.Comp.Count >= entity.Comp.Capacity)
return;
if (args.Handled || !_whitelistSystem.IsValid(entity.Comp.Whitelist, args.Used))
return;
@@ -65,6 +69,6 @@ public abstract class SharedScatteringGrenadeSystem : EntitySystem
if (!TryComp<AppearanceComponent>(entity, out var appearanceComponent))
return;
_appearance.SetData(entity, ClusterGrenadeVisuals.GrenadesCounter, entity.Comp.UnspawnedCount + entity.Comp.Container.ContainedEntities.Count, appearanceComponent);
_appearance.SetData(entity, ClusterGrenadeVisuals.GrenadesCounter, entity.Comp.Count, appearanceComponent);
}
}

View File

@@ -59,12 +59,6 @@ public sealed partial class FaxMachineComponent : Component
[DataField]
public bool ReceiveNukeCodes { get; set; } = false;
/// <summary>
/// Sound to play when fax has been emagged
/// </summary>
[DataField]
public SoundSpecifier EmagSound = new SoundCollectionSpecifier("sparks");
/// <summary>
/// Sound to play when fax printing new message
/// </summary>

View File

@@ -4,7 +4,7 @@ namespace Content.Shared.Follower.Components;
[RegisterComponent]
[Access(typeof(FollowerSystem))]
[NetworkedComponent, AutoGenerateComponentState]
[NetworkedComponent, AutoGenerateComponentState(RaiseAfterAutoHandleState = true)]
public sealed partial class FollowerComponent : Component
{
[AutoNetworkedField, DataField("following")]

View File

@@ -21,10 +21,4 @@ public sealed partial class OrbitVisualsComponent : Component
/// How long should the orbit stop animation last in seconds?
/// </summary>
public float OrbitStopLength = 1.0f;
/// <summary>
/// How far along in the orbit, from 0 to 1, is this entity?
/// </summary>
[Animatable]
public float Orbit { get; set; } = 0.0f;
}

View File

@@ -7,6 +7,7 @@ using Content.Shared.Ghost;
using Content.Shared.Hands;
using Content.Shared.Movement.Events;
using Content.Shared.Movement.Pulling.Events;
using Content.Shared.Polymorph;
using Content.Shared.Tag;
using Content.Shared.Verbs;
using Robust.Shared.Containers;
@@ -39,11 +40,13 @@ public sealed class FollowerSystem : EntitySystem
SubscribeLocalEvent<FollowerComponent, MoveInputEvent>(OnFollowerMove);
SubscribeLocalEvent<FollowerComponent, PullStartedMessage>(OnPullStarted);
SubscribeLocalEvent<FollowerComponent, EntityTerminatingEvent>(OnFollowerTerminating);
SubscribeLocalEvent<FollowerComponent, AfterAutoHandleStateEvent>(OnAfterHandleState);
SubscribeLocalEvent<FollowedComponent, ComponentGetStateAttemptEvent>(OnFollowedAttempt);
SubscribeLocalEvent<FollowerComponent, GotEquippedHandEvent>(OnGotEquippedHand);
SubscribeLocalEvent<FollowedComponent, EntityTerminatingEvent>(OnFollowedTerminating);
SubscribeLocalEvent<BeforeSerializationEvent>(OnBeforeSave);
SubscribeLocalEvent<FollowedComponent, PolymorphedEvent>(OnFollowedPolymorphed);
}
private void OnFollowedAttempt(Entity<FollowedComponent> ent, ref ComponentGetStateAttemptEvent args)
@@ -142,6 +145,11 @@ public sealed class FollowerSystem : EntitySystem
StopFollowingEntity(uid, component.Following, deparent: false);
}
private void OnAfterHandleState(Entity<FollowerComponent> entity, ref AfterAutoHandleStateEvent args)
{
StartFollowingEntity(entity, entity.Comp.Following);
}
// Since we parent our observer to the followed entity, we need to detach
// before they get deleted so that we don't get recursively deleted too.
private void OnFollowedTerminating(EntityUid uid, FollowedComponent component, ref EntityTerminatingEvent args)
@@ -149,6 +157,15 @@ public sealed class FollowerSystem : EntitySystem
StopAllFollowers(uid, component);
}
private void OnFollowedPolymorphed(Entity<FollowedComponent> entity, ref PolymorphedEvent args)
{
foreach (var follower in entity.Comp.Following)
{
// Stop following the target's old entity and start following the new one
StartFollowingEntity(follower, args.NewEntity);
}
}
/// <summary>
/// Makes an entity follow another entity, by parenting to it.
/// </summary>
@@ -209,6 +226,7 @@ public sealed class FollowerSystem : EntitySystem
RaiseLocalEvent(follower, followerEv);
RaiseLocalEvent(entity, entityEv);
Dirty(entity, followedComp);
Dirty(follower, followerComp);
}
/// <summary>
@@ -220,7 +238,7 @@ public sealed class FollowerSystem : EntitySystem
if (!Resolve(target, ref followed, false))
return;
if (!HasComp<FollowerComponent>(uid))
if (!TryComp<FollowerComponent>(uid, out var followerComp) || followerComp.Following != target)
return;
followed.Following.Remove(uid);

View File

@@ -0,0 +1,13 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Forensics.Components;
/// <summary>
/// This component is for mobs that have DNA.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class DnaComponent : Component
{
[DataField("dna"), AutoNetworkedField]
public string DNA = String.Empty;
}

View File

@@ -41,7 +41,10 @@ namespace Content.Shared.GameTicking
private void OnRecordingStart(MappingDataNode metadata, List<object> events)
{
metadata["roundId"] = new ValueDataNode(RoundId.ToString());
if (RoundId != 0)
{
metadata["roundId"] = new ValueDataNode(RoundId.ToString());
}
}
public TimeSpan RoundDuration()

View File

@@ -188,11 +188,13 @@ public abstract partial class SharedHandsSystem : EntitySystem
if (args.Handled)
return;
// TODO: this pattern is super uncommon, but it might be worth changing GetUsedEntityEvent to be recursive.
if (TryComp<VirtualItemComponent>(component.ActiveHandEntity, out var virtualItem))
args.Used = virtualItem.BlockingEntity;
else
args.Used = component.ActiveHandEntity;
if (component.ActiveHandEntity.HasValue)
{
// allow for the item to return a different entity, e.g. virtual items
RaiseLocalEvent(component.ActiveHandEntity.Value, ref args);
}
args.Used ??= component.ActiveHandEntity;
}
//TODO: Actually shows all items/clothing/etc.

View File

@@ -1,3 +1,4 @@
using Content.Shared.Camera;
using Content.Shared.Hands.Components;
using Content.Shared.Movement.Systems;
@@ -7,6 +8,8 @@ public abstract partial class SharedHandsSystem
{
private void InitializeRelay()
{
SubscribeLocalEvent<HandsComponent, GetEyeOffsetRelayedEvent>(RelayEvent);
SubscribeLocalEvent<HandsComponent, GetEyePvsScaleRelayedEvent>(RelayEvent);
SubscribeLocalEvent<HandsComponent, RefreshMovementSpeedModifiersEvent>(RelayEvent);
}

View File

@@ -2,12 +2,12 @@ using Robust.Shared.GameStates;
namespace Content.Shared.Holopad;
[RegisterComponent, NetworkedComponent]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class HolographicAvatarComponent : Component
{
/// <summary>
/// The prototype sprite layer data for the hologram
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public PrototypeLayerData[] LayerData;
}

View File

@@ -1,4 +1,5 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using System.Numerics;
namespace Content.Shared.Holopad;
@@ -6,7 +7,7 @@ namespace Content.Shared.Holopad;
/// <summary>
/// Holds data pertaining to holopad holograms
/// </summary>
[RegisterComponent, NetworkedComponent]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class HolopadHologramComponent : Component
{
/// <summary>
@@ -64,8 +65,8 @@ public sealed partial class HolopadHologramComponent : Component
public Vector2 Offset = new Vector2();
/// <summary>
/// A user that are linked to this hologram
/// An entity that is linked to this hologram
/// </summary>
[ViewVariables]
public Entity<HolopadComponent>? LinkedHolopad;
[ViewVariables, AutoNetworkedField]
public EntityUid? LinkedEntity = null;
}

View File

@@ -20,29 +20,6 @@ public sealed partial class HolopadUserComponent : Component
public HashSet<Entity<HolopadComponent>> LinkedHolopads = new();
}
/// <summary>
/// A networked event raised when the visual state of a hologram is being updated
/// </summary>
[Serializable, NetSerializable]
public sealed class HolopadHologramVisualsUpdateEvent : EntityEventArgs
{
/// <summary>
/// The hologram being updated
/// </summary>
public readonly NetEntity Hologram;
/// <summary>
/// The target the hologram is copying
/// </summary>
public readonly NetEntity? Target;
public HolopadHologramVisualsUpdateEvent(NetEntity hologram, NetEntity? target = null)
{
Hologram = hologram;
Target = target;
}
}
/// <summary>
/// A networked event raised when the visual state of a hologram is being updated
/// </summary>
@@ -65,40 +42,3 @@ public sealed class HolopadUserTypingChangedEvent : EntityEventArgs
IsTyping = isTyping;
}
}
/// <summary>
/// A networked event raised by the server to request the current visual state of a target player entity
/// </summary>
[Serializable, NetSerializable]
public sealed class PlayerSpriteStateRequest : EntityEventArgs
{
/// <summary>
/// The player entity in question
/// </summary>
public readonly NetEntity TargetPlayer;
public PlayerSpriteStateRequest(NetEntity targetPlayer)
{
TargetPlayer = targetPlayer;
}
}
/// <summary>
/// The client's response to a <see cref="PlayerSpriteStateRequest"/>
/// </summary>
[Serializable, NetSerializable]
public sealed class PlayerSpriteStateMessage : EntityEventArgs
{
public readonly NetEntity SpriteEntity;
/// <summary>
/// Data needed to reconstruct the player's sprite component layers
/// </summary>
public readonly PrototypeLayerData[]? SpriteLayerData;
public PlayerSpriteStateMessage(NetEntity spriteEntity, PrototypeLayerData[]? spriteLayerData = null)
{
SpriteEntity = spriteEntity;
SpriteLayerData = spriteLayerData;
}
}

View File

@@ -132,6 +132,34 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
Dirty(uid, humanoid);
}
/// <summary>
/// Clones a humanoid's appearance to a target mob, provided they both have humanoid components.
/// </summary>
/// <param name="source">Source entity to fetch the original appearance from.</param>
/// <param name="target">Target entity to apply the source entity's appearance to.</param>
/// <param name="sourceHumanoid">Source entity's humanoid component.</param>
/// <param name="targetHumanoid">Target entity's humanoid component.</param>
public void CloneAppearance(EntityUid source, EntityUid target, HumanoidAppearanceComponent? sourceHumanoid = null,
HumanoidAppearanceComponent? targetHumanoid = null)
{
if (!Resolve(source, ref sourceHumanoid) || !Resolve(target, ref targetHumanoid))
return;
targetHumanoid.Species = sourceHumanoid.Species;
targetHumanoid.SkinColor = sourceHumanoid.SkinColor;
targetHumanoid.EyeColor = sourceHumanoid.EyeColor;
targetHumanoid.Age = sourceHumanoid.Age;
SetSex(target, sourceHumanoid.Sex, false, targetHumanoid);
targetHumanoid.CustomBaseLayers = new(sourceHumanoid.CustomBaseLayers);
targetHumanoid.MarkingSet = new(sourceHumanoid.MarkingSet);
targetHumanoid.Gender = sourceHumanoid.Gender;
if (TryComp<GrammarComponent>(target, out var grammar))
grammar.Gender = sourceHumanoid.Gender;
Dirty(target, targetHumanoid);
}
/// <summary>
/// Sets the visibility for multiple layers at once on a humanoid's sprite.
/// </summary>

View File

@@ -10,5 +10,6 @@ namespace Content.Shared.Implants.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class ImplantedComponent : Component
{
[ViewVariables(VVAccess.ReadOnly)]
public Container ImplantContainer = default!;
}

View File

@@ -0,0 +1,37 @@
using Content.Shared.Radio;
using Robust.Shared.Prototypes;
namespace Content.Shared.Implants.Components;
/// <summary>
/// Gives the user access to a given channel without the need for a headset.
/// </summary>
[RegisterComponent]
public sealed partial class RadioImplantComponent : Component
{
/// <summary>
/// The radio channel(s) to grant access to.
/// </summary>
[DataField(required: true)]
public HashSet<ProtoId<RadioChannelPrototype>> RadioChannels = new();
/// <summary>
/// The radio channels that have been added by the implant to a user's ActiveRadioComponent.
/// Used to track which channels were successfully added (not already in user)
/// </summary>
/// <remarks>
/// Should not be modified outside RadioImplantSystem.cs
/// </remarks>
[DataField]
public HashSet<ProtoId<RadioChannelPrototype>> ActiveAddedChannels = new();
/// <summary>
/// The radio channels that have been added by the implant to a user's IntrinsicRadioTransmitterComponent.
/// Used to track which channels were successfully added (not already in user)
/// </summary>
/// <remarks>
/// Should not be modified outside RadioImplantSystem.cs
/// </remarks>
[DataField]
public HashSet<ProtoId<RadioChannelPrototype>> TransmitterAddedChannels = new();
}

View File

@@ -52,7 +52,14 @@ public abstract class SharedImplanterSystem : EntitySystem
args.PushMarkup(Loc.GetString("implanter-contained-implant-text", ("desc", component.ImplantData.Item2)));
}
public bool CheckSameImplant(EntityUid target, EntityUid implant)
{
if (!TryComp<ImplantedComponent>(target, out var implanted))
return false;
var implantPrototype = Prototype(implant);
return implanted.ImplantContainer.ContainedEntities.Any(entity => Prototype(entity) == implantPrototype);
}
//Instantly implant something and add all necessary components and containers.
//Set to draw mode if not implant only
public void Implant(EntityUid user, EntityUid target, EntityUid implanter, ImplanterComponent component)
@@ -60,6 +67,16 @@ public abstract class SharedImplanterSystem : EntitySystem
if (!CanImplant(user, target, implanter, component, out var implant, out var implantComp))
return;
// Check if we are trying to implant a implant which is already implanted
// Check AFTER the doafter to prevent "is it a fake?" metagaming against deceptive implants
if (!component.AllowMultipleImplants && CheckSameImplant(target, implant.Value))
{
var name = Identity.Name(target, EntityManager, user);
var msg = Loc.GetString("implanter-component-implant-already", ("implant", implant), ("target", name));
_popup.PopupEntity(msg, target, user);
return;
}
//If the target doesn't have the implanted component, add it.
var implantedComp = EnsureComp<ImplantedComponent>(target);
var implantContainer = implantedComp.ImplantContainer;

View File

@@ -1,4 +1,3 @@
using System.Linq;
using Content.Shared.Actions;
using Content.Shared.Implants.Components;
using Content.Shared.Interaction;
@@ -8,6 +7,7 @@ using Content.Shared.Tag;
using JetBrains.Annotations;
using Robust.Shared.Containers;
using Robust.Shared.Network;
using System.Linq;
namespace Content.Shared.Implants;
@@ -17,6 +17,7 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
public const string BaseStorageId = "storagebase";
@@ -75,16 +76,11 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
if (!_container.TryGetContainer(uid, BaseStorageId, out var storageImplant))
return;
var entCoords = Transform(component.ImplantedEntity.Value).Coordinates;
var containedEntites = storageImplant.ContainedEntities.ToArray();
foreach (var entity in containedEntites)
{
if (Terminating(entity))
continue;
_container.RemoveEntity(storageImplant.Owner, entity, force: true, destination: entCoords);
_transformSystem.DropNextTo(entity, uid);
}
}

View File

@@ -28,7 +28,6 @@ using Content.Shared.UserInterface;
using Content.Shared.Verbs;
using Content.Shared.Wall;
using JetBrains.Annotations;
using Robust.Shared.Configuration;
using Robust.Shared.Containers;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
@@ -38,7 +37,6 @@ using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -65,12 +63,10 @@ namespace Content.Shared.Interaction
[Dependency] private readonly UseDelaySystem _useDelay = default!;
[Dependency] private readonly PullingSystem _pullSystem = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly TagSystem _tagSystem = default!;
[Dependency] private readonly SharedUserInterfaceSystem _ui = default!;
[Dependency] private readonly SharedStrippableSystem _strippable = default!;
[Dependency] private readonly SharedPlayerRateLimitManager _rateLimit = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ISharedChatManager _chat = default!;
private EntityQuery<IgnoreUIRangeComponent> _ignoreUiRangeQuery;
@@ -667,7 +663,8 @@ namespace Content.Shared.Interaction
float range = InteractionRange,
CollisionGroup collisionMask = InRangeUnobstructedMask,
Ignored? predicate = null,
bool popup = false)
bool popup = false,
bool overlapCheck = true)
{
if (!Resolve(other, ref other.Comp))
return false;
@@ -687,7 +684,8 @@ namespace Content.Shared.Interaction
range,
collisionMask,
predicate,
popup);
popup,
overlapCheck);
}
/// <summary>
@@ -717,6 +715,7 @@ namespace Content.Shared.Interaction
/// <returns>
/// True if the two points are within a given range without being obstructed.
/// </returns>
/// <param name="overlapCheck">If true, if the broadphase query returns an overlap (0f distance) this function will early out true with no raycast made.</param>
public bool InRangeUnobstructed(
Entity<TransformComponent?> origin,
Entity<TransformComponent?> other,
@@ -725,7 +724,8 @@ namespace Content.Shared.Interaction
float range = InteractionRange,
CollisionGroup collisionMask = InRangeUnobstructedMask,
Ignored? predicate = null,
bool popup = false)
bool popup = false,
bool overlapCheck = true)
{
Ignored combinedPredicate = e => e == origin.Owner || (predicate?.Invoke(e) ?? false);
var inRange = true;
@@ -748,7 +748,7 @@ namespace Content.Shared.Interaction
fixtureB.FixtureCount > 0 &&
Resolve(origin, ref origin.Comp))
{
var (worldPosA, worldRotA) = origin.Comp.GetWorldPositionRotation();
var (worldPosA, worldRotA) = _transform.GetWorldPositionRotation(origin.Comp);
var xfA = new Transform(worldPosA, worldRotA);
var parentRotB = _transform.GetWorldRotation(otherCoordinates.EntityId);
var xfB = new Transform(targetPos.Position, parentRotB + otherAngle);
@@ -768,7 +768,7 @@ namespace Content.Shared.Interaction
inRange = false;
}
// Overlap, early out and no raycast.
else if (distance.Equals(0f))
else if (overlapCheck && distance.Equals(0f))
{
return true;
}
@@ -821,7 +821,7 @@ namespace Content.Shared.Interaction
Ignored? predicate = null)
{
var transform = Transform(target);
var (position, rotation) = transform.GetWorldPositionRotation();
var (position, rotation) = _transform.GetWorldPositionRotation(transform);
var mapPos = new MapCoordinates(position, transform.MapID);
var combinedPredicate = GetPredicate(origin, target, mapPos, rotation, collisionMask, predicate);
@@ -1409,7 +1409,7 @@ namespace Content.Shared.Interaction
/// <returns>If there is an entity being used.</returns>
public bool TryGetUsedEntity(EntityUid user, [NotNullWhen(true)] out EntityUid? used, bool checkCanUse = true)
{
var ev = new GetUsedEntityEvent();
var ev = new GetUsedEntityEvent(user);
RaiseLocalEvent(user, ref ev);
used = ev.Used;
@@ -1460,8 +1460,9 @@ namespace Content.Shared.Interaction
/// Raised directed by-ref on an entity to determine what item will be used in interactions.
/// </summary>
[ByRefEvent]
public record struct GetUsedEntityEvent()
public record struct GetUsedEntityEvent(EntityUid User)
{
public EntityUid User = User;
public EntityUid? Used = null;
public bool Handled => Used != null;

View File

@@ -1,13 +1,10 @@
namespace Content.Shared.Inventory.Events;
public sealed class RefreshEquipmentHudEvent<T> : EntityEventArgs, IInventoryRelayEvent where T : IComponent
[ByRefEvent]
public record struct RefreshEquipmentHudEvent<T>(SlotFlags TargetSlots) : IInventoryRelayEvent
where T : IComponent
{
public SlotFlags TargetSlots { get; init; }
public SlotFlags TargetSlots { get; } = TargetSlots;
public bool Active = false;
public List<T> Components = new();
public RefreshEquipmentHudEvent(SlotFlags targetSlots)
{
TargetSlots = targetSlots;
}
}

View File

@@ -40,7 +40,7 @@ public partial class InventorySystem
/// </summary>
public bool TryGetContainingSlot(Entity<TransformComponent?, MetaDataComponent?> entity, [NotNullWhen(true)] out SlotDefinition? slot)
{
if (!_containerSystem.TryGetContainingContainer(entity.Owner, out var container, entity.Comp2, entity.Comp1))
if (!_containerSystem.TryGetContainingContainer(entity, out var container))
{
slot = null;
return false;

View File

@@ -1,3 +1,4 @@
using Content.Shared.Armor;
using Content.Shared.Chat;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Hypospray.Events;
@@ -40,6 +41,7 @@ public partial class InventorySystem
SubscribeLocalEvent<InventoryComponent, TargetBeforeHyposprayInjectsEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, SelfBeforeGunShotEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, SelfBeforeClimbEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, CoefficientQueryEvent>(RelayInventoryEvent);
// by-ref events
SubscribeLocalEvent<InventoryComponent, GetExplosionResistanceEvent>(RefRelayInventoryEvent);
@@ -55,14 +57,14 @@ public partial class InventorySystem
SubscribeLocalEvent<InventoryComponent, SolutionScanEvent>(RelayInventoryEvent);
// ComponentActivatedClientSystems
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowJobIconsComponent>>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowHealthBarsComponent>>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowHealthIconsComponent>>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowHungerIconsComponent>>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowThirstIconsComponent>>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowMindShieldIconsComponent>>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowSyndicateIconsComponent>>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowCriminalRecordIconsComponent>>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowJobIconsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowHealthBarsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowHealthIconsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowHungerIconsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowThirstIconsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowMindShieldIconsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowSyndicateIconsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowCriminalRecordIconsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, GetVerbsEvent<EquipmentVerb>>(OnGetEquipmentVerbs);
}

View File

@@ -46,6 +46,8 @@ public abstract class SharedVirtualItemSystem : EntitySystem
SubscribeLocalEvent<VirtualItemComponent, BeforeRangedInteractEvent>(OnBeforeRangedInteract);
SubscribeLocalEvent<VirtualItemComponent, GettingInteractedWithAttemptEvent>(OnGettingInteractedWithAttemptEvent);
SubscribeLocalEvent<VirtualItemComponent, GetUsedEntityEvent>(OnGetUsedEntity);
}
/// <summary>
@@ -81,6 +83,23 @@ public abstract class SharedVirtualItemSystem : EntitySystem
args.Cancelled = true;
}
private void OnGetUsedEntity(Entity<VirtualItemComponent> ent, ref GetUsedEntityEvent args)
{
if (args.Handled)
return;
// if the user is holding the real item the virtual item points to,
// we allow them to use it in the interaction
foreach (var hand in _handsSystem.EnumerateHands(args.User))
{
if (hand.HeldEntity == ent.Comp.BlockingEntity)
{
args.Used = ent.Comp.BlockingEntity;
return;
}
}
}
#region Hands
/// <summary>

View File

@@ -0,0 +1,43 @@
using Robust.Shared.GameStates;
namespace Content.Shared.ItemRecall;
/// <summary>
/// Component for the ItemRecall action.
/// Used for marking a held item and recalling it back into your hand with second action use.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedItemRecallSystem))]
public sealed partial class ItemRecallComponent : Component
{
/// <summary>
/// The name the action should have while an entity is marked.
/// </summary>
[DataField]
public LocId? WhileMarkedName = "item-recall-marked-name";
/// <summary>
/// The description the action should have while an entity is marked.
/// </summary>
[DataField]
public LocId? WhileMarkedDescription = "item-recall-marked-description";
/// <summary>
/// The name the action starts with.
/// This shouldn't be set in yaml.
/// </summary>
[DataField]
public string? InitialName;
/// <summary>
/// The description the action starts with.
/// This shouldn't be set in yaml.
/// </summary>
[DataField]
public string? InitialDescription;
/// <summary>
/// The entity currently marked to be recalled by this action.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? MarkedEntity;
}

View File

@@ -0,0 +1,9 @@
using Content.Shared.Actions;
namespace Content.Shared.ItemRecall;
/// <summary>
/// Raised when using the ItemRecall action.
/// </summary>
[ByRefEvent]
public sealed partial class OnItemRecallActionEvent : InstantActionEvent;

View File

@@ -0,0 +1,18 @@
using Robust.Shared.GameStates;
using Robust.Shared.Utility;
namespace Content.Shared.ItemRecall;
/// <summary>
/// Component used as a marker for an item marked by the ItemRecall ability.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedItemRecallSystem))]
public sealed partial class RecallMarkerComponent : Component
{
/// <summary>
/// The action that marked this item.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? MarkedByAction;
}

View File

@@ -0,0 +1,187 @@
using Content.Shared.Actions;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Popups;
using Content.Shared.Projectiles;
using Robust.Shared.GameStates;
using Robust.Shared.Player;
namespace Content.Shared.ItemRecall;
/// <summary>
/// System for handling the ItemRecall ability for wizards.
/// </summary>
public abstract partial class SharedItemRecallSystem : EntitySystem
{
[Dependency] private readonly ISharedPlayerManager _player = default!;
[Dependency] private readonly SharedPvsOverrideSystem _pvs = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly SharedPopupSystem _popups = default!;
[Dependency] private readonly SharedProjectileSystem _proj = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ItemRecallComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ItemRecallComponent, OnItemRecallActionEvent>(OnItemRecallActionUse);
SubscribeLocalEvent<RecallMarkerComponent, ComponentShutdown>(OnRecallMarkerShutdown);
}
private void OnMapInit(Entity<ItemRecallComponent> ent, ref MapInitEvent args)
{
ent.Comp.InitialName = Name(ent);
ent.Comp.InitialDescription = Description(ent);
}
private void OnItemRecallActionUse(Entity<ItemRecallComponent> ent, ref OnItemRecallActionEvent args)
{
if (ent.Comp.MarkedEntity == null)
{
if (!TryComp<HandsComponent>(args.Performer, out var hands))
return;
var markItem = _hands.GetActiveItem((args.Performer, hands));
if (markItem == null)
{
_popups.PopupClient(Loc.GetString("item-recall-item-mark-empty"), args.Performer, args.Performer);
return;
}
if (HasComp<RecallMarkerComponent>(markItem))
{
_popups.PopupClient(Loc.GetString("item-recall-item-already-marked", ("item", markItem)), args.Performer, args.Performer);
return;
}
_popups.PopupClient(Loc.GetString("item-recall-item-marked", ("item", markItem.Value)), args.Performer, args.Performer);
TryMarkItem(ent, markItem.Value);
return;
}
RecallItem(ent.Comp.MarkedEntity.Value);
args.Handled = true;
}
private void RecallItem(Entity<RecallMarkerComponent?> ent)
{
if (!Resolve(ent.Owner, ref ent.Comp, false))
return;
if (!TryComp<InstantActionComponent>(ent.Comp.MarkedByAction, out var instantAction))
return;
var actionOwner = instantAction.AttachedEntity;
if (actionOwner == null)
return;
if (TryComp<EmbeddableProjectileComponent>(ent, out var projectile))
_proj.EmbedDetach(ent, projectile, actionOwner.Value);
_popups.PopupPredicted(Loc.GetString("item-recall-item-summon", ("item", ent)), actionOwner.Value, actionOwner.Value);
_hands.TryForcePickupAnyHand(actionOwner.Value, ent);
}
private void OnRecallMarkerShutdown(Entity<RecallMarkerComponent> ent, ref ComponentShutdown args)
{
TryUnmarkItem(ent);
}
private void TryMarkItem(Entity<ItemRecallComponent> ent, EntityUid item)
{
if (!TryComp<InstantActionComponent>(ent, out var instantAction))
return;
var actionOwner = instantAction.AttachedEntity;
if (actionOwner == null)
return;
AddToPvsOverride(item, actionOwner.Value);
var marker = AddComp<RecallMarkerComponent>(item);
ent.Comp.MarkedEntity = item;
Dirty(ent);
marker.MarkedByAction = ent.Owner;
UpdateActionAppearance(ent);
Dirty(item, marker);
}
private void TryUnmarkItem(EntityUid item)
{
if (!TryComp<RecallMarkerComponent>(item, out var marker))
return;
if (!TryComp<InstantActionComponent>(marker.MarkedByAction, out var instantAction))
return;
if (TryComp<ItemRecallComponent>(marker.MarkedByAction, out var action))
{
// For some reason client thinks the station grid owns the action on client and this doesn't work. It doesn't work in PopupEntity(mispredicts) and PopupPredicted either(doesnt show).
// I don't have the heart to move this code to server because of this small thing.
// This line will only do something once that is fixed.
if (instantAction.AttachedEntity != null)
{
_popups.PopupClient(Loc.GetString("item-recall-item-unmark", ("item", item)), instantAction.AttachedEntity.Value, instantAction.AttachedEntity.Value, PopupType.MediumCaution);
RemoveFromPvsOverride(item, instantAction.AttachedEntity.Value);
}
action.MarkedEntity = null;
UpdateActionAppearance((marker.MarkedByAction.Value, action));
Dirty(marker.MarkedByAction.Value, action);
}
RemCompDeferred<RecallMarkerComponent>(item);
}
private void UpdateActionAppearance(Entity<ItemRecallComponent> action)
{
if (!TryComp<InstantActionComponent>(action, out var instantAction))
return;
if (action.Comp.MarkedEntity == null)
{
if (action.Comp.InitialName != null)
_metaData.SetEntityName(action, action.Comp.InitialName);
if (action.Comp.InitialDescription != null)
_metaData.SetEntityDescription(action, action.Comp.InitialDescription);
_actions.SetEntityIcon(action, null, instantAction);
}
else
{
if (action.Comp.WhileMarkedName != null)
_metaData.SetEntityName(action, Loc.GetString(action.Comp.WhileMarkedName,
("item", action.Comp.MarkedEntity.Value)));
if (action.Comp.WhileMarkedDescription != null)
_metaData.SetEntityDescription(action, Loc.GetString(action.Comp.WhileMarkedDescription,
("item", action.Comp.MarkedEntity.Value)));
_actions.SetEntityIcon(action, action.Comp.MarkedEntity, instantAction);
}
}
private void AddToPvsOverride(EntityUid uid, EntityUid user)
{
if (!_player.TryGetSessionByEntity(user, out var mindSession))
return;
_pvs.AddSessionOverride(uid, mindSession);
}
private void RemoveFromPvsOverride(EntityUid uid, EntityUid user)
{
if (!_player.TryGetSessionByEntity(user, out var mindSession))
return;
_pvs.RemoveSessionOverride(uid, mindSession);
}
}

View File

@@ -1,4 +1,4 @@
using Content.Shared.Research.Prototypes;
using Content.Shared.Lathe.Prototypes;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
@@ -9,15 +9,15 @@ namespace Content.Shared.Lathe
public sealed partial class EmagLatheRecipesComponent : Component
{
/// <summary>
/// All of the dynamic recipes that the lathe is capable to get using EMAG
/// All of the dynamic recipe packs that the lathe is capable to get using EMAG
/// </summary>
[DataField, AutoNetworkedField]
public List<ProtoId<LatheRecipePrototype>> EmagDynamicRecipes = new();
public List<ProtoId<LatheRecipePackPrototype>> EmagDynamicPacks = new();
/// <summary>
/// All of the static recipes that the lathe is capable to get using EMAG
/// All of the static recipe packs that the lathe is capable to get using EMAG
/// </summary>
[DataField, AutoNetworkedField]
public List<ProtoId<LatheRecipePrototype>> EmagStaticRecipes = new();
public List<ProtoId<LatheRecipePackPrototype>> EmagStaticPacks = new();
}
}

View File

@@ -1,4 +1,5 @@
using Content.Shared.Construction.Prototypes;
using Content.Shared.Lathe.Prototypes;
using Content.Shared.Research.Prototypes;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
@@ -10,16 +11,16 @@ namespace Content.Shared.Lathe
public sealed partial class LatheComponent : Component
{
/// <summary>
/// All of the recipes that the lathe has by default
/// All of the recipe packs that the lathe has by default
/// </summary>
[DataField]
public List<ProtoId<LatheRecipePrototype>> StaticRecipes = new();
public List<ProtoId<LatheRecipePackPrototype>> StaticPacks = new();
/// <summary>
/// All of the recipes that the lathe is capable of researching
/// All of the recipe packs that the lathe is capable of researching
/// </summary>
[DataField]
public List<ProtoId<LatheRecipePrototype>> DynamicRecipes = new();
public List<ProtoId<LatheRecipePackPrototype>> DynamicPacks = new();
/// <summary>
/// The lathe's construction queue

View File

@@ -0,0 +1,31 @@
using Content.Shared.Research.Prototypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
namespace Content.Shared.Lathe.Prototypes;
/// <summary>
/// A pack of lathe recipes that one or more lathes can use.
/// Packs will inherit the parents recipes when using inheritance, so you don't need to copy paste them.
/// </summary>
[Prototype]
public sealed partial class LatheRecipePackPrototype : IPrototype, IInheritingPrototype
{
[ViewVariables]
[IdDataField]
public string ID { get; private set; } = default!;
[ParentDataField(typeof(AbstractPrototypeIdArraySerializer<LatheRecipePackPrototype>))]
public string[]? Parents { get; }
[NeverPushInheritance]
[AbstractDataField]
public bool Abstract { get; }
/// <summary>
/// The lathe recipes contained by this pack.
/// </summary>
[DataField(required: true)]
[AlwaysPushInheritance]
public HashSet<ProtoId<LatheRecipePrototype>> Recipes = new();
}

View File

@@ -2,6 +2,7 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.Emag.Systems;
using Content.Shared.Examine;
using Content.Shared.Lathe.Prototypes;
using Content.Shared.Localizations;
using Content.Shared.Materials;
using Content.Shared.Research.Prototypes;
@@ -18,6 +19,7 @@ public abstract class SharedLatheSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly SharedMaterialStorageSystem _materialStorage = default!;
[Dependency] private readonly EmagSystem _emag = default!;
public readonly Dictionary<string, List<LatheRecipePrototype>> InverseRecipes = new();
@@ -32,6 +34,18 @@ public abstract class SharedLatheSystem : EntitySystem
BuildInverseRecipeDictionary();
}
/// <summary>
/// Add every recipe in the list of recipe packs to a single hashset.
/// </summary>
public void AddRecipesFromPacks(HashSet<ProtoId<LatheRecipePrototype>> recipes, IEnumerable<ProtoId<LatheRecipePackPrototype>> packs)
{
foreach (var id in packs)
{
var pack = _proto.Index(id);
recipes.UnionWith(pack.Recipes);
}
}
private void OnExamined(Entity<LatheComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
@@ -66,6 +80,12 @@ public abstract class SharedLatheSystem : EntitySystem
private void OnEmagged(EntityUid uid, EmagLatheRecipesComponent component, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(uid, EmagType.Interaction))
return;
args.Handled = true;
}

View File

@@ -2,6 +2,7 @@ using Content.Shared.Actions;
using Content.Shared.Emag.Systems;
using Content.Shared.Light.Components;
using Content.Shared.Mind.Components;
using Content.Shared.Storage.Components;
using Content.Shared.Toggleable;
using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems;
@@ -22,6 +23,7 @@ public sealed class UnpoweredFlashlightSystem : EntitySystem
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly SharedPointLightSystem _light = default!;
[Dependency] private readonly EmagSystem _emag = default!;
public override void Initialize()
{
@@ -78,6 +80,9 @@ public sealed class UnpoweredFlashlightSystem : EntitySystem
private void OnGotEmagged(EntityUid uid, UnpoweredFlashlightComponent component, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (!_light.TryGetLight(uid, out var light))
return;

View File

@@ -148,7 +148,7 @@ namespace Content.Shared.Localizations
public static string FormatPlaytime(TimeSpan time)
{
var hours = (int)time.TotalHours;
var minutes = time.Minutes;
var minutes = (int)Math.Ceiling(time.TotalMinutes);
return Loc.GetString($"zzzz-fmt-playtime", ("hours", hours), ("minutes", minutes));
}

View File

@@ -54,9 +54,9 @@ public sealed partial class LockComponent : Component
/// <summary>
/// Whether or not an emag disables it.
/// </summary>
[DataField("breakOnEmag")]
[DataField]
[AutoNetworkedField]
public bool BreakOnEmag = true;
public bool BreakOnAccessBreaker = true;
/// <summary>
/// Amount of do-after time needed to lock the entity.

View File

@@ -28,6 +28,7 @@ public sealed class LockSystem : EntitySystem
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly ActivatableUISystem _activatableUI = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPopupSystem _sharedPopupSystem = default!;
@@ -295,7 +296,10 @@ public sealed class LockSystem : EntitySystem
private void OnEmagged(EntityUid uid, LockComponent component, ref GotEmaggedEvent args)
{
if (!component.Locked || !component.BreakOnEmag)
if (!_emag.CompareFlag(args.Type, EmagType.Access))
return;
if (!component.Locked || !component.BreakOnAccessBreaker)
return;
_audio.PlayPredicted(component.UnlockSound, uid, args.UserUid);
@@ -307,7 +311,7 @@ public sealed class LockSystem : EntitySystem
var ev = new LockToggledEvent(false);
RaiseLocalEvent(uid, ref ev, true);
RemComp<LockComponent>(uid); //Literally destroys the lock as a tell it was emagged
args.Repeatable = true;
args.Handled = true;
}

View File

@@ -0,0 +1,11 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Magic.Components;
// Used on whitelist for animate spell/wand
[RegisterComponent, NetworkedComponent]
public sealed partial class AnimateableComponent : Component
{
}

View File

@@ -0,0 +1,16 @@
using Content.Shared.Actions;
using Robust.Shared.Prototypes;
namespace Content.Shared.Magic.Events;
public sealed partial class AnimateSpellEvent : EntityTargetActionEvent, ISpeakSpell
{
[DataField]
public string? Speech { get; private set; }
[DataField]
public ComponentRegistry AddComponents = new();
[DataField]
public HashSet<string> RemoveComponents = new();
}

View File

@@ -0,0 +1,24 @@
using Content.Shared.Actions;
using Content.Shared.Chat.Prototypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Magic.Events;
public sealed partial class VoidApplauseSpellEvent : EntityTargetActionEvent, ISpeakSpell
{
[DataField]
public string? Speech { get; private set; }
/// <summary>
/// Emote to use.
/// </summary>
[DataField]
public ProtoId<EmotePrototype> Emote = "ClapSingle";
/// <summary>
/// Visual effect entity that is spawned at both the user's and the target's location.
/// </summary>
[DataField]
public EntProtoId Effect = "EffectVoidBlink";
}

View File

@@ -1,3 +1,4 @@
using System.Linq;
using System.Numerics;
using Content.Shared.Actions;
using Content.Shared.Body.Components;
@@ -7,7 +8,6 @@ using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Humanoid;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Lock;
@@ -15,9 +15,6 @@ using Content.Shared.Magic.Components;
using Content.Shared.Magic.Events;
using Content.Shared.Maps;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.Speech.Muting;
@@ -30,13 +27,20 @@ using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Network;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Spawners;
namespace Content.Shared.Magic;
// TODO: Move BeforeCast & Prerequirements (like Wizard clothes) to action comp
// Alt idea - make it its own comp and split, like the Charge PR
// TODO: Move speech to actionComp or again, its own ECS
// TODO: Use the MagicComp just for pure backend things like spawning patterns?
/// <summary>
/// Handles learning and using spells (actions)
/// </summary>
@@ -60,7 +64,6 @@ public abstract class SharedMagicSystem : EntitySystem
[Dependency] private readonly LockSystem _lock = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedStunSystem _stun = default!;
@@ -80,79 +83,8 @@ public abstract class SharedMagicSystem : EntitySystem
SubscribeLocalEvent<ChargeSpellEvent>(OnChargeSpell);
SubscribeLocalEvent<RandomGlobalSpawnSpellEvent>(OnRandomGlobalSpawnSpell);
SubscribeLocalEvent<MindSwapSpellEvent>(OnMindSwapSpell);
// Spell wishlist
// A wishlish of spells that I'd like to implement or planning on implementing in a future PR
// TODO: InstantDoAfterSpell and WorldDoafterSpell
// Both would be an action that take in an event, that passes an event to trigger once the doafter is done
// This would be three events:
// 1 - Event that triggers from the action that starts the doafter
// 2 - The doafter event itself, which passes the event with it
// 3 - The event to trigger once the do-after finishes
// TODO: Inanimate objects to life ECS
// AI sentience
// TODO: Flesh2Stone
// Entity Target spell
// Synergy with Inanimate object to life (detects player and allows player to move around)
// TODO: Lightning Spell
// Should just fire lightning, try to prevent arc back to caster
// TODO: Magic Missile (homing projectile ecs)
// Instant action, target any player (except self) on screen
// TODO: Random projectile ECS for magic-carp, wand of magic
// TODO: Recall Spell
// mark any item in hand to recall
// ItemRecallComponent
// Event adds the component if it doesn't exist and the performer isn't stored in the comp
// 2nd firing of the event checks to see if the recall comp has this uid, and if it does it calls it
// if no free hands, summon at feet
// if item deleted, clear stored item
// TODO: Jaunt (should be its own ECS)
// Instant action
// When clicked, disappear/reappear (goes to paused map)
// option to restrict to tiles
// option for requiring entry/exit (blood jaunt)
// speed option
// TODO: Summon Events
// List of wizard events to add into the event pool that frequently activate
// floor is lava
// change places
// ECS that when triggered, will periodically trigger a random GameRule
// Would need a controller/controller entity?
// TODO: Summon Guns
// Summon a random gun at peoples feet
// Get every alive player (not in cryo, not a simplemob)
// TODO: After Antag Rework - Rare chance of giving gun collector status to people
// TODO: Summon Magic
// Summon a random magic wand at peoples feet
// Get every alive player (not in cryo, not a simplemob)
// TODO: After Antag Rework - Rare chance of giving magic collector status to people
// TODO: Bottle of Blood
// Summons Slaughter Demon
// TODO: Slaughter Demon
// Also see Jaunt
// TODO: Field Spells
// Should be able to specify a grid of tiles (3x3 for example) that it effects
// Timed despawn - so it doesn't last forever
// Ignore caster - for spells that shouldn't effect the caster (ie if timestop should effect the caster)
// TODO: Touch toggle spell
// 1 - When toggled on, show in hand
// 2 - Block hand when toggled on
// - Require free hand
// 3 - use spell event when toggled & click
SubscribeLocalEvent<VoidApplauseSpellEvent>(OnVoidApplause);
SubscribeLocalEvent<AnimateSpellEvent>(OnAnimateSpell);
}
private void OnBeforeCastSpell(Entity<MagicComponent> ent, ref BeforeCastSpellEvent args)
@@ -371,22 +303,8 @@ public abstract class SharedMagicSystem : EntitySystem
ev.Handled = true;
Speak(ev);
foreach (var toRemove in ev.ToRemove)
{
if (_compFact.TryGetRegistration(toRemove, out var registration))
RemComp(ev.Target, registration.Type);
}
foreach (var (name, data) in ev.ToAdd)
{
if (HasComp(ev.Target, data.Component.GetType()))
continue;
var component = (Component)_compFact.GetComponent(name);
var temp = (object)component;
_seriMan.CopyTo(data.Component, ref temp);
EntityManager.AddComponent(ev.Target, (Component)temp!);
}
RemoveComponents(ev.Target, ev.ToRemove);
AddComponents(ev.Target, ev.ToAdd);
}
// End Change Component Spells
#endregion
@@ -402,8 +320,7 @@ public abstract class SharedMagicSystem : EntitySystem
return;
var transform = Transform(args.Performer);
if (transform.MapID != args.Target.GetMapId(EntityManager) || !_interaction.InRangeUnobstructed(args.Performer, args.Target, range: 1000F, collisionMask: CollisionGroup.Opaque, popup: true))
if (transform.MapID != _transform.GetMapId(args.Target) || !_interaction.InRangeUnobstructed(args.Performer, args.Target, range: 1000F, collisionMask: CollisionGroup.Opaque, popup: true))
return;
_transform.SetCoordinates(args.Performer, args.Target);
@@ -411,6 +328,17 @@ public abstract class SharedMagicSystem : EntitySystem
Speak(args);
args.Handled = true;
}
public virtual void OnVoidApplause(VoidApplauseSpellEvent ev)
{
if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer))
return;
ev.Handled = true;
Speak(ev);
_transform.SwapPositions(ev.Performer, ev.Target);
}
// End Teleport Spells
#endregion
#region Spell Helpers
@@ -433,9 +361,32 @@ public abstract class SharedMagicSystem : EntitySystem
comp.Uid = performer;
}
}
private void AddComponents(EntityUid target, ComponentRegistry comps)
{
foreach (var (name, data) in comps)
{
if (HasComp(target, data.Component.GetType()))
continue;
var component = (Component)_compFact.GetComponent(name);
var temp = (object)component;
_seriMan.CopyTo(data.Component, ref temp);
EntityManager.AddComponent(target, (Component)temp!);
}
}
private void RemoveComponents(EntityUid target, HashSet<string> comps)
{
foreach (var toRemove in comps)
{
if (_compFact.TryGetRegistration(toRemove, out var registration))
RemComp(target, registration.Type);
}
}
// End Spell Helpers
#endregion
#region Smite Spells
#region Touch Spells
private void OnSmiteSpell(SmiteSpellEvent ev)
{
if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer))
@@ -454,7 +405,8 @@ public abstract class SharedMagicSystem : EntitySystem
_body.GibBody(ev.Target, true, body);
}
// End Smite Spells
// End Touch Spells
#endregion
#region Knock Spells
/// <summary>
@@ -576,6 +528,33 @@ public abstract class SharedMagicSystem : EntitySystem
_stun.TryParalyze(ev.Performer, ev.PerformerStunDuration, true);
}
#endregion
#region Animation Spells
private void OnAnimateSpell(AnimateSpellEvent ev)
{
if (ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer) || !TryComp<FixturesComponent>(ev.Target, out var fixtures) ||
!TryComp<PhysicsComponent>(ev.Target, out var physics))
return;
ev.Handled = true;
//Speak(ev);
RemoveComponents(ev.Target, ev.RemoveComponents);
AddComponents(ev.Target, ev.AddComponents);
var xform = Transform(ev.Target);
var fixture = fixtures.Fixtures.First();
_transform.Unanchor(ev.Target);
_physics.SetCanCollide(ev.Target, true, true, false, fixtures, physics);
_physics.SetCollisionMask(ev.Target, fixture.Key, fixture.Value, (int)CollisionGroup.FlyingMobMask, fixtures, physics);
_physics.SetCollisionLayer(ev.Target, fixture.Key, fixture.Value, (int)CollisionGroup.FlyingMobLayer, fixtures, physics);
_physics.SetBodyType(ev.Target, BodyType.KinematicController, fixtures, physics, xform);
_physics.SetBodyStatus(ev.Target, physics, BodyStatus.InAir, true);
_physics.SetFixedRotation(ev.Target, false, true, fixtures, physics);
}
#endregion
// End Spells
#endregion

View File

@@ -120,10 +120,11 @@ namespace Content.Shared.Maps
private static bool GetWorldTileBox(TileRef turf, out Box2Rotated res)
{
var entManager = IoCManager.Resolve<IEntityManager>();
var xformSystem = entManager.System<SharedTransformSystem>();
if (entManager.TryGetComponent<MapGridComponent>(turf.GridUid, out var tileGrid))
{
var gridRot = entManager.GetComponent<TransformComponent>(turf.GridUid).WorldRotation;
var gridRot = xformSystem.GetWorldRotation(turf.GridUid);
// This is scaled to 90 % so it doesn't encompass walls on other tiles.
var tileBox = Box2.UnitCentered.Scale(0.9f);

View File

@@ -29,6 +29,7 @@ public abstract class SharedMaterialReclaimerSystem : EntitySystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] protected readonly SharedContainerSystem Container = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly EmagSystem _emag = default!;
public const string ActiveReclaimerContainerId = "active-material-reclaimer-container";
@@ -60,6 +61,12 @@ public abstract class SharedMaterialReclaimerSystem : EntitySystem
private void OnEmagged(EntityUid uid, MaterialReclaimerComponent component, ref GotEmaggedEvent args)
{
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (_emag.CheckFlag(uid, EmagType.Interaction))
return;
args.Handled = true;
}
@@ -207,7 +214,7 @@ public abstract class SharedMaterialReclaimerSystem : EntitySystem
component.Enabled &&
!component.Broken &&
HasComp<BodyComponent>(victim) &&
HasComp<EmaggedComponent>(uid);
_emag.CheckFlag(uid, EmagType.Interaction);
}
/// <summary>

View File

@@ -20,6 +20,7 @@ public abstract partial class SharedCryoPodSystem: EntitySystem
{
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly StandingStateSystem _standingStateSystem = default!;
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
@@ -156,9 +157,13 @@ public abstract partial class SharedCryoPodSystem: EntitySystem
protected void OnEmagged(EntityUid uid, CryoPodComponent? cryoPodComponent, ref GotEmaggedEvent args)
{
if (!Resolve(uid, ref cryoPodComponent))
{
return;
}
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
return;
if (cryoPodComponent.PermaLocked && cryoPodComponent.Locked)
return;
cryoPodComponent.PermaLocked = true;
cryoPodComponent.Locked = true;

View File

@@ -0,0 +1,8 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Mindshield.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class FakeMindShieldImplantComponent : Component
{
}

View File

@@ -0,0 +1,22 @@
using Content.Shared.StatusIcon;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Mindshield.Components;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class FakeMindShieldComponent : Component
{
/// <summary>
/// The state of the Fake mindshield, if true the owning entity will display a mindshield effect on their job icon
/// </summary>
[DataField, AutoNetworkedField]
public bool IsEnabled { get; set; } = false;
/// <summary>
/// The Security status icon displayed to the security officer. Should be a duplicate of the one the mindshield uses since it's spoofing that
/// </summary>
[DataField, AutoNetworkedField]
public ProtoId<SecurityIconPrototype> MindShieldStatusIcon = "MindShieldIcon";
}

View File

@@ -0,0 +1,44 @@
using Content.Shared.Actions;
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
using Content.Shared.Mindshield.Components;
using Robust.Shared.Containers;
namespace Content.Shared.Mindshield.FakeMindShield;
public sealed class SharedFakeMindShieldImplantSystem : EntitySystem
{
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SubdermalImplantComponent, FakeMindShieldToggleEvent>(OnFakeMindShieldToggle);
SubscribeLocalEvent<FakeMindShieldImplantComponent, ImplantImplantedEvent>(ImplantCheck);
SubscribeLocalEvent<FakeMindShieldImplantComponent, EntGotRemovedFromContainerMessage>(ImplantDraw);
}
/// <summary>
/// Raise the Action of a Implanted user toggling their implant to the FakeMindshieldComponent on their entity
/// </summary>
private void OnFakeMindShieldToggle(Entity<SubdermalImplantComponent> entity, ref FakeMindShieldToggleEvent ev)
{
ev.Handled = true;
if (entity.Comp.ImplantedEntity is not { } ent)
return;
if (!TryComp<FakeMindShieldComponent>(ent, out var comp))
return;
_actionsSystem.SetToggled(ev.Action, !comp.IsEnabled); // Set it to what the Mindshield component WILL be after this
RaiseLocalEvent(ent, ev); //this reraises the action event to support an eventual future Changeling Antag which will also be using this component for it's "mindshield" ability
}
private void ImplantCheck(EntityUid uid, FakeMindShieldImplantComponent component ,ref ImplantImplantedEvent ev)
{
if (ev.Implanted != null)
EnsureComp<FakeMindShieldComponent>(ev.Implanted.Value);
}
private void ImplantDraw(Entity<FakeMindShieldImplantComponent> ent, ref EntGotRemovedFromContainerMessage ev)
{
RemComp<FakeMindShieldComponent>(ev.Container.Owner);
}
}

Some files were not shown because too many files have changed in this diff Show More