Merge remote-tracking branch 'upstream/stable' into ed-08-09-2025-upstream-sync

# Conflicts:
#	.github/CODEOWNERS
#	Content.IntegrationTests/Tests/CargoTest.cs
#	Content.Server/Chat/Systems/ChatSystem.cs
#	Content.Shared/Chat/SharedChatSystem.cs
#	Content.Shared/Lock/LockSystem.cs
#	Content.Shared/StatusEffectNew/StatusEffectSystem.Relay.cs
#	Content.Shared/Storage/Components/EntityStorageComponent.cs
#	Resources/Prototypes/Entities/Mobs/Customization/Markings/human_hair.yml
#	Resources/Prototypes/game_presets.yml
This commit is contained in:
Ed
2025-09-08 13:12:50 +03:00
656 changed files with 86668 additions and 45617 deletions

View File

@@ -2,7 +2,7 @@
namespace Content.Shared.Administration;
[RegisterComponent, Access(typeof(SharedAdminFrozenSystem))]
[RegisterComponent, Access(typeof(AdminFrozenSystem))]
[NetworkedComponent, AutoGenerateComponentState]
public sealed partial class AdminFrozenComponent : Component
{

View File

@@ -12,15 +12,13 @@ using Content.Shared.Throwing;
namespace Content.Shared.Administration;
// TODO deduplicate with BlockMovementComponent
public abstract class SharedAdminFrozenSystem : EntitySystem
public sealed class AdminFrozenSystem : EntitySystem
{
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
[Dependency] private readonly PullingSystem _pulling = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AdminFrozenComponent, UseAttemptEvent>(OnAttempt);
SubscribeLocalEvent<AdminFrozenComponent, PickupAttemptEvent>(OnAttempt);
SubscribeLocalEvent<AdminFrozenComponent, ThrowAttemptEvent>(OnAttempt);
@@ -35,6 +33,16 @@ public abstract class SharedAdminFrozenSystem : EntitySystem
SubscribeLocalEvent<AdminFrozenComponent, SpeakAttemptEvent>(OnSpeakAttempt);
}
/// <summary>
/// Freezes and mutes the given entity.
/// </summary>
public void FreezeAndMute(EntityUid uid)
{
var comp = EnsureComp<AdminFrozenComponent>(uid);
comp.Muted = true;
Dirty(uid, comp);
}
private void OnInteractAttempt(Entity<AdminFrozenComponent> ent, ref InteractionAttemptEvent args)
{
args.Cancelled = true;

View File

@@ -0,0 +1,99 @@
using Content.Shared.Alert;
using Content.Shared.Damage;
using Robust.Shared.GameStates;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Prototypes;
namespace Content.Shared.Atmos.Components
{
[RegisterComponent, NetworkedComponent]
public sealed partial class FlammableComponent : Component
{
[DataField]
public bool Resisting;
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool OnFire;
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float FireStacks;
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float MaximumFireStacks = 10f;
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float MinimumFireStacks = -10f;
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public string FlammableFixtureID = "flammable";
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float MinIgnitionTemperature = 373.15f;
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool FireSpread { get; private set; } = false;
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool CanResistFire { get; private set; } = false;
[DataField(required: true)]
[ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier Damage = new(); // Empty by default, we don't want any funny NREs.
/// <summary>
/// Used for the fixture created to handle passing firestacks when two flammable objects collide.
/// </summary>
[DataField]
public IPhysShape FlammableCollisionShape = new PhysShapeCircle(0.35f);
/// <summary>
/// Should the component be set on fire by interactions with isHot entities
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool AlwaysCombustible = false;
/// <summary>
/// Can the component anyhow lose its FireStacks?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool CanExtinguish = true;
/// <summary>
/// How many firestacks should be applied to component when being set on fire?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public float FirestacksOnIgnite = 2.0f;
/// <summary>
/// Determines how quickly the object will fade out. With positive values, the object will flare up instead of going out.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float FirestackFade = -0.1f;
[DataField]
public ProtoId<AlertPrototype> FireAlert = "Fire";
/// <summary>
/// CrystallEdge fireplace fuel
/// </summary>
[DataField]
public float CP14FireplaceFuel = 10f;
/// <summary>
/// the value is cached to check if it has changed
/// </summary>
[DataField]
public bool OnFireOld = false;
}
}

View File

@@ -198,12 +198,6 @@ public sealed partial class BloodstreamComponent : Component
[ViewVariables]
public Entity<SolutionComponent>? TemporarySolution;
/// <summary>
/// Variable that stores the amount of status time added by having a low blood level.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan StatusTime;
/// <summary>
/// Alert to show when bleeding.
/// </summary>

View File

@@ -0,0 +1,9 @@
namespace Content.Shared.Body.Events;
/// <summary>
/// Raised on an entity before they bleed to modify the amount.
/// </summary>
/// <param name="BleedAmount">The amount of blood the entity will lose.</param>
/// <param name="BleedReductionAmount">The amount of bleed reduction that will happen.</param>
[ByRefEvent]
public record struct BleedModifierEvent(float BleedAmount, float BleedReductionAmount);

View File

@@ -6,7 +6,6 @@ using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage;
using Content.Shared.Drunk;
using Content.Shared.EntityEffects.Effects;
using Content.Shared.FixedPoint;
using Content.Shared.Fluids;
@@ -16,7 +15,7 @@ using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Random.Helpers;
using Content.Shared.Rejuvenate;
using Content.Shared.Speech.EntitySystems;
using Content.Shared.StatusEffectNew;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
@@ -27,17 +26,18 @@ namespace Content.Shared.Body.Systems;
public abstract class SharedBloodstreamSystem : EntitySystem
{
public static readonly EntProtoId Bloodloss = "StatusEffectBloodloss";
[Dependency] protected readonly SharedSolutionContainerSystem SolutionContainer = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedPuddleSystem _puddle = default!;
[Dependency] private readonly StatusEffectsSystem _status = default!;
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly SharedDrunkSystem _drunkSystem = default!;
[Dependency] private readonly SharedStutteringSystem _stutteringSystem = default!;
public override void Initialize()
{
@@ -81,10 +81,14 @@ public abstract class SharedBloodstreamSystem : EntitySystem
// as well as stop their bleeding to a certain extent.
if (bloodstream.BleedAmount > 0)
{
var ev = new BleedModifierEvent(bloodstream.BleedAmount, bloodstream.BleedReductionAmount);
RaiseLocalEvent(uid, ref ev);
// Blood is removed from the bloodstream at a 1-1 rate with the bleed amount
TryModifyBloodLevel((uid, bloodstream), -bloodstream.BleedAmount);
TryModifyBloodLevel((uid, bloodstream), -ev.BleedAmount);
// Bleed rate is reduced by the bleed reduction amount in the bloodstream component.
TryModifyBleedAmount((uid, bloodstream), -bloodstream.BleedReductionAmount);
TryModifyBleedAmount((uid, bloodstream), -ev.BleedReductionAmount);
}
// deal bloodloss damage if their blood level is below a threshold.
@@ -100,15 +104,7 @@ public abstract class SharedBloodstreamSystem : EntitySystem
// Apply dizziness as a symptom of bloodloss.
// The effect is applied in a way that it will never be cleared without being healthy.
// Multiplying by 2 is arbitrary but works for this case, it just prevents the time from running out
_drunkSystem.TryApplyDrunkenness(
uid,
(float)bloodstream.AdjustedUpdateInterval.TotalSeconds * 2,
applySlur: false);
_stutteringSystem.DoStutter(uid, bloodstream.AdjustedUpdateInterval * 2, refresh: false);
// storing the drunk and stutter time so we can remove it independently from other effects additions
bloodstream.StatusTime += bloodstream.AdjustedUpdateInterval * 2;
DirtyField(uid, bloodstream, nameof(BloodstreamComponent.StatusTime));
_status.TrySetStatusEffectDuration(uid, Bloodloss);
}
else if (!_mobStateSystem.IsDead(uid))
{
@@ -118,12 +114,7 @@ public abstract class SharedBloodstreamSystem : EntitySystem
bloodstream.BloodlossHealDamage * bloodPercentage,
ignoreResistances: true, interruptsDoAfters: false);
// Remove the drunk effect when healthy. Should only remove the amount of drunk and stutter added by low blood level
_drunkSystem.TryRemoveDrunkenessTime(uid, bloodstream.StatusTime.TotalSeconds);
_stutteringSystem.DoRemoveStutterTime(uid, bloodstream.StatusTime.TotalSeconds);
// Reset the drunk and stutter time to zero
bloodstream.StatusTime = TimeSpan.Zero;
DirtyField(uid, bloodstream, nameof(BloodstreamComponent.StatusTime));
_status.TryRemoveStatusEffect(uid, Bloodloss);
}
}
}

View File

@@ -1,7 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Content.Shared.Alert;
using Content.Shared.ActionBlocker;
using Content.Shared.Buckle.Components;
using Content.Shared.Cuffs.Components;
using Content.Shared.Database;
@@ -13,7 +12,6 @@ using Content.Shared.Movement.Pulling.Events;
using Content.Shared.Physics;
using Content.Shared.Popups;
using Content.Shared.Pulling.Events;
using Content.Shared.Rotation;
using Content.Shared.Standing;
using Content.Shared.Storage.Components;
using Content.Shared.Stunnable;
@@ -453,9 +451,9 @@ public abstract partial class SharedBuckleSystem
private void Unbuckle(Entity<BuckleComponent> buckle, Entity<StrapComponent> strap, EntityUid? user)
{
if (user == buckle.Owner)
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{user} unbuckled themselves from {strap}");
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):user} unbuckled themselves from {ToPrettyString(strap):strap}");
else if (user != null)
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{user} unbuckled {buckle} from {strap}");
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):user} unbuckled {ToPrettyString(buckle):target} from {ToPrettyString(strap):strap}");
_audio.PlayPredicted(strap.Comp.UnbuckleSound, strap, user);

View File

@@ -56,4 +56,12 @@ public sealed partial class CCVars
[CVarControl(AdminFlags.VarEdit)]
public static readonly CVarDef<float> MovementPushMassCap =
CVarDef.Create("movement.push_mass_cap", 1.75f, CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// Is crawling enabled
/// </summary>
[CVarControl(AdminFlags.VarEdit)]
public static readonly CVarDef<bool> MovementCrawling =
CVarDef.Create("movement.crawling", true, CVar.SERVER | CVar.REPLICATED);
}

View File

@@ -2,7 +2,7 @@
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;
namespace Content.Shared.Changeling.Devour;
namespace Content.Shared.Changeling;
/// <summary>
/// Action event for Devour, someone has initiated a devour on someone, begin to windup.

View File

@@ -2,7 +2,7 @@
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;
namespace Content.Shared.Changeling.Transform;
namespace Content.Shared.Changeling;
/// <summary>
/// Action event for opening the changeling transformation radial menu.

View File

@@ -1,3 +1,4 @@
using Content.Shared.Changeling.Systems;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
@@ -7,7 +8,7 @@ using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Changeling.Devour;
namespace Content.Shared.Changeling.Components;
/// <summary>
/// Component responsible for Changelings Devour attack. Including the amount of damage

View File

@@ -2,13 +2,13 @@ using Content.Shared.Cloning;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Changeling;
namespace Content.Shared.Changeling.Components;
/// <summary>
/// The storage component for Changelings, it handles the link between a changeling and its consumed identities
/// that exist on a paused map.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true)]
public sealed partial class ChangelingIdentityComponent : Component
{
/// <summary>
@@ -29,6 +29,7 @@ public sealed partial class ChangelingIdentityComponent : Component
/// The cloning settings passed to the CloningSystem, contains a list of all components to copy or have handled by their
/// respective systems.
/// </summary>
[DataField]
public ProtoId<CloningSettingsPrototype> IdentityCloningSettings = "ChangelingCloningSettings";
public override bool SendOnlyToOwner => true;

View File

@@ -1,7 +1,7 @@
using Robust.Shared.GameStates;
using Robust.Shared.Player;
namespace Content.Shared.Changeling;
namespace Content.Shared.Changeling.Components;
/// <summary>
/// Marker component for cloned identities devoured by a changeling.

View File

@@ -1,9 +1,10 @@
using Content.Shared.Changeling.Systems;
using Content.Shared.Cloning;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Changeling.Transform;
namespace Content.Shared.Changeling.Components;
/// <summary>
/// The component containing information about Changelings Transformation action

View File

@@ -3,6 +3,7 @@ using Content.Shared.Administration.Logs;
using Content.Shared.Armor;
using Content.Shared.Atmos.Rotting;
using Content.Shared.Body.Components;
using Content.Shared.Changeling.Components;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.DoAfter;
@@ -19,7 +20,7 @@ using Robust.Shared.Network;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Shared.Changeling.Devour;
namespace Content.Shared.Changeling.Systems;
public sealed class ChangelingDevourSystem : EntitySystem
{
@@ -31,7 +32,7 @@ public sealed class ChangelingDevourSystem : EntitySystem
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly ChangelingIdentitySystem _changelingIdentitySystem = default!;
[Dependency] private readonly SharedChangelingIdentitySystem _changelingIdentitySystem = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;

View File

@@ -0,0 +1,21 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Changeling.Systems;
/// <summary>
/// Send when a player selects an intentity to transform into in the radial menu.
/// </summary>
[Serializable, NetSerializable]
public sealed class ChangelingTransformIdentitySelectMessage(NetEntity targetIdentity) : BoundUserInterfaceMessage
{
/// <summary>
/// The uid of the cloned identity.
/// </summary>
public readonly NetEntity TargetIdentity = targetIdentity;
}
[Serializable, NetSerializable]
public enum ChangelingTransformUiKey : byte
{
Key,
}

View File

@@ -1,5 +1,6 @@
using Content.Shared.Actions;
using Content.Shared.Administration.Logs;
using Content.Shared.Changeling.Components;
using Content.Shared.Cloning;
using Content.Shared.Database;
using Content.Shared.DoAfter;
@@ -10,7 +11,7 @@ using Robust.Shared.Audio.Systems;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
namespace Content.Shared.Changeling.Transform;
namespace Content.Shared.Changeling.Systems;
public sealed partial class ChangelingTransformSystem : EntitySystem
{
@@ -43,7 +44,7 @@ public sealed partial class ChangelingTransformSystem : EntitySystem
_actionsSystem.AddAction(ent, ref ent.Comp.ChangelingTransformActionEntity, ent.Comp.ChangelingTransformAction);
var userInterfaceComp = EnsureComp<UserInterfaceComponent>(ent);
_uiSystem.SetUi((ent, userInterfaceComp), TransformUI.Key, new InterfaceData(ChangelingBuiXmlGeneratedName));
_uiSystem.SetUi((ent, userInterfaceComp), ChangelingTransformUiKey.Key, new InterfaceData(ChangelingBuiXmlGeneratedName));
}
private void OnShutdown(Entity<ChangelingTransformComponent> ent, ref ComponentShutdown args)
@@ -63,18 +64,9 @@ public sealed partial class ChangelingTransformSystem : EntitySystem
if (!TryComp<ChangelingIdentityComponent>(ent, out var userIdentity))
return;
if (!_uiSystem.IsUiOpen((ent, userInterfaceComp), TransformUI.Key, args.Performer))
if (!_uiSystem.IsUiOpen((ent, userInterfaceComp), ChangelingTransformUiKey.Key, args.Performer))
{
_uiSystem.OpenUi((ent, userInterfaceComp), TransformUI.Key, args.Performer);
var identityData = new List<NetEntity>();
foreach (var consumedIdentity in userIdentity.ConsumedIdentities)
{
identityData.Add(GetNetEntity(consumedIdentity));
}
_uiSystem.SetUiState((ent, userInterfaceComp), TransformUI.Key, new ChangelingTransformBoundUserInterfaceState(identityData));
_uiSystem.OpenUi((ent, userInterfaceComp), ChangelingTransformUiKey.Key, args.Performer);
} //TODO: Can add a Else here with TransformInto and CloseUI to make a quick switch,
// issue right now is that Radials cover the Action buttons so clicking the action closes the UI (due to clicking off a radial causing it to close, even with UI)
// but pressing the number does.
@@ -102,12 +94,12 @@ public sealed partial class ChangelingTransformSystem : EntitySystem
if (_net.IsServer)
ent.Comp.CurrentTransformSound = _audio.PlayPvs(ent.Comp.TransformAttemptNoise, ent)?.Entity;
if(TryComp<ChangelingStoredIdentityComponent>(targetIdentity, out var storedIdentity) && storedIdentity.OriginalSession != null)
if (TryComp<ChangelingStoredIdentityComponent>(targetIdentity, out var storedIdentity) && storedIdentity.OriginalSession != null)
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} begun an attempt to transform into \"{Name(targetIdentity)}\" ({storedIdentity.OriginalSession:player}) ");
else
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} begun an attempt to transform into \"{Name(targetIdentity)}\"");
var result = _doAfterSystem.TryStartDoAfter(new DoAfterArgs(
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(
EntityManager,
ent,
ent.Comp.TransformWindup,
@@ -126,7 +118,7 @@ public sealed partial class ChangelingTransformSystem : EntitySystem
private void OnTransformSelected(Entity<ChangelingTransformComponent> ent,
ref ChangelingTransformIdentitySelectMessage args)
{
_uiSystem.CloseUi(ent.Owner, TransformUI.Key, ent);
_uiSystem.CloseUi(ent.Owner, ChangelingTransformUiKey.Key, ent);
if (!TryGetEntity(args.TargetIdentity, out var targetIdentity))
return;
@@ -162,8 +154,8 @@ public sealed partial class ChangelingTransformSystem : EntitySystem
_humanoidAppearanceSystem.CloneAppearance(targetIdentity, args.User);
_cloningSystem.CloneComponents(targetIdentity, args.User, settings);
if(TryComp<ChangelingStoredIdentityComponent>(targetIdentity, out var storedIdentity) && storedIdentity.OriginalSession != null)
if (TryComp<ChangelingStoredIdentityComponent>(targetIdentity, out var storedIdentity) && storedIdentity.OriginalSession != null)
_adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(ent.Owner):player} successfully transformed into \"{Name(targetIdentity)}\" ({storedIdentity.OriginalSession:player})");
else
_adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(ent.Owner):player} successfully transformed into \"{Name(targetIdentity)}\"");

View File

@@ -1,7 +1,7 @@
using System.Numerics;
using Content.Shared.Changeling.Components;
using Content.Shared.Cloning;
using Content.Shared.Humanoid;
using Content.Shared.Mind.Components;
using Content.Shared.NameModifier.EntitySystems;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
@@ -9,9 +9,9 @@ using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Shared.Changeling;
namespace Content.Shared.Changeling.Systems;
public sealed class ChangelingIdentitySystem : EntitySystem
public abstract class SharedChangelingIdentitySystem : EntitySystem
{
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
@@ -31,22 +31,19 @@ public sealed class ChangelingIdentitySystem : EntitySystem
SubscribeLocalEvent<ChangelingIdentityComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ChangelingIdentityComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<ChangelingIdentityComponent, MindAddedMessage>(OnMindAdded);
SubscribeLocalEvent<ChangelingIdentityComponent, MindRemovedMessage>(OnMindRemoved);
SubscribeLocalEvent<ChangelingIdentityComponent, PlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<ChangelingIdentityComponent, PlayerDetachedEvent>(OnPlayerDetached);
SubscribeLocalEvent<ChangelingStoredIdentityComponent, ComponentRemove>(OnStoredRemove);
}
private void OnMindAdded(Entity<ChangelingIdentityComponent> ent, ref MindAddedMessage args)
private void OnPlayerAttached(Entity<ChangelingIdentityComponent> ent, ref PlayerAttachedEvent args)
{
if (!TryComp<ActorComponent>(args.Container.Owner, out var actor))
return;
HandOverPvsOverride(actor.PlayerSession, ent.Comp);
HandOverPvsOverride(ent, args.Player);
}
private void OnMindRemoved(Entity<ChangelingIdentityComponent> ent, ref MindRemovedMessage args)
private void OnPlayerDetached(Entity<ChangelingIdentityComponent> ent, ref PlayerDetachedEvent args)
{
CleanupPvsOverride(ent, args.Container.Owner);
CleanupPvsOverride(ent, args.Player);
}
private void OnMapInit(Entity<ChangelingIdentityComponent> ent, ref MapInitEvent args)
@@ -58,7 +55,8 @@ public sealed class ChangelingIdentitySystem : EntitySystem
private void OnShutdown(Entity<ChangelingIdentityComponent> ent, ref ComponentShutdown args)
{
CleanupPvsOverride(ent, ent.Owner);
if (TryComp<ActorComponent>(ent, out var actor))
CleanupPvsOverride(ent, actor.PlayerSession);
CleanupChangelingNullspaceIdentities(ent);
}
@@ -106,66 +104,63 @@ public sealed class ChangelingIdentitySystem : EntitySystem
// Movercontrollers and mob collisions are currently being calculated even for paused entities.
// Spawning all of them in the same spot causes severe performance problems.
// Cryopods and Polymorph have the same problem.
var mob = Spawn(speciesPrototype.Prototype, new MapCoordinates(new Vector2(2 * _numberOfStoredIdentities++, 0), PausedMapId!.Value));
var clone = Spawn(speciesPrototype.Prototype, new MapCoordinates(new Vector2(2 * _numberOfStoredIdentities++, 0), PausedMapId!.Value));
var storedIdentity = EnsureComp<ChangelingStoredIdentityComponent>(mob);
var storedIdentity = EnsureComp<ChangelingStoredIdentityComponent>(clone);
storedIdentity.OriginalEntity = target; // TODO: network this once we have WeakEntityReference or the autonetworking source gen is fixed
if (TryComp<ActorComponent>(target, out var actor))
storedIdentity.OriginalSession = actor.PlayerSession;
_humanoidSystem.CloneAppearance(target, mob);
_cloningSystem.CloneComponents(target, mob, settings);
_humanoidSystem.CloneAppearance(target, clone);
_cloningSystem.CloneComponents(target, clone, settings);
var targetName = _nameMod.GetBaseName(target);
_metaSystem.SetEntityName(mob, targetName);
ent.Comp.ConsumedIdentities.Add(mob);
_metaSystem.SetEntityName(clone, targetName);
ent.Comp.ConsumedIdentities.Add(clone);
Dirty(ent);
HandlePvsOverride(ent, mob);
HandlePvsOverride(ent, clone);
return mob;
return clone;
}
/// <summary>
/// Simple helper to add a PVS override to a Nullspace Identity
/// Simple helper to add a PVS override to a nullspace identity.
/// </summary>
/// <param name="uid"></param>
/// <param name="target"></param>
private void HandlePvsOverride(EntityUid uid, EntityUid target)
/// <param name="uid">The actor that should get the override.</param>
/// <param name="identity">The identity stored in nullspace.</param>
private void HandlePvsOverride(EntityUid uid, EntityUid identity)
{
if (!TryComp<ActorComponent>(uid, out var actor))
return;
_pvsOverrideSystem.AddSessionOverride(target, actor.PlayerSession);
_pvsOverrideSystem.AddSessionOverride(identity, actor.PlayerSession);
}
/// <summary>
/// Cleanup all Pvs Overrides for the owner of the ChangelingIdentity
/// Cleanup all PVS overrides for the owner of the ChangelingIdentity
/// </summary>
/// <param name="ent">the Changeling itself</param>
/// <param name="entityUid">Who specifically to cleanup from, usually just the same owner, but in the case of a mindswap we want to clean up the victim</param>
private void CleanupPvsOverride(Entity<ChangelingIdentityComponent> ent, EntityUid entityUid)
/// <param name="ent">The changeling storing the identities.</param>
/// <param name="entityUid"The session you wish to remove the overrides from.</param>
private void CleanupPvsOverride(Entity<ChangelingIdentityComponent> ent, ICommonSession session)
{
if (!TryComp<ActorComponent>(entityUid, out var actor))
return;
foreach (var identity in ent.Comp.ConsumedIdentities)
{
_pvsOverrideSystem.RemoveSessionOverride(identity, actor.PlayerSession);
_pvsOverrideSystem.RemoveSessionOverride(identity, session);
}
}
/// <summary>
/// Inform another Session of the entities stored for Transformation
/// Inform another session of the entities stored for transformation.
/// </summary>
/// <param name="session">The Session you wish to inform</param>
/// <param name="comp">The Target storage of identities</param>
public void HandOverPvsOverride(ICommonSession session, ChangelingIdentityComponent comp)
/// <param name="ent">The changeling storing the identities.</param>
/// <param name="session">The session you wish to inform.</param>
public void HandOverPvsOverride(Entity<ChangelingIdentityComponent> ent, ICommonSession session)
{
foreach (var entity in comp.ConsumedIdentities)
foreach (var identity in ent.Comp.ConsumedIdentities)
{
_pvsOverrideSystem.AddSessionOverride(entity, session);
_pvsOverrideSystem.AddSessionOverride(identity, session);
}
}

View File

@@ -1,33 +0,0 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Changeling.Transform;
/// <summary>
/// Send when a player selects an intentity to transform into in the radial menu.
/// </summary>
[Serializable, NetSerializable]
public sealed class ChangelingTransformIdentitySelectMessage(NetEntity targetIdentity) : BoundUserInterfaceMessage
{
/// <summary>
/// The uid of the cloned identity.
/// </summary>
public readonly NetEntity TargetIdentity = targetIdentity;
}
// TODO: Replace with component states.
// We are already networking the ChangelingIdentityComponent, which contains all this information,
// so we can just read it from them from the component and update the UI in an AfterAuotHandleState subscription.
[Serializable, NetSerializable]
public sealed class ChangelingTransformBoundUserInterfaceState(List<NetEntity> identities) : BoundUserInterfaceState
{
/// <summary>
/// The uids of the cloned identities.
/// </summary>
public readonly List<NetEntity> Identites = identities;
}
[Serializable, NetSerializable]
public enum TransformUI : byte
{
Key,
}

View File

@@ -16,7 +16,7 @@ public sealed partial class LimitedChargesComponent : Component
/// <summary>
/// The max charges this action has.
/// </summary>
[DataField, AutoNetworkedField, Access(Other = AccessPermissions.Read)]
[DataField, AutoNetworkedField]
public int MaxCharges = 3;
/// <summary>

View File

@@ -94,6 +94,12 @@ public abstract class SharedChargesSystem : EntitySystem
/// <summary>
/// Adds the specified charges. Does not reset the accumulator.
/// </summary>
/// <param name="action">
/// The action to add charges to. If it doesn't have <see cref="LimitedChargesComponent"/>, it will be added.
/// </param>
/// <param name="addCharges">
/// The number of charges to add. Can be negative. Resulting charge count is clamped to [0, MaxCharges].
/// </param>
public void AddCharges(Entity<LimitedChargesComponent?, AutoRechargeComponent?> action, int addCharges)
{
if (addCharges == 0)
@@ -170,9 +176,21 @@ public abstract class SharedChargesSystem : EntitySystem
Dirty(action);
}
/// <summary>
/// Set the number of charges an action has.
/// </summary>
/// <param name="action">The action in question</param>
/// <param name="value">
/// The number of charges. Clamped to [0, MaxCharges].
/// </param>
/// <remarks>
/// This method doesn't implicitly add <see cref="LimitedChargesComponent"/>
/// unlike some other methods in this system.
/// </remarks>
public void SetCharges(Entity<LimitedChargesComponent?> action, int value)
{
action.Comp ??= EnsureComp<LimitedChargesComponent>(action.Owner);
if (!Resolve(action, ref action.Comp))
return;
var adjusted = Math.Clamp(value, 0, action.Comp.MaxCharges);
@@ -186,6 +204,31 @@ public abstract class SharedChargesSystem : EntitySystem
Dirty(action);
}
/// <summary>
/// Sets the maximum charges of a given action.
/// </summary>
/// <param name="action">The action being modified.</param>
/// <param name="value">The new maximum charges of the action. Clamped to zero.</param>
/// <remarks>
/// Does not change the current charge count, or adjust the
/// accumulator for auto-recharge. It also doesn't implicitly add
/// <see cref="LimitedChargesComponent"/> unlike some other methods
/// in this system.
/// </remarks>
public void SetMaxCharges(Entity<LimitedChargesComponent?> action, int value)
{
if (!Resolve(action, ref action.Comp))
return;
// You can't have negative max charges (even zero is a bit goofy but eh)
var adjusted = Math.Max(0, value);
if (action.Comp.MaxCharges == adjusted)
return;
action.Comp.MaxCharges = adjusted;
Dirty(action);
}
/// <summary>
/// The next time a charge will be considered to be filled.
/// </summary>

View File

@@ -3,6 +3,7 @@ using System.Text.RegularExpressions;
using Content.Shared.Popups;
using Content.Shared.Radio;
using Content.Shared.Speech;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -27,7 +28,8 @@ public abstract class SharedChatSystem : EntitySystem
public const int VoiceRange = 10; // how far voice goes in world units
public const int WhisperClearRange = 2; // how far whisper goes while still being understandable, in world units
public const int WhisperMuffledRange = 5; // how far whisper goes at all, in world units
public const string DefaultAnnouncementSound = "/Audio/_CP14/Announce/event_boom.ogg"; //CP14 replaced default announce sound
public static readonly SoundSpecifier DefaultAnnouncementSound
= new SoundPathSpecifier("/Audio/_CP14/Announce/event_boom.ogg"); //CP14 replaced default announce sound
public static readonly ProtoId<RadioChannelPrototype> CommonChannel = "Common";

View File

@@ -144,7 +144,7 @@ public sealed class HypospraySystem : EntitySystem
return false;
}
_popup.PopupClient(Loc.GetString(msgFormat ?? "hypospray-component-inject-other-message", ("other", target)), target, user);
_popup.PopupClient(Loc.GetString(msgFormat ?? "hypospray-component-inject-other-message", ("other", Identity.Entity(target, EntityManager))), target, user);
if (target != user)
{

View File

@@ -63,6 +63,12 @@ namespace Content.Shared.Chemistry.Reagent
[DataField]
public bool Recognizable;
/// <summary>
/// Whether this reagent stands out (blood, slime).
/// </summary>
[DataField]
public bool Standsout;
[DataField]
public ProtoId<FlavorPrototype>? Flavor;

View File

@@ -15,6 +15,13 @@ public sealed partial class ClothingSpeedModifierComponent : Component
[DataField]
public float SprintModifier = 1.0f;
/// <summary>
/// An optional required standing state.
/// Set to true if you need to be standing, false if you need to not be standing, null if you don't care.
/// </summary>
[DataField]
public bool? Standing;
}
[Serializable, NetSerializable]

View File

@@ -3,6 +3,7 @@ using Content.Shared.Inventory;
using Content.Shared.Item.ItemToggle;
using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.Standing;
using Content.Shared.Verbs;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
@@ -12,10 +13,11 @@ namespace Content.Shared.Clothing;
public sealed class ClothingSpeedModifierSystem : EntitySystem
{
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly ExamineSystemShared _examine = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!;
[Dependency] private readonly ItemToggleSystem _toggle = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly StandingStateSystem _standing = default!;
public override void Initialize()
{
@@ -54,8 +56,13 @@ public sealed class ClothingSpeedModifierSystem : EntitySystem
private void OnRefreshMoveSpeed(EntityUid uid, ClothingSpeedModifierComponent component, InventoryRelayedEvent<RefreshMovementSpeedModifiersEvent> args)
{
if (_toggle.IsActivated(uid))
args.Args.ModifySpeed(component.WalkModifier, component.SprintModifier);
if (!_toggle.IsActivated(uid))
return;
if (component.Standing != null && !_standing.IsMatchingState(args.Owner, component.Standing.Value))
return;
args.Args.ModifySpeed(component.WalkModifier, component.SprintModifier);
}
private void OnClothingVerbExamine(EntityUid uid, ClothingSpeedModifierComponent component, GetVerbsEvent<ExamineVerb> args)

View File

@@ -1,23 +1,64 @@
using Content.Shared.Clothing.Components;
using Content.Shared.Gravity;
using Content.Shared.Inventory;
using Content.Shared.Standing;
namespace Content.Shared.Clothing.EntitySystems;
/// <remarks>
/// We check standing state on all clothing because we don't want you to have anti-gravity unless you're standing.
/// This is for balance reasons as it prevents you from wearing anti-grav clothing to cheese being stun cuffed, as
/// well as other worse things.
/// </remarks>
public sealed class AntiGravityClothingSystem : EntitySystem
{
[Dependency] private readonly StandingStateSystem _standing = default!;
[Dependency] private readonly SharedGravitySystem _gravity = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<AntiGravityClothingComponent, InventoryRelayedEvent<IsWeightlessEvent>>(OnIsWeightless);
SubscribeLocalEvent<AntiGravityClothingComponent, ClothingGotEquippedEvent>(OnEquipped);
SubscribeLocalEvent<AntiGravityClothingComponent, ClothingGotUnequippedEvent>(OnUnequipped);
SubscribeLocalEvent<AntiGravityClothingComponent, InventoryRelayedEvent<DownedEvent>>(OnDowned);
SubscribeLocalEvent<AntiGravityClothingComponent, InventoryRelayedEvent<StoodEvent>>(OnStood);
}
private void OnIsWeightless(Entity<AntiGravityClothingComponent> ent, ref InventoryRelayedEvent<IsWeightlessEvent> args)
{
if (args.Args.Handled)
if (args.Args.Handled || _standing.IsDown(args.Owner))
return;
args.Args.Handled = true;
args.Args.IsWeightless = true;
}
private void OnEquipped(Entity<AntiGravityClothingComponent> entity, ref ClothingGotEquippedEvent args)
{
// This clothing item does nothing if we're not standing
if (_standing.IsDown(args.Wearer))
return;
_gravity.RefreshWeightless(args.Wearer, true);
}
private void OnUnequipped(Entity<AntiGravityClothingComponent> entity, ref ClothingGotUnequippedEvent args)
{
// This clothing item does nothing if we're not standing
if (_standing.IsDown(args.Wearer))
return;
_gravity.RefreshWeightless(args.Wearer, false);
}
private void OnDowned(Entity<AntiGravityClothingComponent> entity, ref InventoryRelayedEvent<DownedEvent> args)
{
_gravity.RefreshWeightless(args.Owner, false);
}
private void OnStood(Entity<AntiGravityClothingComponent> entity, ref InventoryRelayedEvent<StoodEvent> args)
{
_gravity.RefreshWeightless(args.Owner, true);
}
}

View File

@@ -213,7 +213,7 @@ public sealed class ToggleableClothingSystem : EntitySystem
if (!TryComp(component.AttachedUid, out ToggleableClothingComponent? toggleComp))
return;
if (toggleComp.LifeStage > ComponentLifeStage.Running)
if (LifeStage(component.AttachedUid) > EntityLifeStage.MapInitialized)
return;
// As unequipped gets called in the middle of container removal, we cannot call a container-insert without causing issues.

View File

@@ -150,7 +150,7 @@ public sealed class LoadoutSystem : EntitySystem
{
// First, randomly pick a startingGear profile from those specified, and equip it.
if (startingGear != null && startingGear.Count > 0)
_station.EquipStartingGear(uid, _random.Pick(startingGear));
_station.EquipStartingGear(uid, _random.Pick(startingGear), false);
if (loadoutGroups == null)
{

View File

@@ -1,4 +1,5 @@
using Content.Shared.Alert;
using Content.Shared.Inventory;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
@@ -16,10 +17,4 @@ public sealed partial class MagbootsComponent : Component
/// </summary>
[DataField]
public bool RequiresGrid = true;
/// <summary>
/// Slot the clothing has to be worn in to work.
/// </summary>
[DataField]
public string Slot = "shoes";
}

View File

@@ -32,14 +32,8 @@ public sealed class SharedMagbootsSystem : EntitySystem
private void OnToggled(Entity<MagbootsComponent> ent, ref ItemToggledEvent args)
{
var (uid, comp) = ent;
// only stick to the floor if being worn in the correct slot
if (_container.TryGetContainingContainer((uid, null, null), out var container) &&
_inventory.TryGetSlotEntity(container.Owner, comp.Slot, out var worn)
&& uid == worn)
{
if (_container.TryGetContainingContainer((ent.Owner, null, null), out var container))
UpdateMagbootEffects(container.Owner, ent, args.Activated);
}
}
private void OnGotUnequipped(Entity<MagbootsComponent> ent, ref ClothingGotUnequippedEvent args)
@@ -58,6 +52,8 @@ public sealed class SharedMagbootsSystem : EntitySystem
if (TryComp<MovedByPressureComponent>(user, out var moved))
moved.Enabled = !state;
_gravity.RefreshWeightless(user);
if (state)
_alerts.ShowAlert(user, ent.Comp.MagbootsAlert);
else

View File

@@ -2,7 +2,6 @@ using Content.Shared.Construction.EntitySystems;
using Content.Shared.Tools;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Construction.Components
{

View File

@@ -102,6 +102,13 @@ public sealed partial class AnchorableSystem : EntitySystem
private void OnAnchoredExamine(EntityUid uid, AnchorableComponent component, ExaminedEvent args)
{
var isAnchored = Comp<TransformComponent>(uid).Anchored;
if (isAnchored && (component.Flags & AnchorableFlags.Unanchorable) == 0x0)
return;
if (!isAnchored && (component.Flags & AnchorableFlags.Anchorable) == 0x0)
return;
var messageId = isAnchored ? "examinable-anchored" : "examinable-unanchored";
args.PushMarkup(Loc.GetString(messageId, ("target", uid)));
}

View File

@@ -164,12 +164,11 @@ public abstract partial class SharedDoAfterSystem : EntitySystem
if (args.Target is { } target && !xformQuery.TryGetComponent(target, out targetXform))
return true;
TransformComponent? usedXform = null;
if (args.Used is { } @using && !xformQuery.TryGetComponent(@using, out usedXform))
if (args.Used is { } @using && !xformQuery.HasComp(@using))
return true;
// TODO: Re-use existing xform query for these calculations.
if (args.BreakOnMove && !(!args.BreakOnWeightlessMove && _gravity.IsWeightless(args.User, xform: userXform)))
if (args.BreakOnMove && !(!args.BreakOnWeightlessMove && _gravity.IsWeightless(args.User)))
{
// Whether the user has moved too much from their original position.
if (!_transform.InRange(userXform.Coordinates, doAfter.UserPosition, args.MovementThreshold))

View File

@@ -1,6 +0,0 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Drunk;
[RegisterComponent, NetworkedComponent]
public sealed partial class DrunkComponent : Component { }

View File

@@ -0,0 +1,11 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Drunk;
/// <summary>
/// This is used by a status effect entity to apply the <see cref="DrunkComponent"/> to an entity.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class DrunkStatusEffectComponent : Component
{
}

View File

@@ -1,48 +0,0 @@
using Content.Shared.Speech.EntitySystems;
using Content.Shared.StatusEffect;
using Content.Shared.Traits.Assorted;
using Robust.Shared.Prototypes;
namespace Content.Shared.Drunk;
public abstract class SharedDrunkSystem : EntitySystem
{
public static readonly ProtoId<StatusEffectPrototype> DrunkKey = "Drunk";
[Dependency] private readonly StatusEffectsSystem _statusEffectsSystem = default!;
[Dependency] private readonly SharedSlurredSystem _slurredSystem = default!;
public void TryApplyDrunkenness(EntityUid uid, float boozePower, bool applySlur = true,
StatusEffectsComponent? status = null)
{
if (!Resolve(uid, ref status, false))
return;
if (TryComp<LightweightDrunkComponent>(uid, out var trait))
boozePower *= trait.BoozeStrengthMultiplier;
if (applySlur)
{
_slurredSystem.DoSlur(uid, TimeSpan.FromSeconds(boozePower), status);
}
if (!_statusEffectsSystem.HasStatusEffect(uid, DrunkKey, status))
{
_statusEffectsSystem.TryAddStatusEffect<DrunkComponent>(uid, DrunkKey, TimeSpan.FromSeconds(boozePower), true, status);
}
else
{
_statusEffectsSystem.TryAddTime(uid, DrunkKey, TimeSpan.FromSeconds(boozePower), status);
}
}
public void TryRemoveDrunkenness(EntityUid uid)
{
_statusEffectsSystem.TryRemoveStatusEffect(uid, DrunkKey);
}
public void TryRemoveDrunkenessTime(EntityUid uid, double timeRemoved)
{
_statusEffectsSystem.TryRemoveTime(uid, DrunkKey, TimeSpan.FromSeconds(timeRemoved));
}
}

View File

@@ -0,0 +1,50 @@
using Content.Shared.Speech.EntitySystems;
using Content.Shared.StatusEffectNew;
using Content.Shared.Traits.Assorted;
using Robust.Shared.Prototypes;
namespace Content.Shared.Drunk;
public abstract class SharedDrunkSystem : EntitySystem
{
public static EntProtoId Drunk = "StatusEffectDrunk";
public static EntProtoId Woozy = "StatusEffectWoozy";
/* I have no clue why this magic number was chosen, I copied it from slur system and needed it for the overlay
If you have a more intelligent magic number be my guest to completely explode this value.
There were no comments as to why this value was chosen three years ago. */
public static float MagicNumber = 1100f;
[Dependency] protected readonly StatusEffectsSystem Status = default!;
public override void Initialize()
{
SubscribeLocalEvent<LightweightDrunkComponent, DrunkEvent>(OnLightweightDrinking);
}
public void TryApplyDrunkenness(EntityUid uid, TimeSpan boozePower)
{
var ev = new DrunkEvent(boozePower);
RaiseLocalEvent(uid, ref ev);
Status.TryAddStatusEffectDuration(uid, Drunk, ev.Duration);
}
public void TryRemoveDrunkenness(EntityUid uid)
{
Status.TryRemoveStatusEffect(uid, Drunk);
}
public void TryRemoveDrunkennessTime(EntityUid uid, TimeSpan boozePower)
{
Status.TryAddTime(uid, Drunk, - boozePower);
}
private void OnLightweightDrinking(Entity<LightweightDrunkComponent> entity, ref DrunkEvent args)
{
args.Duration *= entity.Comp.BoozeStrengthMultiplier;
}
[ByRefEvent]
public record struct DrunkEvent(TimeSpan Duration);
}

View File

@@ -0,0 +1,14 @@
using Content.Shared.Engineering.Systems;
using Content.Shared.Weapons.Melee.Balloon;
namespace Content.Shared.Engineering.Components;
/// <summary>
/// Implements logic to allow inflatable objects to be safely deflated by <see cref="BalloonPopperComponent"/> items.
/// </summary>
/// <remarks>
/// The owning entity must have <see cref="DisassembleOnAltVerbComponent"/> to implement the logic.
/// </remarks>
/// <seealso cref="InflatableSafeDisassemblySystem"/>
[RegisterComponent]
public sealed partial class InflatableSafeDisassemblyComponent : Component;

View File

@@ -19,14 +19,12 @@ public sealed partial class DisassembleOnAltVerbSystem : EntitySystem
SubscribeLocalEvent<DisassembleOnAltVerbComponent, GetVerbsEvent<AlternativeVerb>>(AddDisassembleVerb);
SubscribeLocalEvent<DisassembleOnAltVerbComponent, DisassembleDoAfterEvent>(OnDisassembleDoAfter);
}
private void AddDisassembleVerb(Entity<DisassembleOnAltVerbComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanInteract || !args.CanAccess || args.Hands == null)
return;
public void StartDisassembly(Entity<DisassembleOnAltVerbComponent> entity, EntityUid user)
{
// Doafter setup
var doAfterArgs = new DoAfterArgs(EntityManager,
args.User,
user,
entity.Comp.DisassembleTime,
new DisassembleDoAfterEvent(),
entity,
@@ -35,12 +33,22 @@ public sealed partial class DisassembleOnAltVerbSystem : EntitySystem
BreakOnMove = true,
};
_doAfter.TryStartDoAfter(doAfterArgs);
}
private void AddDisassembleVerb(Entity<DisassembleOnAltVerbComponent> entity, ref GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanInteract || !args.CanAccess || args.Hands == null)
return;
var user = args.User;
// Actual verb stuff
AlternativeVerb verb = new()
{
Act = () =>
{
_doAfter.TryStartDoAfter(doAfterArgs);
StartDisassembly(entity, user);
},
Text = Loc.GetString("disassemble-system-verb-disassemble"),
Priority = 2

View File

@@ -0,0 +1,39 @@
using Content.Shared.Engineering.Components;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Weapons.Melee.Balloon;
namespace Content.Shared.Engineering.Systems;
/// <summary>
/// Implements <see cref="InflatableSafeDisassemblyComponent"/>
/// </summary>
public sealed class InflatableSafeDisassemblySystem : EntitySystem
{
[Dependency] private readonly DisassembleOnAltVerbSystem _disassembleOnAltVerbSystem = null!;
[Dependency] private readonly SharedPopupSystem _popupSystem = null!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<InflatableSafeDisassemblyComponent, InteractUsingEvent>(InteractHandler);
}
private void InteractHandler(Entity<InflatableSafeDisassemblyComponent> ent, ref InteractUsingEvent args)
{
if (args.Handled)
return;
if (!HasComp<BalloonPopperComponent>(args.Used))
return;
_popupSystem.PopupPredicted(
Loc.GetString("inflatable-safe-disassembly", ("item", args.Used), ("target", ent.Owner)),
ent,
args.User);
_disassembleOnAltVerbSystem.StartDisassembly((ent, Comp<DisassembleOnAltVerbComponent>(ent)), args.User);
args.Handled = true;
}
}

View File

@@ -16,13 +16,13 @@ public sealed partial class JobCondition : EntityEffectCondition
{
args.EntityManager.TryGetComponent<MindContainerComponent>(args.TargetEntity, out var mindContainer);
if ( mindContainer is null
|| !args.EntityManager.TryGetComponent<MindComponent>(mindContainer.Mind, out var mind))
if (mindContainer is null
|| !args.EntityManager.TryGetComponent<MindComponent>(mindContainer.Mind, out var mind))
return false;
foreach (var roleId in mind.MindRoles)
foreach (var roleId in mind.MindRoleContainer.ContainedEntities)
{
if(!args.EntityManager.HasComponent<JobRoleComponent>(roleId))
if (!args.EntityManager.HasComponent<JobRoleComponent>(roleId))
continue;
if (!args.EntityManager.TryGetComponent<MindRoleComponent>(roleId, out var mindRole))

View File

@@ -9,13 +9,7 @@ public sealed partial class Drunk : EntityEffect
/// BoozePower is how long each metabolism cycle will make the drunk effect last for.
/// </summary>
[DataField]
public float BoozePower = 3f;
/// <summary>
/// Whether speech should be slurred.
/// </summary>
[DataField]
public bool SlurSpeech = true;
public TimeSpan BoozePower = TimeSpan.FromSeconds(3f);
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
=> Loc.GetString("reagent-effect-guidebook-drunk", ("chance", Probability));
@@ -24,11 +18,10 @@ public sealed partial class Drunk : EntityEffect
{
var boozePower = BoozePower;
if (args is EntityEffectReagentArgs reagentArgs) {
if (args is EntityEffectReagentArgs reagentArgs)
boozePower *= reagentArgs.Scale.Float();
}
var drunkSys = args.EntityManager.EntitySysManager.GetEntitySystem<SharedDrunkSystem>();
drunkSys.TryApplyDrunkenness(args.TargetEntity, boozePower, SlurSpeech);
drunkSys.TryApplyDrunkenness(args.TargetEntity, boozePower);
}
}

View File

@@ -0,0 +1,51 @@
using Content.Shared.EntityTable.EntitySelectors;
using Content.Shared.GameTicking.Rules;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityTable.Conditions;
/// <summary>
/// Condition that only succeeds if a table supplies a sufficient "cost" to a given
/// </summary>
public sealed partial class HasBudgetCondition : EntityTableCondition
{
public const string BudgetContextKey = "Budget";
/// <summary>
/// Used for determining the cost for the budget.
/// If null, attempts to fetch the cost from the attached selector.
/// </summary>
[DataField]
public int? CostOverride;
protected override bool EvaluateImplementation(EntityTableSelector root,
IEntityManager entMan,
IPrototypeManager proto,
EntityTableContext ctx)
{
if (!ctx.TryGetData<float>(BudgetContextKey, out var budget))
return false;
int cost;
if (CostOverride != null)
{
cost = CostOverride.Value;
}
else
{
if (root is not EntSelector entSelector)
return false;
if (!proto.Index(entSelector.Id).TryGetComponent(out DynamicRuleCostComponent? costComponent, entMan.ComponentFactory))
{
var log = Logger.GetSawmill("HasBudgetCondition");
log.Error($"Rule {entSelector.Id} does not have a DynamicRuleCostComponent.");
return false;
}
cost = costComponent.Cost;
}
return budget >= cost;
}
}

View File

@@ -0,0 +1,54 @@
using System.Linq;
using Content.Shared.EntityTable.EntitySelectors;
using Content.Shared.GameTicking;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityTable.Conditions;
/// <summary>
/// Condition that succeeds only when the specified gamerule has been run under a certain amount of times
/// </summary>
/// <remarks>
/// This is meant to be attached directly to EntSelector. If it is not, then you'll need to specify what rule
/// is being used inside RuleOverride.
/// </remarks>
public sealed partial class MaxRuleOccurenceCondition : EntityTableCondition
{
/// <summary>
/// The maximum amount of times this rule can have already be run.
/// </summary>
[DataField]
public int Max = 1;
/// <summary>
/// The rule that is being checked for occurrences.
/// If null, it will use the value on the attached selector.
/// </summary>
[DataField]
public EntProtoId? RuleOverride;
protected override bool EvaluateImplementation(EntityTableSelector root,
IEntityManager entMan,
IPrototypeManager proto,
EntityTableContext ctx)
{
string rule;
if (RuleOverride is { } ruleOverride)
{
rule = ruleOverride;
}
else
{
rule = root is EntSelector entSelector
? entSelector.Id
: string.Empty;
}
if (rule == string.Empty)
return false;
var gameTicker = entMan.System<SharedGameTicker>();
return gameTicker.AllPreviousGameRules.Count(p => p.Item2 == rule) < Max;
}
}

View File

@@ -0,0 +1,49 @@
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Content.Shared.EntityTable.EntitySelectors;
using Content.Shared.GameTicking;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityTable.Conditions;
public sealed partial class ReoccurrenceDelayCondition : EntityTableCondition
{
/// <summary>
/// The maximum amount of times this rule can have already be run.
/// </summary>
[DataField]
public TimeSpan Delay = TimeSpan.Zero;
/// <summary>
/// The rule that is being checked for occurrences.
/// If null, it will use the value on the attached selector.
/// </summary>
[DataField]
public EntProtoId? RuleOverride;
protected override bool EvaluateImplementation(EntityTableSelector root,
IEntityManager entMan,
IPrototypeManager proto,
EntityTableContext ctx)
{
string rule;
if (RuleOverride is { } ruleOverride)
{
rule = ruleOverride;
}
else
{
rule = root is EntSelector entSelector
? entSelector.Id
: string.Empty;
}
if (rule == string.Empty)
return false;
var gameTicker = entMan.System<SharedGameTicker>();
return gameTicker.AllPreviousGameRules.Any(
p => p.Item2 == rule && p.Item1 + Delay <= gameTicker.RoundDuration());
}
}

View File

@@ -0,0 +1,34 @@
using Content.Shared.EntityTable.EntitySelectors;
using Content.Shared.GameTicking;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityTable.Conditions;
/// <summary>
/// Condition that passes only if the current round time falls between the minimum and maximum time values.
/// </summary>
public sealed partial class RoundDurationCondition : EntityTableCondition
{
/// <summary>
/// Minimum time the round must have gone on for this condition to pass.
/// </summary>
[DataField]
public TimeSpan Min = TimeSpan.Zero;
/// <summary>
/// Maximum amount of time the round could go on for this condition to pass.
/// </summary>
[DataField]
public TimeSpan Max = TimeSpan.MaxValue;
protected override bool EvaluateImplementation(EntityTableSelector root,
IEntityManager entMan,
IPrototypeManager proto,
EntityTableContext ctx)
{
var gameTicker = entMan.System<SharedGameTicker>();
var duration = gameTicker.RoundDuration();
return duration >= Min && duration <= Max;
}
}

View File

@@ -26,6 +26,9 @@ public sealed partial class GroupSelector : EntityTableSelector
children.Add(child, child.Weight);
}
if (children.Count == 0)
return Array.Empty<EntProtoId>();
var pick = SharedRandomExtensions.Pick(children, rand);
return pick.GetSpawns(rand, entMan, proto, ctx);

View File

@@ -21,6 +21,7 @@ using Robust.Shared.Random;
using Robust.Shared.Timing;
using System.Linq;
using Content.Shared.Movement.Systems;
using Content.Shared.Random.Helpers;
namespace Content.Shared.Flash;
@@ -204,7 +205,8 @@ public abstract class SharedFlashSystem : EntitySystem
foreach (var entity in _entSet)
{
// TODO: Use RandomPredicted https://github.com/space-wizards/RobustToolbox/pull/5849
var rand = new System.Random((int)_timing.CurTick.Value + GetNetEntity(entity).Id);
var seed = SharedRandomExtensions.HashCodeCombine(new() { (int)_timing.CurTick.Value, GetNetEntity(entity).Id });
var rand = new System.Random(seed);
if (!rand.Prob(probability))
continue;

View File

@@ -22,11 +22,7 @@ public abstract partial class SharedPuddleSystem : EntitySystem
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
private static readonly ProtoId<ReagentPrototype> Blood = "Blood";
private static readonly ProtoId<ReagentPrototype> Slime = "Slime";
private static readonly ProtoId<ReagentPrototype> CopperBlood = "CopperBlood";
private static readonly string[] StandoutReagents = [Blood, Slime, CopperBlood];
private string[] _standoutReagents = [];
/// <summary>
/// The lowest threshold to be considered for puddle sprite states as well as slipperiness of a puddle.
@@ -48,9 +44,26 @@ public abstract partial class SharedPuddleSystem : EntitySystem
SubscribeLocalEvent<PuddleComponent, ExaminedEvent>(HandlePuddleExamined);
SubscribeLocalEvent<PuddleComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
CacheStandsout();
InitializeSpillable();
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs ev)
{
if (ev.WasModified<ReagentPrototype>())
CacheStandsout();
}
/// <summary>
/// Used to cache standout reagents for future use.
/// </summary>
private void CacheStandsout()
{
_standoutReagents = [.. _prototypeManager.EnumeratePrototypes<ReagentPrototype>().Where(x => x.Standsout).Select(x => x.ID)];
}
protected virtual void OnSolutionUpdate(Entity<PuddleComponent> entity, ref SolutionContainerChangedEvent args)
{
if (args.SolutionId != entity.Comp.SolutionName)
@@ -158,10 +171,10 @@ public abstract partial class SharedPuddleSystem : EntitySystem
// Kinda EH
// Could potentially do alpha per-solution but future problem.
color = solution.GetColorWithout(_prototypeManager, StandoutReagents);
color = solution.GetColorWithout(_prototypeManager, _standoutReagents);
color = color.WithAlpha(0.7f);
foreach (var standout in StandoutReagents)
foreach (var standout in _standoutReagents)
{
var quantity = solution.GetTotalPrototypeQuantity(standout);
if (quantity <= FixedPoint2.Zero)

View File

@@ -73,7 +73,7 @@ namespace Content.Shared.Friction
// If we're not touching the ground, don't use tileFriction.
// TODO: Make IsWeightless event-based; we already have grid traversals tracked so just raise events
if (body.BodyStatus == BodyStatus.InAir || _gravity.IsWeightless(uid, body, xform) || !xform.Coordinates.IsValid(EntityManager))
if (body.BodyStatus == BodyStatus.InAir || _gravity.IsWeightless(uid) || !xform.Coordinates.IsValid(EntityManager))
friction = xform.GridUid == null || !_gridQuery.HasComp(xform.GridUid) ? _offGridDamping : _airDamping;
else
friction = _frictionModifier * GetTileFriction(uid, body, xform);

View File

@@ -0,0 +1,71 @@
using Content.Shared.EntityTable.EntitySelectors;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.GameTicking.Rules;
/// <summary>
/// Gamerule the spawns multiple antags at intervals based on a budget
/// </summary>
[RegisterComponent, AutoGenerateComponentPause]
public sealed partial class DynamicRuleComponent : Component
{
/// <summary>
/// The total budget for antags.
/// </summary>
[DataField]
public float Budget;
/// <summary>
/// The last time budget was updated.
/// </summary>
[DataField]
public TimeSpan LastBudgetUpdate;
/// <summary>
/// The amount of budget accumulated every second.
/// </summary>
[DataField]
public float BudgetPerSecond = 0.1f;
/// <summary>
/// The minimum or lower bound for budgets to start at.
/// </summary>
[DataField]
public int StartingBudgetMin = 200;
/// <summary>
/// The maximum or upper bound for budgets to start at.
/// </summary>
[DataField]
public int StartingBudgetMax = 350;
/// <summary>
/// The time at which the next rule will start
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan NextRuleTime;
/// <summary>
/// Minimum delay between rules
/// </summary>
[DataField]
public TimeSpan MinRuleInterval = TimeSpan.FromMinutes(10);
/// <summary>
/// Maximum delay between rules
/// </summary>
[DataField]
public TimeSpan MaxRuleInterval = TimeSpan.FromMinutes(30);
/// <summary>
/// A table of rules that are picked from.
/// </summary>
[DataField]
public EntityTableSelector Table = new NoneSelector();
/// <summary>
/// The rules that have been spawned
/// </summary>
[DataField]
public List<EntityUid> Rules = new();
}

View File

@@ -0,0 +1,14 @@
namespace Content.Shared.GameTicking.Rules;
/// <summary>
/// Component that tracks how much a rule "costs" for Dynamic
/// </summary>
[RegisterComponent]
public sealed partial class DynamicRuleCostComponent : Component
{
/// <summary>
/// The amount of budget a rule takes up
/// </summary>
[DataField(required: true)]
public int Cost;
}

View File

@@ -15,6 +15,12 @@ namespace Content.Shared.GameTicking
[Dependency] private readonly IReplayRecordingManager _replay = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
/// <summary>
/// A list storing the start times of all game rules that have been started this round.
/// Game rules can be started and stopped at any time, including midround.
/// </summary>
public abstract IReadOnlyList<(TimeSpan, string)> AllPreviousGameRules { get; }
// See ideally these would be pulled from the job definition or something.
// But this is easier, and at least it isn't hardcoded.
//TODO: Move these, they really belong in StationJobsSystem or a cvar.

View File

@@ -10,20 +10,17 @@ public sealed partial class FloatingVisualsComponent : Component
/// <summary>
/// How long it takes to go from the bottom of the animation to the top.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField, AutoNetworkedField]
public float AnimationTime = 2f;
/// <summary>
/// How far it goes in any direction.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField, AutoNetworkedField]
public Vector2 Offset = new(0, 0.2f);
[ViewVariables(VVAccess.ReadWrite)]
[AutoNetworkedField]
public bool CanFloat = false;
[DataField, AutoNetworkedField]
public bool CanFloat;
public readonly string AnimationKey = "gravity";
}

View File

@@ -0,0 +1,17 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Gravity;
/// <summary>
/// This Component allows a target to be considered "weightless" when Weightless is true. Without this component, the
/// target will never be weightless.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class GravityAffectedComponent : Component
{
/// <summary>
/// If true, this entity will be considered "weightless"
/// </summary>
[ViewVariables, AutoNetworkedField]
public bool Weightless = true;
}

View File

@@ -5,34 +5,20 @@ using Robust.Shared.Serialization;
namespace Content.Shared.Gravity
{
[RegisterComponent]
[AutoGenerateComponentState]
[NetworkedComponent]
public sealed partial class GravityComponent : Component
{
[DataField("gravityShakeSound")]
[DataField, AutoNetworkedField]
public SoundSpecifier GravityShakeSound { get; set; } = new SoundPathSpecifier("/Audio/Effects/alert.ogg");
[ViewVariables(VVAccess.ReadWrite)]
public bool EnabledVV
{
get => Enabled;
set
{
if (Enabled == value) return;
Enabled = value;
var ev = new GravityChangedEvent(Owner, value);
IoCManager.Resolve<IEntityManager>().EventBus.RaiseLocalEvent(Owner, ref ev);
Dirty();
}
}
[DataField("enabled")]
[DataField, AutoNetworkedField]
public bool Enabled;
/// <summary>
/// Inherent gravity ensures GravitySystem won't change Enabled according to the gravity generators attached to this entity.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("inherent")]
[DataField, AutoNetworkedField]
public bool Inherent;
}
}

View File

@@ -8,15 +8,14 @@ namespace Content.Shared.Gravity;
/// </summary>
public abstract class SharedFloatingVisualizerSystem : EntitySystem
{
[Dependency] private readonly SharedGravitySystem GravitySystem = default!;
[Dependency] private readonly SharedGravitySystem _gravity = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<FloatingVisualsComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<GravityChangedEvent>(OnGravityChanged);
SubscribeLocalEvent<FloatingVisualsComponent, EntParentChangedMessage>(OnEntParentChanged);
SubscribeLocalEvent<FloatingVisualsComponent, WeightlessnessChangedEvent>(OnWeightlessnessChanged);
}
/// <summary>
@@ -24,48 +23,28 @@ public abstract class SharedFloatingVisualizerSystem : EntitySystem
/// </summary>
public virtual void FloatAnimation(EntityUid uid, Vector2 offset, string animationKey, float animationTime, bool stop = false) { }
protected bool CanFloat(EntityUid uid, FloatingVisualsComponent component, TransformComponent? transform = null)
protected bool CanFloat(Entity<FloatingVisualsComponent> entity)
{
if (!Resolve(uid, ref transform))
return false;
if (transform.MapID == MapId.Nullspace)
return false;
component.CanFloat = GravitySystem.IsWeightless(uid, xform: transform);
Dirty(uid, component);
return component.CanFloat;
entity.Comp.CanFloat = _gravity.IsWeightless(entity.Owner);
Dirty(entity);
return entity.Comp.CanFloat;
}
private void OnComponentStartup(EntityUid uid, FloatingVisualsComponent component, ComponentStartup args)
private void OnComponentStartup(Entity<FloatingVisualsComponent> entity, ref ComponentStartup args)
{
if (CanFloat(uid, component))
FloatAnimation(uid, component.Offset, component.AnimationKey, component.AnimationTime);
if (CanFloat(entity))
FloatAnimation(entity, entity.Comp.Offset, entity.Comp.AnimationKey, entity.Comp.AnimationTime);
}
private void OnGravityChanged(ref GravityChangedEvent args)
private void OnWeightlessnessChanged(Entity<FloatingVisualsComponent> entity, ref WeightlessnessChangedEvent args)
{
var query = EntityQueryEnumerator<FloatingVisualsComponent, TransformComponent>();
while (query.MoveNext(out var uid, out var floating, out var transform))
{
if (transform.MapID == MapId.Nullspace)
continue;
if (entity.Comp.CanFloat == args.Weightless)
return;
if (transform.GridUid != args.ChangedGridIndex)
continue;
entity.Comp.CanFloat = CanFloat(entity);
Dirty(entity);
floating.CanFloat = !args.HasGravity;
Dirty(uid, floating);
if (!args.HasGravity)
FloatAnimation(uid, floating.Offset, floating.AnimationKey, floating.AnimationTime);
}
}
private void OnEntParentChanged(EntityUid uid, FloatingVisualsComponent component, ref EntParentChangedMessage args)
{
var transform = args.Transform;
if (CanFloat(uid, component, transform))
FloatAnimation(uid, component.Offset, component.AnimationKey, component.AnimationTime);
if (args.Weightless)
FloatAnimation(entity, entity.Comp.Offset, entity.Comp.AnimationKey, entity.Comp.AnimationTime);
}
}

View File

@@ -1,171 +1,255 @@
using Content.Shared.Alert;
using Content.Shared.Inventory;
using Content.Shared.Movement.Components;
using Robust.Shared.GameStates;
using Content.Shared.Throwing;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
namespace Content.Shared.Gravity
namespace Content.Shared.Gravity;
public abstract partial class SharedGravitySystem : EntitySystem
{
public abstract partial class SharedGravitySystem : EntitySystem
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
public static readonly ProtoId<AlertPrototype> WeightlessAlert = "Weightless";
protected EntityQuery<GravityComponent> GravityQuery;
private EntityQuery<GravityAffectedComponent> _weightlessQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
public override void Initialize()
{
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
base.Initialize();
// Grid Gravity
SubscribeLocalEvent<GridInitializeEvent>(OnGridInit);
SubscribeLocalEvent<GravityChangedEvent>(OnGravityChange);
public static readonly ProtoId<AlertPrototype> WeightlessAlert = "Weightless";
// Weightlessness
SubscribeLocalEvent<GravityAffectedComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<GravityAffectedComponent, EntParentChangedMessage>(OnEntParentChanged);
SubscribeLocalEvent<GravityAffectedComponent, PhysicsBodyTypeChangedEvent>(OnBodyTypeChanged);
private EntityQuery<GravityComponent> _gravityQuery;
// Alerts
SubscribeLocalEvent<AlertSyncEvent>(OnAlertsSync);
SubscribeLocalEvent<AlertsComponent, WeightlessnessChangedEvent>(OnWeightlessnessChanged);
SubscribeLocalEvent<AlertsComponent, EntParentChangedMessage>(OnAlertsParentChange);
public bool IsWeightless(EntityUid uid, PhysicsComponent? body = null, TransformComponent? xform = null)
{
Resolve(uid, ref body, false);
// Impulse
SubscribeLocalEvent<GravityAffectedComponent, ShooterImpulseEvent>(OnShooterImpulse);
SubscribeLocalEvent<GravityAffectedComponent, ThrowerImpulseEvent>(OnThrowerImpulse);
if ((body?.BodyType & (BodyType.Static | BodyType.Kinematic)) != 0)
return false;
GravityQuery = GetEntityQuery<GravityComponent>();
_weightlessQuery = GetEntityQuery<GravityAffectedComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
}
if (TryComp<MovementIgnoreGravityComponent>(uid, out var ignoreGravityComponent))
return ignoreGravityComponent.Weightless;
public override void Update(float frameTime)
{
base.Update(frameTime);
UpdateShake();
}
var ev = new IsWeightlessEvent(uid);
RaiseLocalEvent(uid, ref ev);
if (ev.Handled)
return ev.IsWeightless;
public bool IsWeightless(Entity<GravityAffectedComponent?> entity)
{
// If we can be weightless and are weightless, return true, otherwise return false
return _weightlessQuery.Resolve(entity, ref entity.Comp, false) && entity.Comp.Weightless;
}
if (!Resolve(uid, ref xform))
return true;
private bool GetWeightless(Entity<GravityAffectedComponent, PhysicsComponent?> entity)
{
if (!_physicsQuery.Resolve(entity, ref entity.Comp2, false))
return false;
// If grid / map has gravity
if (EntityGridOrMapHaveGravity((uid, xform)))
return false;
if (entity.Comp2.BodyType is BodyType.Static or BodyType.Kinematic)
return false;
// Check if something other than the grid or map is overriding our gravity
var ev = new IsWeightlessEvent();
RaiseLocalEvent(entity, ref ev);
if (ev.Handled)
return ev.IsWeightless;
return !EntityGridOrMapHaveGravity(entity.Owner);
}
/// <summary>
/// Refreshes weightlessness status, needs to be called anytime it would change.
/// </summary>
/// <param name="entity">The entity we are updating the weightless status of</param>
public void RefreshWeightless(Entity<GravityAffectedComponent?> entity)
{
if (!_weightlessQuery.Resolve(entity, ref entity.Comp))
return;
UpdateWeightless(entity!);
}
/// <summary>
/// Overload of <see cref="RefreshWeightless(Entity{GravityAffectedComponent?})"/> which also takes a bool for the weightlessness value we want to change to.
/// This method should only be called if there is no chance something can override the weightless value you're trying to change to.
/// This is really only the case if you're applying a weightless value that overrides non-conditionally from events or are a grid with the gravity component.
/// </summary>
/// <param name="entity">The entity we are updating the weightless status of</param>
/// <param name="weightless">The weightless value we are trying to change to, helps avoid needless networking</param>
public void RefreshWeightless(Entity<GravityAffectedComponent?> entity, bool weightless)
{
if (!_weightlessQuery.Resolve(entity, ref entity.Comp))
return;
// Only update if we're changing our weightless status
if (entity.Comp.Weightless == weightless)
return;
UpdateWeightless(entity!);
}
private void UpdateWeightless(Entity<GravityAffectedComponent> entity)
{
var newWeightless = GetWeightless(entity);
// Don't network or raise events if it's not changing
if (newWeightless == entity.Comp.Weightless)
return;
entity.Comp.Weightless = newWeightless;
Dirty(entity);
var ev = new WeightlessnessChangedEvent(entity.Comp.Weightless);
RaiseLocalEvent(entity, ref ev);
}
private void OnMapInit(Entity<GravityAffectedComponent> entity, ref MapInitEvent args)
{
RefreshWeightless((entity.Owner, entity.Comp));
}
private void OnWeightlessnessChanged(Entity<AlertsComponent> entity, ref WeightlessnessChangedEvent args)
{
if (args.Weightless)
_alerts.ShowAlert(entity, WeightlessAlert);
else
_alerts.ClearAlert(entity, WeightlessAlert);
}
private void OnEntParentChanged(Entity<GravityAffectedComponent> entity, ref EntParentChangedMessage args)
{
// If we've moved but are still on the same grid, then don't do anything.
if (args.OldParent == args.Transform.GridUid)
return;
RefreshWeightless((entity.Owner, entity.Comp));
}
private void OnBodyTypeChanged(Entity<GravityAffectedComponent> entity, ref PhysicsBodyTypeChangedEvent args)
{
// No need to update weightlessness if we're not weightless and we're a body type that can't be weightless
if (args.New is BodyType.Static or BodyType.Kinematic && entity.Comp.Weightless == false)
return;
RefreshWeightless((entity.Owner, entity.Comp));
}
/// <summary>
/// Checks if a given entity is currently standing on a grid or map that supports having gravity at all.
/// </summary>
public bool EntityOnGravitySupportingGridOrMap(Entity<TransformComponent?> entity)
{
entity.Comp ??= Transform(entity);
return GravityQuery.HasComp(entity.Comp.GridUid) ||
GravityQuery.HasComp(entity.Comp.MapUid);
}
/// <summary>
/// Checks if a given entity is currently standing on a grid or map that has gravity of some kind.
/// </summary>
public bool EntityGridOrMapHaveGravity(Entity<TransformComponent?> entity)
{
entity.Comp ??= Transform(entity);
// DO NOT SET TO WEIGHTLESS IF THEY'RE IN NULL-SPACE
// TODO: If entities actually properly pause when leaving PVS rather than entering null-space this can probably go.
if (entity.Comp.MapID == MapId.Nullspace)
return true;
}
/// <summary>
/// Checks if a given entity is currently standing on a grid or map that supports having gravity at all.
/// </summary>
public bool EntityOnGravitySupportingGridOrMap(Entity<TransformComponent?> entity)
return GravityQuery.TryComp(entity.Comp.GridUid, out var gravity) && gravity.Enabled ||
GravityQuery.TryComp(entity.Comp.MapUid, out var mapGravity) && mapGravity.Enabled;
}
private void OnGravityChange(ref GravityChangedEvent args)
{
var gravity = AllEntityQuery<GravityAffectedComponent, TransformComponent>();
while(gravity.MoveNext(out var uid, out var weightless, out var xform))
{
entity.Comp ??= Transform(entity);
if (xform.GridUid != args.ChangedGridIndex)
continue;
return _gravityQuery.HasComp(entity.Comp.GridUid) ||
_gravityQuery.HasComp(entity.Comp.MapUid);
}
/// <summary>
/// Checks if a given entity is currently standing on a grid or map that has gravity of some kind.
/// </summary>
public bool EntityGridOrMapHaveGravity(Entity<TransformComponent?> entity)
{
entity.Comp ??= Transform(entity);
return _gravityQuery.TryComp(entity.Comp.GridUid, out var gravity) && gravity.Enabled ||
_gravityQuery.TryComp(entity.Comp.MapUid, out var mapGravity) && mapGravity.Enabled;
}
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GridInitializeEvent>(OnGridInit);
SubscribeLocalEvent<AlertSyncEvent>(OnAlertsSync);
SubscribeLocalEvent<AlertsComponent, EntParentChangedMessage>(OnAlertsParentChange);
SubscribeLocalEvent<GravityChangedEvent>(OnGravityChange);
SubscribeLocalEvent<GravityComponent, ComponentGetState>(OnGetState);
SubscribeLocalEvent<GravityComponent, ComponentHandleState>(OnHandleState);
_gravityQuery = GetEntityQuery<GravityComponent>();
}
public override void Update(float frameTime)
{
base.Update(frameTime);
UpdateShake();
}
private void OnHandleState(EntityUid uid, GravityComponent component, ref ComponentHandleState args)
{
if (args.Current is not GravityComponentState state)
return;
if (component.EnabledVV == state.Enabled)
return;
component.EnabledVV = state.Enabled;
var ev = new GravityChangedEvent(uid, component.EnabledVV);
RaiseLocalEvent(uid, ref ev, true);
}
private void OnGetState(EntityUid uid, GravityComponent component, ref ComponentGetState args)
{
args.State = new GravityComponentState(component.EnabledVV);
}
private void OnGravityChange(ref GravityChangedEvent ev)
{
var alerts = AllEntityQuery<AlertsComponent, TransformComponent>();
while(alerts.MoveNext(out var uid, out _, out var xform))
{
if (xform.GridUid != ev.ChangedGridIndex)
continue;
if (!ev.HasGravity)
{
_alerts.ShowAlert(uid, WeightlessAlert);
}
else
{
_alerts.ClearAlert(uid, WeightlessAlert);
}
}
}
private void OnAlertsSync(AlertSyncEvent ev)
{
if (IsWeightless(ev.Euid))
{
_alerts.ShowAlert(ev.Euid, WeightlessAlert);
}
else
{
_alerts.ClearAlert(ev.Euid, WeightlessAlert);
}
}
private void OnAlertsParentChange(EntityUid uid, AlertsComponent component, ref EntParentChangedMessage args)
{
if (IsWeightless(uid))
{
_alerts.ShowAlert(uid, WeightlessAlert);
}
else
{
_alerts.ClearAlert(uid, WeightlessAlert);
}
}
private void OnGridInit(GridInitializeEvent ev)
{
EnsureComp<GravityComponent>(ev.EntityUid);
}
[Serializable, NetSerializable]
private sealed class GravityComponentState : ComponentState
{
public bool Enabled { get; }
public GravityComponentState(bool enabled)
{
Enabled = enabled;
}
RefreshWeightless((uid, weightless), !args.HasGravity);
}
}
[ByRefEvent]
public record struct IsWeightlessEvent(EntityUid Entity, bool IsWeightless = false, bool Handled = false) : IInventoryRelayEvent
private void OnAlertsSync(AlertSyncEvent ev)
{
SlotFlags IInventoryRelayEvent.TargetSlots => ~SlotFlags.POCKET;
if (IsWeightless(ev.Euid))
_alerts.ShowAlert(ev.Euid, WeightlessAlert);
else
_alerts.ClearAlert(ev.Euid, WeightlessAlert);
}
private void OnAlertsParentChange(EntityUid uid, AlertsComponent component, ref EntParentChangedMessage args)
{
if (IsWeightless(uid))
_alerts.ShowAlert(uid, WeightlessAlert);
else
_alerts.ClearAlert(uid, WeightlessAlert);
}
private void OnGridInit(GridInitializeEvent ev)
{
EnsureComp<GravityComponent>(ev.EntityUid);
}
[Serializable, NetSerializable]
private sealed class GravityComponentState : ComponentState
{
public bool Enabled { get; }
public GravityComponentState(bool enabled)
{
Enabled = enabled;
}
}
private void OnThrowerImpulse(Entity<GravityAffectedComponent> entity, ref ThrowerImpulseEvent args)
{
args.Push = true;
}
private void OnShooterImpulse(Entity<GravityAffectedComponent> entity, ref ShooterImpulseEvent args)
{
args.Push = true;
}
}
/// <summary>
/// Raised to determine if an entity's weightlessness is being overwritten by a component or item with a component.
/// </summary>
/// <param name="IsWeightless">Whether we should be weightless</param>
/// <param name="Handled">Whether something is trying to override our weightlessness</param>
[ByRefEvent]
public record struct IsWeightlessEvent(bool IsWeightless = false, bool Handled = false) : IInventoryRelayEvent
{
SlotFlags IInventoryRelayEvent.TargetSlots => ~SlotFlags.POCKET;
}
/// <summary>
/// Raised on an entity when their weightless status changes.
/// </summary>
[ByRefEvent]
public readonly record struct WeightlessnessChangedEvent(bool Weightless);

View File

@@ -3,6 +3,7 @@ using Content.Shared.Database;
using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.Inventory.VirtualItem;
using Content.Shared.Storage.Components;
using Content.Shared.Tag;
using Robust.Shared.Containers;
using Robust.Shared.Map;
@@ -20,6 +21,7 @@ public abstract partial class SharedHandsSystem
private void InitializeDrop()
{
SubscribeLocalEvent<HandsComponent, EntRemovedFromContainerMessage>(HandleEntityRemoved);
SubscribeLocalEvent<HandsComponent, EntityStorageIntoContainerAttemptEvent>(OnEntityStorageDump);
}
protected virtual void HandleEntityRemoved(EntityUid uid, HandsComponent hands, EntRemovedFromContainerMessage args)
@@ -39,6 +41,14 @@ public abstract partial class SharedHandsSystem
_virtualSystem.DeleteVirtualItem((args.Entity, @virtual), uid);
}
private void OnEntityStorageDump(Entity<HandsComponent> ent, ref EntityStorageIntoContainerAttemptEvent args)
{
// If you're physically carrying an EntityStroage which tries to dump its contents out,
// we want those contents to fall to the floor.
args.Cancelled = true;
}
private bool ShouldIgnoreRestrictions(EntityUid user)
{
//Checks if the Entity is something that shouldn't care about drop distance or walls ie Aghost

View File

@@ -0,0 +1,18 @@
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
namespace Content.Shared.Implants.Components;
/// <summary>
/// Added to implants with the see <see cref="SubdermalImplantComponent"/>.
/// When implanted it will cause other implants in the whitelist to be deleted and thus replaced.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ReplacementImplantComponent : Component
{
/// <summary>
/// Whitelist for which implants to delete.
/// </summary>
[DataField(required: true)]
public EntityWhitelist Whitelist = new();
}

View File

@@ -0,0 +1,11 @@
using Content.Shared.Storage;
using Robust.Shared.GameStates;
namespace Content.Shared.Implants.Components;
/// <summary>
/// Handles emptying the implant's <see cref="StorageComponent"/> when the implant is removed.
/// Without this the contents would be deleted.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class StorageImplantComponent : Component;

View File

@@ -16,13 +16,21 @@ public sealed partial class SubdermalImplantComponent : Component
/// <summary>
/// Used where you want the implant to grant the owner an instant action.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("implantAction")]
[DataField]
public EntProtoId? ImplantAction;
/// <summary>
/// The provided action entity.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? Action;
/// <summary>
/// Components to add/remove to the implantee when the implant is injected/extracted.
/// </summary>
[DataField]
public ComponentRegistry ImplantComponents = new();
/// <summary>
/// The entity this implant is inside
/// </summary>
@@ -32,8 +40,7 @@ public sealed partial class SubdermalImplantComponent : Component
/// <summary>
/// Should this implant be removeable?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("permanent"), AutoNetworkedField]
[DataField, AutoNetworkedField]
public bool Permanent = false;
/// <summary>
@@ -61,23 +68,20 @@ public sealed partial class SubdermalImplantComponent : Component
/// <summary>
/// Used for opening the storage implant via action.
/// </summary>
public sealed partial class OpenStorageImplantEvent : InstantActionEvent
{
}
/// <remarks>
/// TODO: Delete this and just add a ToggleUIOnTriggerComponent
/// </remarks>
public sealed partial class OpenStorageImplantEvent : InstantActionEvent;
/// <summary>
/// Used for triggering trigger events on the implant via action
/// </summary>
public sealed partial class ActivateImplantEvent : InstantActionEvent
{
}
public sealed partial class ActivateImplantEvent : InstantActionEvent;
/// <summary>
/// Used for opening the uplink implant via action.
/// </summary>
public sealed partial class OpenUplinkImplantEvent : InstantActionEvent
{
}
/// <remarks>
/// TODO: Delete this and just add a ToggleUIOnTriggerComponent
/// </remarks>
public sealed partial class OpenUplinkImplantEvent : InstantActionEvent;

View File

@@ -0,0 +1,34 @@
using Content.Shared.Implants.Components;
using Content.Shared.Whitelist;
using Robust.Shared.Containers;
namespace Content.Shared.Implants;
public sealed class ReplacementImplantSystem : EntitySystem
{
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ReplacementImplantComponent, ImplantImplantedEvent>(OnImplantImplanted);
}
private void OnImplantImplanted(Entity<ReplacementImplantComponent> ent, ref ImplantImplantedEvent args)
{
if (!_container.TryGetContainer(args.Implanted, ImplanterComponent.ImplantSlotId, out var implantContainer))
return;
foreach (var implant in implantContainer.ContainedEntities)
{
if (implant == ent.Owner)
continue; // don't delete the replacement
if (_whitelist.IsWhitelistPass(ent.Comp.Whitelist, implant))
PredictedQueueDel(implant);
}
}
}

View File

@@ -118,7 +118,7 @@ public abstract class SharedImplanterSystem : EntitySystem
//Set to draw mode if not implant only
public void Implant(EntityUid user, EntityUid target, EntityUid implanter, ImplanterComponent component)
{
if (!CanImplant(user, target, implanter, component, out var implant, out var implantComp))
if (!CanImplant(user, target, implanter, component, out var implant, out _))
return;
// Check if we are trying to implant a implant which is already implanted
@@ -137,7 +137,6 @@ public abstract class SharedImplanterSystem : EntitySystem
if (component.ImplanterSlot.ContainerSlot != null)
_container.Remove(implant.Value, component.ImplanterSlot.ContainerSlot);
implantComp.ImplantedEntity = target;
implantContainer.OccludesLight = false;
_container.Insert(implant.Value, implantContainer);
@@ -280,7 +279,6 @@ public abstract class SharedImplanterSystem : EntitySystem
private void DrawImplantIntoImplanter(EntityUid implanter, EntityUid target, EntityUid implant, BaseContainer implantContainer, ContainerSlot implanterContainer, SubdermalImplantComponent implantComp)
{
_container.Remove(implant, implantContainer);
implantComp.ImplantedEntity = null;
_container.Insert(implant, implanterContainer);
var ev = new TransferDnaEvent { Donor = target, Recipient = implanter };

View File

@@ -0,0 +1,50 @@
using Content.Shared.Implants.Components;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Mobs;
namespace Content.Shared.Implants;
public abstract partial class SharedSubdermalImplantSystem
{
public void InitializeRelay()
{
SubscribeLocalEvent<ImplantedComponent, MobStateChangedEvent>(RelayToImplantEvent);
SubscribeLocalEvent<ImplantedComponent, AfterInteractUsingEvent>(RelayToImplantEvent);
SubscribeLocalEvent<ImplantedComponent, SuicideEvent>(RelayToImplantEvent);
}
/// <summary>
/// Relays events from the implanted to the implant.
/// </summary>
private void RelayToImplantEvent<T>(EntityUid uid, ImplantedComponent component, T args) where T : notnull
{
if (!_container.TryGetContainer(uid, ImplanterComponent.ImplantSlotId, out var implantContainer))
return;
var relayEv = new ImplantRelayEvent<T>(args, uid);
foreach (var implant in implantContainer.ContainedEntities)
{
if (args is HandledEntityEventArgs { Handled: true })
return;
RaiseLocalEvent(implant, relayEv);
}
}
}
/// <summary>
/// Wrapper for relaying events from an implanted entity to their implants.
/// </summary>
public sealed class ImplantRelayEvent<T> where T : notnull
{
public readonly T Event;
public readonly EntityUid ImplantedEntity;
public ImplantRelayEvent(T ev, EntityUid implantedEntity)
{
Event = ev;
ImplantedEntity = implantedEntity;
}
}

View File

@@ -1,90 +1,76 @@
using System.Linq;
using Content.Shared.Actions;
using Content.Shared.Implants.Components;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Mobs;
using Content.Shared.Tag;
using JetBrains.Annotations;
using Robust.Shared.Containers;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Shared.Implants;
public abstract class SharedSubdermalImplantSystem : EntitySystem
public abstract partial class SharedSubdermalImplantSystem : EntitySystem
{
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
public const string BaseStorageId = "storagebase";
private static readonly ProtoId<TagPrototype> MicroBombTag = "MicroBomb";
private static readonly ProtoId<TagPrototype> MacroBombTag = "MacroBomb";
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
base.Initialize();
InitializeRelay();
SubscribeLocalEvent<SubdermalImplantComponent, EntGotInsertedIntoContainerMessage>(OnInsert);
SubscribeLocalEvent<SubdermalImplantComponent, ContainerGettingRemovedAttemptEvent>(OnRemoveAttempt);
SubscribeLocalEvent<SubdermalImplantComponent, EntGotRemovedFromContainerMessage>(OnRemove);
SubscribeLocalEvent<ImplantedComponent, MobStateChangedEvent>(RelayToImplantEvent);
SubscribeLocalEvent<ImplantedComponent, AfterInteractUsingEvent>(RelayToImplantEvent);
SubscribeLocalEvent<ImplantedComponent, SuicideEvent>(RelayToImplantEvent);
}
private void OnInsert(EntityUid uid, SubdermalImplantComponent component, EntGotInsertedIntoContainerMessage args)
private void OnInsert(Entity<SubdermalImplantComponent> ent, ref EntGotInsertedIntoContainerMessage args)
{
if (component.ImplantedEntity == null)
// The results of the container change are already networked on their own
if (_timing.ApplyingState)
return;
if (!string.IsNullOrWhiteSpace(component.ImplantAction))
{
_actionsSystem.AddAction(component.ImplantedEntity.Value, ref component.Action, component.ImplantAction, uid);
}
if (args.Container.ID != ImplanterComponent.ImplantSlotId)
return;
// replace micro bomb with macro bomb
// TODO: this shouldn't be hardcoded here
if (_container.TryGetContainer(component.ImplantedEntity.Value, ImplanterComponent.ImplantSlotId, out var implantContainer) && _tag.HasTag(uid, MacroBombTag))
{
foreach (var implant in implantContainer.ContainedEntities)
{
if (_tag.HasTag(implant, MicroBombTag))
{
_container.Remove(implant, implantContainer);
PredictedQueueDel(implant);
}
}
}
ent.Comp.ImplantedEntity = args.Container.Owner;
Dirty(ent);
var ev = new ImplantImplantedEvent(uid, component.ImplantedEntity.Value);
RaiseLocalEvent(uid, ref ev);
EntityManager.AddComponents(ent.Comp.ImplantedEntity.Value, ent.Comp.ImplantComponents);
if (ent.Comp.ImplantAction != null)
_actions.AddAction(ent.Comp.ImplantedEntity.Value, ref ent.Comp.Action, ent.Comp.ImplantAction, ent.Owner);
var ev = new ImplantImplantedEvent(ent.Owner, ent.Comp.ImplantedEntity.Value);
RaiseLocalEvent(ent.Owner, ref ev);
}
private void OnRemoveAttempt(EntityUid uid, SubdermalImplantComponent component, ContainerGettingRemovedAttemptEvent args)
private void OnRemoveAttempt(Entity<SubdermalImplantComponent> ent, ref ContainerGettingRemovedAttemptEvent args)
{
if (component.Permanent && component.ImplantedEntity != null)
if (ent.Comp.Permanent && ent.Comp.ImplantedEntity != null)
args.Cancel();
}
private void OnRemove(EntityUid uid, SubdermalImplantComponent component, EntGotRemovedFromContainerMessage args)
private void OnRemove(Entity<SubdermalImplantComponent> ent, ref EntGotRemovedFromContainerMessage args)
{
if (component.ImplantedEntity == null || Terminating(component.ImplantedEntity.Value))
// The results of the container change are already networked on their own
if (_timing.ApplyingState)
return;
if (component.ImplantAction != null)
_actionsSystem.RemoveProvidedActions(component.ImplantedEntity.Value, uid);
if (!_container.TryGetContainer(uid, BaseStorageId, out var storageImplant))
if (args.Container.ID != ImplanterComponent.ImplantSlotId)
return;
var containedEntites = storageImplant.ContainedEntities.ToArray();
if (ent.Comp.ImplantedEntity == null || Terminating(ent.Comp.ImplantedEntity.Value))
return;
foreach (var entity in containedEntites)
{
_transformSystem.DropNextTo(entity, uid);
}
EntityManager.RemoveComponents(ent.Comp.ImplantedEntity.Value, ent.Comp.ImplantComponents);
_actions.RemoveAction(ent.Comp.ImplantedEntity.Value, ent.Comp.Action);
ent.Comp.Action = null;
var ev = new ImplantRemovedEvent(ent.Owner, ent.Comp.ImplantedEntity.Value);
RaiseLocalEvent(ent.Owner, ref ev);
ent.Comp.ImplantedEntity = null;
Dirty(ent);
}
/// <summary>
@@ -106,23 +92,26 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
/// <returns>
/// The implant, if it was successfully created. Otherwise, null.
/// </returns>>
public EntityUid? AddImplant(EntityUid uid, String implantId)
public EntityUid? AddImplant(EntityUid target, EntProtoId implantId)
{
var coords = Transform(uid).Coordinates;
var ent = Spawn(implantId, coords);
if (_net.IsClient)
return null; // can't interact with predicted spawns yet
if (TryComp<SubdermalImplantComponent>(ent, out var implant))
var coords = Transform(target).Coordinates;
var implant = Spawn(implantId, coords);
if (TryComp<SubdermalImplantComponent>(implant, out var implantComp))
{
ForceImplant(uid, ent, implant);
ForceImplant(target, (implant, implantComp));
}
else
{
Log.Warning($"Found invalid starting implant '{implantId}' on {uid} {ToPrettyString(uid):implanted}");
Del(ent);
Log.Warning($"Tried to inject implant '{implantId}' without SubdermalImplantComponent into {ToPrettyString(target):implanted}");
Del(implant);
return null;
}
return ent;
return implant;
}
/// <summary>
@@ -131,15 +120,16 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
/// </summary>
/// <param name="target">The entity to be implanted</param>
/// <param name="implant"> The implant</param>
/// <param name="component">The implant component</param>
public void ForceImplant(EntityUid target, EntityUid implant, SubdermalImplantComponent component)
public void ForceImplant(EntityUid target, Entity<SubdermalImplantComponent?> implant)
{
if (!Resolve(implant, ref implant.Comp))
return;
//If the target doesn't have the implanted component, add it.
var implantedComp = EnsureComp<ImplantedComponent>(target);
var implantContainer = implantedComp.ImplantContainer;
component.ImplantedEntity = target;
_container.Insert(implant, implantContainer);
implant.Comp.ImplantedEntity = target;
_container.Insert(implant.Owner, implantedComp.ImplantContainer);
}
/// <summary>
@@ -147,60 +137,25 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
/// </summary>
/// <param name="target">the implanted entity</param>
/// <param name="implant">the implant</param>
[PublicAPI]
public void ForceRemove(EntityUid target, EntityUid implant)
public void ForceRemove(Entity<ImplantedComponent?> target, EntityUid implant)
{
if (!TryComp<ImplantedComponent>(target, out var implanted))
if (!Resolve(target, ref target.Comp))
return;
var implantContainer = implanted.ImplantContainer;
_container.Remove(implant, implantContainer);
QueueDel(implant);
_container.Remove(implant, target.Comp.ImplantContainer);
PredictedQueueDel(implant);
}
/// <summary>
/// Removes and deletes implants by force
/// </summary>
/// <param name="target">The entity to have implants removed</param>
[PublicAPI]
public void WipeImplants(EntityUid target)
public void WipeImplants(Entity<ImplantedComponent?> target)
{
if (!TryComp<ImplantedComponent>(target, out var implanted))
if (!Resolve(target, ref target.Comp, false))
return;
var implantContainer = implanted.ImplantContainer;
_container.CleanContainer(implantContainer);
}
//Relays from the implanted to the implant
private void RelayToImplantEvent<T>(EntityUid uid, ImplantedComponent component, T args) where T : notnull
{
if (!_container.TryGetContainer(uid, ImplanterComponent.ImplantSlotId, out var implantContainer))
return;
var relayEv = new ImplantRelayEvent<T>(args, uid);
foreach (var implant in implantContainer.ContainedEntities)
{
if (args is HandledEntityEventArgs { Handled : true })
return;
RaiseLocalEvent(implant, relayEv);
}
}
}
public sealed class ImplantRelayEvent<T> where T : notnull
{
public readonly T Event;
public readonly EntityUid ImplantedEntity;
public ImplantRelayEvent(T ev, EntityUid implantedEntity)
{
Event = ev;
ImplantedEntity = implantedEntity;
_container.CleanContainer(target.Comp.ImplantContainer);
}
}
@@ -212,12 +167,30 @@ public sealed class ImplantRelayEvent<T> where T : notnull
/// implant implant implant implant
/// </remarks>
[ByRefEvent]
public readonly struct ImplantImplantedEvent
public readonly record struct ImplantImplantedEvent
{
public readonly EntityUid Implant;
public readonly EntityUid? Implanted;
public readonly EntityUid Implanted;
public ImplantImplantedEvent(EntityUid implant, EntityUid? implanted)
public ImplantImplantedEvent(EntityUid implant, EntityUid implanted)
{
Implant = implant;
Implanted = implanted;
}
}
/// <summary>
/// Event that is raised whenever an implant is removed from someone.
/// Raised on the the implant entity.
/// </summary>
[ByRefEvent]
public readonly record struct ImplantRemovedEvent
{
public readonly EntityUid Implant;
public readonly EntityUid Implanted;
public ImplantRemovedEvent(EntityUid implant, EntityUid implanted)
{
Implant = implant;
Implanted = implanted;

View File

@@ -0,0 +1,36 @@
using System.Linq;
using Content.Shared.Implants.Components;
using Content.Shared.Storage;
using Robust.Shared.Containers;
using Robust.Shared.Network;
namespace Content.Shared.Implants;
public sealed class StorageImplantSystem : EntitySystem
{
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly INetManager _net = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StorageImplantComponent, ImplantRemovedEvent>(OnImplantRemoved);
}
private void OnImplantRemoved(Entity<StorageImplantComponent> ent, ref ImplantRemovedEvent args)
{
if (_net.IsClient)
return; // TODO: RandomPredicted and DropNextToPredicted
if (!_container.TryGetContainer(ent.Owner, StorageComponent.ContainerId, out var storageImplant))
return;
var contained = storageImplant.ContainedEntities.ToArray();
foreach (var entity in contained)
{
_transform.DropNextTo(entity, ent.Owner);
}
}
}

View File

@@ -159,7 +159,7 @@ public sealed class SmartEquipSystem : EntitySystem
}
_hands.TryDrop((uid, hands), hands.ActiveHandId!);
_storage.Insert(slotItem, handItem.Value, out var stacked, out _);
_storage.Insert(slotItem, handItem.Value, out var stacked, out _, user: uid);
// if the hand item stacked with the things in inventory, but there's no more space left for the rest
// of the stack, place the stack back in hand rather than dropping it on the floor

View File

@@ -27,6 +27,7 @@ using Content.Shared.Overlays;
using Content.Shared.Projectiles;
using Content.Shared.Radio;
using Content.Shared.Slippery;
using Content.Shared.Standing;
using Content.Shared.Strip.Components;
using Content.Shared.Temperature;
using Content.Shared.Verbs;
@@ -66,6 +67,8 @@ public partial class InventorySystem
SubscribeLocalEvent<InventoryComponent, IsUnequippingTargetAttemptEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, ChameleonControllerOutfitSelectedEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, BeforeEmoteEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, StoodEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, DownedEvent>(RelayInventoryEvent);
// by-ref events
SubscribeLocalEvent<InventoryComponent, RefreshFrictionModifiersEvent>(RefRelayInventoryEvent);
@@ -123,7 +126,7 @@ public partial class InventorySystem
return;
// this copies the by-ref event if it is a struct
var ev = new InventoryRelayedEvent<T>(args);
var ev = new InventoryRelayedEvent<T>(args, inventory.Owner);
var enumerator = new InventorySlotEnumerator(inventory, args.TargetSlots);
while (enumerator.NextItem(out var item))
{
@@ -139,7 +142,7 @@ public partial class InventorySystem
if (args.TargetSlots == SlotFlags.NONE)
return;
var ev = new InventoryRelayedEvent<T>(args);
var ev = new InventoryRelayedEvent<T>(args, inventory.Owner);
var enumerator = new InventorySlotEnumerator(inventory, args.TargetSlots);
while (enumerator.NextItem(out var item))
{
@@ -150,7 +153,7 @@ public partial class InventorySystem
private void OnGetEquipmentVerbs(EntityUid uid, InventoryComponent component, GetVerbsEvent<EquipmentVerb> args)
{
// Automatically relay stripping related verbs to all equipped clothing.
var ev = new InventoryRelayedEvent<GetVerbsEvent<EquipmentVerb>>(args);
var ev = new InventoryRelayedEvent<GetVerbsEvent<EquipmentVerb>>(args, uid);
var enumerator = new InventorySlotEnumerator(component);
while (enumerator.NextItem(out var item, out var slotDef))
{
@@ -162,7 +165,7 @@ public partial class InventorySystem
private void OnGetInnateVerbs(EntityUid uid, InventoryComponent component, GetVerbsEvent<InnateVerb> args)
{
// Automatically relay stripping related verbs to all equipped clothing.
var ev = new InventoryRelayedEvent<GetVerbsEvent<InnateVerb>>(args);
var ev = new InventoryRelayedEvent<GetVerbsEvent<InnateVerb>>(args, uid);
var enumerator = new InventorySlotEnumerator(component, SlotFlags.WITHOUT_POCKET);
while (enumerator.NextItem(out var item))
{
@@ -185,9 +188,12 @@ public sealed class InventoryRelayedEvent<TEvent> : EntityEventArgs
{
public TEvent Args;
public InventoryRelayedEvent(TEvent args)
public EntityUid Owner;
public InventoryRelayedEvent(TEvent args, EntityUid owner)
{
Args = args;
Owner = owner;
}
}

View File

@@ -1,40 +1,143 @@
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Content.Shared.Nutrition.Components;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Kitchen.Components;
/// <summary>
/// Used to mark entity that should act as a spike.
/// </summary>
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState, AutoGenerateComponentPause]
[Access(typeof(SharedKitchenSpikeSystem))]
public sealed partial class KitchenSpikeComponent : Component
{
[DataField("delay")]
public float SpikeDelay = 7.0f;
/// <summary>
/// Default sound to play when the victim is hooked or unhooked.
/// </summary>
private static readonly ProtoId<SoundCollectionPrototype> DefaultSpike = new("Spike");
[ViewVariables(VVAccess.ReadWrite)]
[DataField("sound")]
public SoundSpecifier SpikeSound = new SoundPathSpecifier("/Audio/Effects/Fluids/splat.ogg");
/// <summary>
/// Default sound to play when the victim is butchered.
/// </summary>
private static readonly ProtoId<SoundCollectionPrototype> DefaultSpikeButcher = new("SpikeButcher");
public List<string>? PrototypesToSpawn;
/// <summary>
/// ID of the container where the victim will be stored.
/// </summary>
[DataField, AutoNetworkedField]
public string ContainerId = "body";
// TODO: Spiking alive mobs? (Replace with uid) (deal damage to their limbs on spiking, kill on first butcher attempt?)
public string MeatSource1p = "?";
public string MeatSource0 = "?";
public string Victim = "?";
/// <summary>
/// Container where the victim will be stored.
/// </summary>
[ViewVariables]
public ContainerSlot BodyContainer = default!;
// Prevents simultaneous spiking of two bodies (could be replaced with CancellationToken, but I don't see any situation where Cancel could be called)
public bool InUse;
/// <summary>
/// Sound to play when the victim is hooked or unhooked.
/// </summary>
[DataField, AutoNetworkedField]
public SoundSpecifier SpikeSound = new SoundCollectionSpecifier(DefaultSpike);
[Serializable, NetSerializable]
public enum KitchenSpikeVisuals : byte
/// <summary>
/// Sound to play when the victim is butchered.
/// </summary>
[DataField, AutoNetworkedField]
public SoundSpecifier ButcherSound = new SoundCollectionSpecifier(DefaultSpikeButcher);
/// <summary>
/// Damage that will be applied to the victim when they are hooked or unhooked.
/// </summary>
[DataField, AutoNetworkedField]
public DamageSpecifier SpikeDamage = new()
{
Status
}
DamageDict = new Dictionary<string, FixedPoint2>
{
{ "Piercing", 10 },
},
};
[Serializable, NetSerializable]
public enum KitchenSpikeStatus : byte
/// <summary>
/// Damage that will be applied to the victim when they are butchered.
/// </summary>
[DataField, AutoNetworkedField]
public DamageSpecifier ButcherDamage = new()
{
Empty,
Bloody
}
DamageDict = new Dictionary<string, FixedPoint2>
{
{ "Slash", 20 },
},
};
/// <summary>
/// Damage that the victim will receive over time.
/// </summary>
[DataField, AutoNetworkedField]
public DamageSpecifier TimeDamage = new()
{
DamageDict = new Dictionary<string, FixedPoint2>
{
{ "Blunt", 1 }, // Mobs are only gibbed from blunt (at least for now).
},
};
/// <summary>
/// The next time when the damage will be applied to the victim.
/// </summary>
[AutoPausedField, AutoNetworkedField]
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan NextDamage;
/// <summary>
/// How often the damage should be applied to the victim.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan DamageInterval = TimeSpan.FromSeconds(10);
/// <summary>
/// Time that it will take to put the victim on the spike.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan HookDelay = TimeSpan.FromSeconds(7);
/// <summary>
/// Time that it will take to put the victim off the spike.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan UnhookDelay = TimeSpan.FromSeconds(10);
/// <summary>
/// Time that it will take to butcher the victim while they are alive.
/// </summary>
/// <remarks>
/// This is summed up with a <see cref="ButcherableComponent"/>'s butcher delay in butcher DoAfter.
/// </remarks>
[DataField, AutoNetworkedField]
public TimeSpan ButcherDelayAlive = TimeSpan.FromSeconds(8);
/// <summary>
/// Value by which the butchering delay will be multiplied if the victim is dead.
/// </summary>
[DataField, AutoNetworkedField]
public float ButcherModifierDead = 0.5f;
}
[Serializable, NetSerializable]
public enum KitchenSpikeVisuals : byte
{
Status,
}
[Serializable, NetSerializable]
public enum KitchenSpikeStatus : byte
{
Empty,
Bloody, // TODO: Add sprites for different species.
}

View File

@@ -0,0 +1,10 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Kitchen.Components;
/// <summary>
/// Used to mark entities that are currently hooked on the spike.
/// </summary>
[RegisterComponent, NetworkedComponent]
[Access(typeof(SharedKitchenSpikeSystem))]
public sealed partial class KitchenSpikeHookedComponent : Component;

View File

@@ -0,0 +1,10 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Kitchen.Components;
/// <summary>
/// Used to mark entity that was butchered on the spike.
/// </summary>
[RegisterComponent, NetworkedComponent]
[Access(typeof(SharedKitchenSpikeSystem))]
public sealed partial class KitchenSpikeVictimComponent : Component;

View File

@@ -0,0 +1,26 @@
using Content.Shared.Nutrition.Components;
using Robust.Shared.GameStates;
namespace Content.Shared.Kitchen.Components;
/// <summary>
/// Applies to items that are capable of butchering entities, or
/// are otherwise sharp for some purpose.
/// </summary>
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState]
public sealed partial class SharpComponent : Component
{
/// <summary>
/// List of the entities that are currently being butchered.
/// </summary>
// TODO just make this a tool type. Move SharpSystem to shared.
[AutoNetworkedField]
public readonly HashSet<EntityUid> Butchering = [];
/// <summary>
/// Affects butcher delay of the <see cref="ButcherableComponent"/>.
/// </summary>
[DataField, AutoNetworkedField]
public float ButcherDelayModifier = 1.0f;
}

View File

@@ -1,38 +1,456 @@
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Systems;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.Destructible;
using Content.Shared.DoAfter;
using Content.Shared.DragDrop;
using Content.Shared.Examine;
using Content.Shared.Hands;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Inventory.Events;
using Content.Shared.Item;
using Content.Shared.Kitchen.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Events;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Throwing;
using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
namespace Content.Shared.Kitchen;
public abstract class SharedKitchenSpikeSystem : EntitySystem
/// <summary>
/// Used to butcher some entities like monkeys.
/// </summary>
public sealed class SharedKitchenSpikeSystem : EntitySystem
{
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly MetaDataSystem _metaDataSystem = default!;
[Dependency] private readonly ISharedAdminLogManager _logger = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly SharedBodySystem _bodySystem = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IRobustRandom _random = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<KitchenSpikeComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<KitchenSpikeComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
SubscribeLocalEvent<KitchenSpikeComponent, EntInsertedIntoContainerMessage>(OnEntInsertedIntoContainer);
SubscribeLocalEvent<KitchenSpikeComponent, EntRemovedFromContainerMessage>(OnEntRemovedFromContainer);
SubscribeLocalEvent<KitchenSpikeComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<KitchenSpikeComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<KitchenSpikeComponent, CanDropTargetEvent>(OnCanDrop);
SubscribeLocalEvent<KitchenSpikeComponent, DragDropTargetEvent>(OnDragDrop);
SubscribeLocalEvent<KitchenSpikeComponent, SpikeHookDoAfterEvent>(OnSpikeHookDoAfter);
SubscribeLocalEvent<KitchenSpikeComponent, SpikeUnhookDoAfterEvent>(OnSpikeUnhookDoAfter);
SubscribeLocalEvent<KitchenSpikeComponent, SpikeButcherDoAfterEvent>(OnSpikeButcherDoAfter);
SubscribeLocalEvent<KitchenSpikeComponent, ExaminedEvent>(OnSpikeExamined);
SubscribeLocalEvent<KitchenSpikeComponent, GetVerbsEvent<Verb>>(OnGetVerbs);
SubscribeLocalEvent<KitchenSpikeComponent, DestructionEventArgs>(OnDestruction);
SubscribeLocalEvent<KitchenSpikeVictimComponent, ExaminedEvent>(OnVictimExamined);
// Prevent the victim from doing anything while on the spike.
SubscribeLocalEvent<KitchenSpikeHookedComponent, ChangeDirectionAttemptEvent>(OnAttempt);
SubscribeLocalEvent<KitchenSpikeHookedComponent, UpdateCanMoveEvent>(OnAttempt);
SubscribeLocalEvent<KitchenSpikeHookedComponent, UseAttemptEvent>(OnAttempt);
SubscribeLocalEvent<KitchenSpikeHookedComponent, ThrowAttemptEvent>(OnAttempt);
SubscribeLocalEvent<KitchenSpikeHookedComponent, DropAttemptEvent>(OnAttempt);
SubscribeLocalEvent<KitchenSpikeHookedComponent, AttackAttemptEvent>(OnAttempt);
SubscribeLocalEvent<KitchenSpikeHookedComponent, PickupAttemptEvent>(OnAttempt);
SubscribeLocalEvent<KitchenSpikeHookedComponent, IsEquippingAttemptEvent>(OnAttempt);
SubscribeLocalEvent<KitchenSpikeHookedComponent, IsUnequippingAttemptEvent>(OnAttempt);
}
private void OnCanDrop(EntityUid uid, KitchenSpikeComponent component, ref CanDropTargetEvent args)
private void OnInit(Entity<KitchenSpikeComponent> ent, ref ComponentInit args)
{
if (args.Handled)
ent.Comp.BodyContainer = _containerSystem.EnsureContainer<ContainerSlot>(ent, ent.Comp.ContainerId);
}
private void OnInsertAttempt(Entity<KitchenSpikeComponent> ent, ref ContainerIsInsertingAttemptEvent args)
{
if (args.Cancelled || TryComp<ButcherableComponent>(args.EntityUid, out var butcherable) && butcherable.Type == ButcheringType.Spike)
return;
args.Cancel();
}
private void OnEntInsertedIntoContainer(Entity<KitchenSpikeComponent> ent, ref EntInsertedIntoContainerMessage args)
{
EnsureComp<KitchenSpikeHookedComponent>(args.Entity);
_damageableSystem.TryChangeDamage(args.Entity, ent.Comp.SpikeDamage, true);
// TODO: Add sprites for different species.
_appearanceSystem.SetData(ent.Owner, KitchenSpikeVisuals.Status, KitchenSpikeStatus.Bloody);
}
private void OnEntRemovedFromContainer(Entity<KitchenSpikeComponent> ent, ref EntRemovedFromContainerMessage args)
{
RemComp<KitchenSpikeHookedComponent>(args.Entity);
_damageableSystem.TryChangeDamage(args.Entity, ent.Comp.SpikeDamage, true);
_appearanceSystem.SetData(ent.Owner, KitchenSpikeVisuals.Status, KitchenSpikeStatus.Empty);
}
private void OnInteractHand(Entity<KitchenSpikeComponent> ent, ref InteractHandEvent args)
{
var victim = ent.Comp.BodyContainer.ContainedEntity;
if (args.Handled || !victim.HasValue)
return;
_popupSystem.PopupClient(Loc.GetString("butcherable-need-knife",
("target", Identity.Entity(victim.Value, EntityManager))),
ent,
args.User,
PopupType.Medium);
args.Handled = true;
}
private void OnInteractUsing(Entity<KitchenSpikeComponent> ent, ref InteractUsingEvent args)
{
var victim = ent.Comp.BodyContainer.ContainedEntity;
if (args.Handled || !TryComp<ButcherableComponent>(victim, out var butcherable) || butcherable.SpawnedEntities.Count == 0)
return;
args.Handled = true;
if (!HasComp<ButcherableComponent>(args.Dragged))
if (!TryComp<SharpComponent>(args.Used, out var sharp))
{
args.CanDrop = false;
_popupSystem.PopupClient(Loc.GetString("butcherable-need-knife",
("target", Identity.Entity(victim.Value, EntityManager))),
ent,
args.User,
PopupType.Medium);
return;
}
// TODO: Once we get silicons need to check organic
args.CanDrop = true;
var victimIdentity = Identity.Entity(victim.Value, EntityManager);
_popupSystem.PopupPredicted(Loc.GetString("comp-kitchen-spike-begin-butcher-self", ("victim", victimIdentity)),
Loc.GetString("comp-kitchen-spike-begin-butcher", ("user", Identity.Entity(args.User, EntityManager)), ("victim", victimIdentity)),
ent,
args.User,
PopupType.MediumCaution);
var delay = TimeSpan.FromSeconds(sharp.ButcherDelayModifier * butcherable.ButcherDelay);
if (_mobStateSystem.IsAlive(victim.Value))
delay += ent.Comp.ButcherDelayAlive;
else
delay *= ent.Comp.ButcherModifierDead;
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager,
args.User,
delay,
new SpikeButcherDoAfterEvent(),
ent,
target: victim,
used: args.Used)
{
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = true,
});
}
private void OnCanDrop(Entity<KitchenSpikeComponent> ent, ref CanDropTargetEvent args)
{
if (args.Handled)
return;
args.CanDrop = _containerSystem.CanInsert(args.Dragged, ent.Comp.BodyContainer);
args.Handled = true;
}
private void OnDragDrop(Entity<KitchenSpikeComponent> ent, ref DragDropTargetEvent args)
{
if (args.Handled)
return;
ShowPopups("comp-kitchen-spike-begin-hook-self",
"comp-kitchen-spike-begin-hook-self-other",
"comp-kitchen-spike-begin-hook-other-self",
"comp-kitchen-spike-begin-hook-other",
args.User,
args.Dragged,
ent);
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager,
args.User,
ent.Comp.HookDelay,
new SpikeHookDoAfterEvent(),
ent,
target: args.Dragged)
{
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = true,
});
args.Handled = true;
}
private void OnSpikeHookDoAfter(Entity<KitchenSpikeComponent> ent, ref SpikeHookDoAfterEvent args)
{
if (args.Handled || args.Cancelled || !args.Target.HasValue)
return;
if (_containerSystem.Insert(args.Target.Value, ent.Comp.BodyContainer))
{
ShowPopups("comp-kitchen-spike-hook-self",
"comp-kitchen-spike-hook-self-other",
"comp-kitchen-spike-hook-other-self",
"comp-kitchen-spike-hook-other",
args.User,
args.Target.Value,
ent);
_logger.Add(LogType.Action,
LogImpact.High,
$"{ToPrettyString(args.User):user} put {ToPrettyString(args.Target):target} on the {ToPrettyString(ent):spike}");
_audioSystem.PlayPredicted(ent.Comp.SpikeSound, ent, args.User);
}
args.Handled = true;
}
private void OnSpikeUnhookDoAfter(Entity<KitchenSpikeComponent> ent, ref SpikeUnhookDoAfterEvent args)
{
if (args.Handled || args.Cancelled || !args.Target.HasValue)
return;
if (_containerSystem.Remove(args.Target.Value, ent.Comp.BodyContainer))
{
ShowPopups("comp-kitchen-spike-unhook-self",
"comp-kitchen-spike-unhook-self-other",
"comp-kitchen-spike-unhook-other-self",
"comp-kitchen-spike-unhook-other",
args.User,
args.Target.Value,
ent);
_logger.Add(LogType.Action,
LogImpact.Medium,
$"{ToPrettyString(args.User):user} took {ToPrettyString(args.Target):target} off the {ToPrettyString(ent):spike}");
_audioSystem.PlayPredicted(ent.Comp.SpikeSound, ent, args.User);
}
args.Handled = true;
}
private void OnSpikeButcherDoAfter(Entity<KitchenSpikeComponent> ent, ref SpikeButcherDoAfterEvent args)
{
if (args.Handled || args.Cancelled || !args.Target.HasValue || !args.Used.HasValue || !TryComp<ButcherableComponent>(args.Target, out var butcherable) )
return;
var victimIdentity = Identity.Entity(args.Target.Value, EntityManager);
_popupSystem.PopupPredicted(Loc.GetString("comp-kitchen-spike-butcher-self", ("victim", victimIdentity)),
Loc.GetString("comp-kitchen-spike-butcher", ("user", Identity.Entity(args.User, EntityManager)), ("victim", victimIdentity)),
ent,
args.User,
PopupType.MediumCaution);
// Get a random entry to spawn.
var index = _random.Next(butcherable.SpawnedEntities.Count);
var entry = butcherable.SpawnedEntities[index];
var uid = PredictedSpawnNextToOrDrop(entry.PrototypeId, ent);
_metaDataSystem.SetEntityName(uid,
Loc.GetString("comp-kitchen-spike-meat-name",
("name", Name(uid)),
("victim", args.Target)));
// Decrease the amount since we spawned an entity from that entry.
entry.Amount--;
// Remove the entry if its new amount is zero, or update it.
if (entry.Amount <= 0)
butcherable.SpawnedEntities.RemoveAt(index);
else
butcherable.SpawnedEntities[index] = entry;
Dirty(args.Target.Value, butcherable);
// Gib the victim if there is nothing else to butcher.
if (butcherable.SpawnedEntities.Count == 0)
{
_bodySystem.GibBody(args.Target.Value, true);
_logger.Add(LogType.Gib,
LogImpact.Extreme,
$"{ToPrettyString(args.User):user} finished butchering {ToPrettyString(args.Target):target} on the {ToPrettyString(ent):spike}");
}
else
{
EnsureComp<KitchenSpikeVictimComponent>(args.Target.Value);
_damageableSystem.TryChangeDamage(args.Target, ent.Comp.ButcherDamage, true);
_logger.Add(LogType.Action,
LogImpact.Extreme,
$"{ToPrettyString(args.User):user} butchered {ToPrettyString(args.Target):target} on the {ToPrettyString(ent):spike}");
}
_audioSystem.PlayPredicted(ent.Comp.ButcherSound, ent, args.User);
_popupSystem.PopupClient(Loc.GetString("butcherable-knife-butchered-success",
("target", Identity.Entity(args.Target.Value, EntityManager)),
("knife", args.Used.Value)),
ent,
args.User,
PopupType.Medium);
args.Handled = true;
}
private void OnSpikeExamined(Entity<KitchenSpikeComponent> ent, ref ExaminedEvent args)
{
var victim = ent.Comp.BodyContainer.ContainedEntity;
if (!victim.HasValue)
return;
// Show it at the end of the examine so it looks good.
args.PushMarkup(Loc.GetString("comp-kitchen-spike-hooked", ("victim", Identity.Entity(victim.Value, EntityManager))), -1);
args.PushMessage(_examineSystem.GetExamineText(victim.Value, args.Examiner), -2);
}
private void OnGetVerbs(Entity<KitchenSpikeComponent> ent, ref GetVerbsEvent<Verb> args)
{
var victim = ent.Comp.BodyContainer.ContainedEntity;
if (!victim.HasValue || !_containerSystem.CanRemove(victim.Value, ent.Comp.BodyContainer))
return;
var user = args.User;
args.Verbs.Add(new Verb()
{
Text = Loc.GetString("comp-kitchen-spike-unhook-verb"),
Act = () => TryUnhook(ent, user, victim.Value),
Impact = LogImpact.Medium,
});
}
private void OnDestruction(Entity<KitchenSpikeComponent> ent, ref DestructionEventArgs args)
{
_containerSystem.EmptyContainer(ent.Comp.BodyContainer, destination: Transform(ent).Coordinates);
}
private void OnVictimExamined(Entity<KitchenSpikeVictimComponent> ent, ref ExaminedEvent args)
{
args.PushMarkup(Loc.GetString("comp-kitchen-spike-victim-examine", ("target", Identity.Entity(ent, EntityManager))));
}
private static void OnAttempt(EntityUid uid, KitchenSpikeHookedComponent component, CancellableEntityEventArgs args)
{
args.Cancel();
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = AllEntityQuery<KitchenSpikeComponent>();
while (query.MoveNext(out var uid, out var kitchenSpike))
{
if (kitchenSpike.NextDamage > _gameTiming.CurTime)
continue;
kitchenSpike.NextDamage += kitchenSpike.DamageInterval;
Dirty(uid, kitchenSpike);
_damageableSystem.TryChangeDamage(kitchenSpike.BodyContainer.ContainedEntity, kitchenSpike.TimeDamage, true);
}
}
/// <summary>
/// A helper method to show predicted popups that can be targeted towards yourself or somebody else.
/// </summary>
private void ShowPopups(string selfLocMessageSelf,
string selfLocMessageOthers,
string locMessageSelf,
string locMessageOthers,
EntityUid user,
EntityUid victim,
EntityUid hook)
{
string messageSelf, messageOthers;
var victimIdentity = Identity.Entity(victim, EntityManager);
if (user == victim)
{
messageSelf = Loc.GetString(selfLocMessageSelf, ("hook", hook));
messageOthers = Loc.GetString(selfLocMessageOthers, ("victim", victimIdentity), ("hook", hook));
}
else
{
messageSelf = Loc.GetString(locMessageSelf, ("victim", victimIdentity), ("hook", hook));
messageOthers = Loc.GetString(locMessageOthers,
("user", Identity.Entity(user, EntityManager)),
("victim", victimIdentity),
("hook", hook));
}
_popupSystem.PopupPredicted(messageSelf, messageOthers, hook, user, PopupType.MediumCaution);
}
/// <summary>
/// Tries to unhook the victim.
/// </summary>
private void TryUnhook(Entity<KitchenSpikeComponent> ent, EntityUid user, EntityUid target)
{
ShowPopups("comp-kitchen-spike-begin-unhook-self",
"comp-kitchen-spike-begin-unhook-self-other",
"comp-kitchen-spike-begin-unhook-other-self",
"comp-kitchen-spike-begin-unhook-other",
user,
target,
ent);
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager,
user,
ent.Comp.UnhookDelay,
new SpikeUnhookDoAfterEvent(),
ent,
target: target)
{
BreakOnDamage = user != target,
BreakOnMove = true,
});
}
}
[Serializable, NetSerializable]
public sealed partial class SpikeDoAfterEvent : SimpleDoAfterEvent
{
}
public sealed partial class SpikeHookDoAfterEvent : SimpleDoAfterEvent;
[Serializable, NetSerializable]
public sealed partial class SpikeUnhookDoAfterEvent : SimpleDoAfterEvent;
[Serializable, NetSerializable]
public sealed partial class SpikeButcherDoAfterEvent : SimpleDoAfterEvent;

View File

@@ -26,10 +26,14 @@ namespace Content.Shared.Lathe
// Otherwise the material arbitrage test and/or LatheSystem.GetAllBaseRecipes needs to be updated
/// <summary>
/// The lathe's construction queue
/// The lathe's construction queue.
/// </summary>
/// <remarks>
/// This is a LinkedList to allow for constant time insertion/deletion (vs a List), and more efficient
/// moves (vs a Queue).
/// </remarks>
[DataField]
public Queue<ProtoId<LatheRecipePrototype>> Queue = new();
public LinkedList<LatheRecipeBatch> Queue = new();
/// <summary>
/// The sound that plays when the lathe is producing an item, if any
@@ -97,6 +101,21 @@ namespace Content.Shared.Lathe
}
}
[Serializable]
public sealed partial class LatheRecipeBatch
{
public ProtoId<LatheRecipePrototype> Recipe;
public int ItemsPrinted;
public int ItemsRequested;
public LatheRecipeBatch(ProtoId<LatheRecipePrototype> recipe, int itemsPrinted, int itemsRequested)
{
Recipe = recipe;
ItemsPrinted = itemsPrinted;
ItemsRequested = itemsRequested;
}
}
/// <summary>
/// Event raised on a lathe when it starts producing a recipe.
/// </summary>

View File

@@ -10,11 +10,11 @@ public sealed class LatheUpdateState : BoundUserInterfaceState
{
public List<ProtoId<LatheRecipePrototype>> Recipes;
public ProtoId<LatheRecipePrototype>[] Queue;
public LatheRecipeBatch[] Queue;
public ProtoId<LatheRecipePrototype>? CurrentlyProducing;
public LatheUpdateState(List<ProtoId<LatheRecipePrototype>> recipes, ProtoId<LatheRecipePrototype>[] queue, ProtoId<LatheRecipePrototype>? currentlyProducing = null)
public LatheUpdateState(List<ProtoId<LatheRecipePrototype>> recipes, LatheRecipeBatch[] queue, ProtoId<LatheRecipePrototype>? currentlyProducing = null)
{
Recipes = recipes;
Queue = queue;
@@ -46,6 +46,33 @@ public sealed class LatheQueueRecipeMessage : BoundUserInterfaceMessage
}
}
/// <summary>
/// Sent to the server to remove a batch from the queue.
/// </summary>
[Serializable, NetSerializable]
public sealed class LatheDeleteRequestMessage(int index) : BoundUserInterfaceMessage
{
public int Index = index;
}
/// <summary>
/// Sent to the server to move the position of a batch in the queue.
/// </summary>
[Serializable, NetSerializable]
public sealed class LatheMoveRequestMessage(int index, int change) : BoundUserInterfaceMessage
{
public int Index = index;
public int Change = change;
}
/// <summary>
/// Sent to the server to stop producing the current item.
/// </summary>
[Serializable, NetSerializable]
public sealed class LatheAbortFabricationMessage() : BoundUserInterfaceMessage
{
}
[NetSerializable, Serializable]
public enum LatheUiKey
{

View File

@@ -0,0 +1,81 @@
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Serialization.Markdown.Validation;
using Robust.Shared.Serialization.TypeSerializers.Interfaces;
namespace Content.Shared.Lathe;
/// <summary>
/// Handles reading, writing, and validation for linked lists of prototypes.
/// </summary>
/// <typeparam name="T">The type of prototype this linked list represents</typeparam>
/// <remarks>
/// This is in the Content.Shared.Lathe namespace as there are no other LinkedList ProtoId instances.
/// </remarks>
[TypeSerializer]
public sealed class LinkedListSerializer<T> : ITypeSerializer<LinkedList<T>, SequenceDataNode>, ITypeCopier<LinkedList<T>> where T : class
{
public ValidationNode Validate(ISerializationManager serializationManager, SequenceDataNode node,
IDependencyCollection dependencies, ISerializationContext? context = null)
{
var list = new List<ValidationNode>();
foreach (var elem in node.Sequence)
{
list.Add(serializationManager.ValidateNode<T>(elem, context));
}
return new ValidatedSequenceNode(list);
}
public DataNode Write(ISerializationManager serializationManager, LinkedList<T> value,
IDependencyCollection dependencies,
bool alwaysWrite = false,
ISerializationContext? context = null)
{
var sequence = new SequenceDataNode();
foreach (var elem in value)
{
sequence.Add(serializationManager.WriteValue(elem, alwaysWrite, context));
}
return sequence;
}
LinkedList<T> ITypeReader<LinkedList<T>, SequenceDataNode>.Read(ISerializationManager serializationManager,
SequenceDataNode node,
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context, ISerializationManager.InstantiationDelegate<LinkedList<T>>? instanceProvider)
{
var list = instanceProvider != null ? instanceProvider() : new LinkedList<T>();
foreach (var dataNode in node.Sequence)
{
list.AddLast(serializationManager.Read<T>(dataNode, hookCtx, context));
}
return list;
}
public void CopyTo(
ISerializationManager serializationManager,
LinkedList<T> source,
ref LinkedList<T> target,
IDependencyCollection dependencies,
SerializationHookContext hookCtx,
ISerializationContext? context = null)
{
target.Clear();
using var enumerator = source.GetEnumerator();
while (enumerator.MoveNext())
{
var current = enumerator.Current;
target.AddLast(current);
}
}
}

View File

@@ -22,6 +22,7 @@ public abstract class SharedLatheSystem : EntitySystem
[Dependency] private readonly EmagSystem _emag = default!;
public readonly Dictionary<string, List<LatheRecipePrototype>> InverseRecipes = new();
public const int MaxItemsPerRequest = 10_000;
public override void Initialize()
{
@@ -86,6 +87,8 @@ public abstract class SharedLatheSystem : EntitySystem
return false;
if (!HasRecipe(uid, recipe, component))
return false;
if (amount <= 0)
return false;
foreach (var (material, needed) in recipe.Materials)
{

View File

@@ -51,11 +51,12 @@ public sealed class LockSystem : EntitySystem
SubscribeLocalEvent<LockComponent, LockDoAfter>(OnDoAfterLock);
SubscribeLocalEvent<LockComponent, UnlockDoAfter>(OnDoAfterUnlock);
SubscribeLocalEvent<LockComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened); //CrystallEdge Lock System Adapt
SubscribeLocalEvent<LockComponent, StorageInteractAttemptEvent>(OnStorageInteractAttempt);
SubscribeLocalEvent<LockedWiresPanelComponent, LockToggleAttemptEvent>(OnLockToggleAttempt);
SubscribeLocalEvent<LockedWiresPanelComponent, AttemptChangePanelEvent>(OnAttemptChangePanel);
SubscribeLocalEvent<LockedAnchorableComponent, UnanchorAttemptEvent>(OnUnanchorAttempt);
SubscribeLocalEvent<LockedStorageComponent, StorageInteractAttemptEvent>(OnStorageInteractAttempt);
SubscribeLocalEvent<UIRequiresLockComponent, ActivatableUIOpenAttemptEvent>(OnUIOpenAttempt);
SubscribeLocalEvent<UIRequiresLockComponent, LockToggledEvent>(LockToggled);
@@ -378,9 +379,9 @@ public sealed class LockSystem : EntitySystem
TryUnlock(uid, args.User, skipDoAfter: true);
}
private void OnStorageInteractAttempt(Entity<LockComponent> ent, ref StorageInteractAttemptEvent args)
private void OnStorageInteractAttempt(Entity<LockedStorageComponent> ent, ref StorageInteractAttemptEvent args)
{
if (ent.Comp.Locked)
if (IsLocked(ent.Owner))
args.Cancelled = true;
}

View File

@@ -0,0 +1,10 @@
using Content.Shared.Storage;
using Robust.Shared.GameStates;
namespace Content.Shared.Lock;
/// <summary>
/// Prevents using an entity's <see cref="StorageComponent"/> if its <see cref="LockComponent"/> is locked.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class LockedStorageComponent : Component;

View File

@@ -1,5 +1,6 @@
using Content.Shared.Actions;
using Content.Shared.Actions.Components;
using Content.Shared.Actions.Components;
using Content.Shared.Charges.Components;
using Content.Shared.Charges.Systems;
using Content.Shared.DoAfter;
using Content.Shared.Interaction.Events;
@@ -29,14 +30,19 @@ public sealed class SpellbookSystem : EntitySystem
{
foreach (var (id, charges) in ent.Comp.SpellActions)
{
var spell = _actionContainer.AddAction(ent, id);
if (spell == null)
var action = _actionContainer.AddAction(ent, id);
if (action is not { } spell)
continue;
// Null means infinite charges.
if (charges is { } count)
_sharedCharges.SetCharges(spell.Value, count);
ent.Comp.Spells.Add(spell.Value);
{
EnsureComp<LimitedChargesComponent>(spell, out var chargeComp);
_sharedCharges.SetMaxCharges((spell, chargeComp), count);
_sharedCharges.SetCharges((spell, chargeComp), count);
}
ent.Comp.Spells.Add(spell);
}
}
@@ -75,9 +81,13 @@ public sealed class SpellbookSystem : EntitySystem
foreach (var (id, charges) in ent.Comp.SpellActions)
{
EntityUid? actionId = null;
if (_actions.AddAction(args.Args.User, ref actionId, id)
&& charges is { } count) // Null means infinite charges
_sharedCharges.SetCharges(actionId.Value, count);
if (!_actions.AddAction(args.Args.User, ref actionId, id)
|| charges is not { } count // Null means infinite charges
|| !TryComp<LimitedChargesComponent>(actionId, out var chargeComp))
continue;
_sharedCharges.SetMaxCharges((actionId.Value, chargeComp), count);
_sharedCharges.SetCharges((actionId.Value, chargeComp), count);
}
}

View File

@@ -14,6 +14,7 @@ using Content.Shared.Mech.Equipment.Components;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.Popups;
using Content.Shared.Storage.Components;
using Content.Shared.Weapons.Melee;
using Content.Shared.Whitelist;
using Robust.Shared.Containers;
@@ -48,6 +49,7 @@ public abstract partial class SharedMechSystem : EntitySystem
SubscribeLocalEvent<MechComponent, UserActivateInWorldEvent>(RelayInteractionEvent);
SubscribeLocalEvent<MechComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<MechComponent, DestructionEventArgs>(OnDestruction);
SubscribeLocalEvent<MechComponent, EntityStorageIntoContainerAttemptEvent>(OnEntityStorageDump);
SubscribeLocalEvent<MechComponent, GetAdditionalAccessEvent>(OnGetAdditionalAccess);
SubscribeLocalEvent<MechComponent, DragDropTargetEvent>(OnDragDrop);
SubscribeLocalEvent<MechComponent, CanDropTargetEvent>(OnCanDragDrop);
@@ -104,6 +106,12 @@ public abstract partial class SharedMechSystem : EntitySystem
BreakMech(uid, component);
}
private void OnEntityStorageDump(Entity<MechComponent> entity, ref EntityStorageIntoContainerAttemptEvent args)
{
// There's no reason we should dump into /any/ of the mech's containers.
args.Cancelled = true;
}
private void OnGetAdditionalAccess(EntityUid uid, MechComponent component, ref GetAdditionalAccessEvent args)
{
var pilot = component.PilotSlot.ContainedEntity;
@@ -147,7 +155,7 @@ public abstract partial class SharedMechSystem : EntitySystem
}
/// <summary>
/// Destroys the mech, removing the user and ejecting all installed equipment.
/// Destroys the mech, removing the user and ejecting anything contained.
/// </summary>
/// <param name="uid"></param>
/// <param name="component"></param>
@@ -237,14 +245,19 @@ public abstract partial class SharedMechSystem : EntitySystem
/// <param name="toRemove"></param>
/// <param name="component"></param>
/// <param name="equipmentComponent"></param>
/// <param name="forced">Whether or not the removal can be cancelled</param>
/// <param name="forced">
/// Whether or not the removal can be cancelled, and if non-mech equipment should be ejected.
/// </param>
public void RemoveEquipment(EntityUid uid, EntityUid toRemove, MechComponent? component = null,
MechEquipmentComponent? equipmentComponent = null, bool forced = false)
{
if (!Resolve(uid, ref component))
return;
if (!Resolve(toRemove, ref equipmentComponent))
// When forced, we also want to handle the possibility that the "equipment" isn't actually equipment.
// This /shouldn't/ be possible thanks to OnEntityStorageDump, but there's been quite a few regressions
// with entities being hardlock stuck inside mechs.
if (!Resolve(toRemove, ref equipmentComponent) && !forced)
return;
if (!forced)
@@ -261,7 +274,9 @@ public abstract partial class SharedMechSystem : EntitySystem
if (component.CurrentSelectedEquipment == toRemove)
CycleEquipment(uid, component);
equipmentComponent.EquipmentOwner = null;
if (forced && equipmentComponent != null)
equipmentComponent.EquipmentOwner = null;
_container.Remove(toRemove, component.EquipmentContainer);
UpdateUserInterface(uid, component);
}

View File

@@ -43,7 +43,7 @@ public sealed partial class HealingComponent : Component
/// How long it takes to apply the damage.
/// </summary>
[DataField, AutoNetworkedField]
public float Delay = 3f;
public TimeSpan Delay = TimeSpan.FromSeconds(3f);
/// <summary>
/// Delay multiplier when healing yourself.

View File

@@ -112,9 +112,17 @@ public sealed class HealingSystem : EntitySystem
// Logic to determine the whether or not to repeat the healing action
args.Repeat = HasDamage((args.Used.Value, healing), target) && !dontRepeat;
if (!args.Repeat && !dontRepeat)
_popupSystem.PopupClient(Loc.GetString("medical-item-finished-using", ("item", args.Used)), target.Owner, args.User);
args.Handled = true;
if (!args.Repeat)
{
_popupSystem.PopupClient(Loc.GetString("medical-item-finished-using", ("item", args.Used)), target.Owner, args.User);
return;
}
// Update our self heal delay so it shortens as we heal more damage.
if (args.User == target.Owner)
args.Args.Delay = healing.Delay * GetScaledHealingPenalty(target.Owner, healing.SelfHealPenaltyMultiplier);
}
private bool HasDamage(Entity<HealingComponent> healing, Entity<DamageableComponent> target)
@@ -203,7 +211,7 @@ public sealed class HealingSystem : EntitySystem
var delay = isNotSelf
? healing.Comp.Delay
: healing.Comp.Delay * GetScaledHealingPenalty(healing);
: healing.Comp.Delay * GetScaledHealingPenalty(target, healing.Comp.SelfHealPenaltyMultiplier);
var doAfterEventArgs =
new DoAfterArgs(EntityManager, user, delay, new HealingDoAfterEvent(), target, target: target, used: healing)
@@ -222,21 +230,21 @@ public sealed class HealingSystem : EntitySystem
/// <summary>
/// Scales the self-heal penalty based on the amount of damage taken
/// </summary>
/// <param name="uid"></param>
/// <param name="component"></param>
/// <returns></returns>
public float GetScaledHealingPenalty(Entity<HealingComponent> healing)
/// <param name="ent">Entity we're healing</param>
/// <param name="mod">Maximum modifier we can have.</param>
/// <returns>Modifier we multiply our healing time by</returns>
public float GetScaledHealingPenalty(Entity<DamageableComponent?, MobThresholdsComponent?> ent, float mod)
{
var output = healing.Comp.Delay;
if (!TryComp<MobThresholdsComponent>(healing, out var mobThreshold) ||
!TryComp<DamageableComponent>(healing, out var damageable))
return output;
if (!_mobThresholdSystem.TryGetThresholdForState(healing, MobState.Critical, out var amount, mobThreshold))
if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2, false))
return mod;
if (!_mobThresholdSystem.TryGetThresholdForState(ent, MobState.Critical, out var amount, ent.Comp2))
return 1;
var percentDamage = (float)(damageable.TotalDamage / amount);
var percentDamage = (float)(ent.Comp1.TotalDamage / amount);
//basically make it scale from 1 to the multiplier.
var modifier = percentDamage * (healing.Comp.SelfHealPenaltyMultiplier - 1) + 1;
return Math.Max(modifier, 1);
var output = percentDamage * (mod - 1) + 1;
return Math.Max(output, 1);
}
}

View File

@@ -1,8 +1,7 @@
using Content.Shared.GameTicking;
using Content.Shared.Mind.Components;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Shared.Mind;
@@ -100,10 +99,16 @@ public sealed partial class MindComponent : Component
public bool PreventSuicide { get; set; }
/// <summary>
/// Mind Role Entities belonging to this Mind
/// Mind Role Entities belonging to this Mind are stored in this container.
/// </summary>
[DataField, AutoNetworkedField]
public List<EntityUid> MindRoles = new List<EntityUid>();
[ViewVariables]
public Container MindRoleContainer = default!;
/// <summary>
/// The id for the MindRoleContainer.
/// </summary>
[ViewVariables]
public const string MindRoleContainerId = "mind_roles";
/// <summary>
/// The mind's current antagonist/special role, or lack thereof;

View File

@@ -23,7 +23,7 @@ public abstract partial class SharedMindSystem : EntitySystem
{
RaiseLocalEvent(mindId, ref ev);
foreach (var role in mindComp.MindRoles)
foreach (var role in mindComp.MindRoleContainer.ContainedEntities)
RaiseLocalEvent(role, ref ev);
}
}
@@ -36,7 +36,7 @@ public abstract partial class SharedMindSystem : EntitySystem
{
RaiseLocalEvent(mindId, ref ev);
foreach (var role in mindComp.MindRoles)
foreach (var role in mindComp.MindRoleContainer.ContainedEntities)
RaiseLocalEvent(role, ref ev);
}

View File

@@ -15,8 +15,8 @@ using Content.Shared.Mobs.Systems;
using Content.Shared.Objectives.Systems;
using Content.Shared.Players;
using Content.Shared.Speech;
using Content.Shared.Whitelist;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
@@ -36,6 +36,7 @@ public abstract partial class SharedMindSystem : EntitySystem
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;
[Dependency] private readonly MetaDataSystem _metadata = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[ViewVariables]
protected readonly Dictionary<NetUserId, EntityUid> UserMinds = new();
@@ -64,6 +65,8 @@ public abstract partial class SharedMindSystem : EntitySystem
private void OnMindStartup(EntityUid uid, MindComponent component, ComponentStartup args)
{
component.MindRoleContainer = _container.EnsureContainer<Container>(uid, MindComponent.MindRoleContainerId);
if (component.UserId == null)
return;

View File

@@ -8,7 +8,7 @@ using Robust.Shared.Timing;
namespace Content.Shared.Mindshield.FakeMindShield;
public sealed class SharedFakeMindShieldSystem : EntitySystem
public sealed class FakeMindShieldSystem : EntitySystem
{
[Dependency] private readonly SharedActionsSystem _actions = default!;
[Dependency] private readonly TagSystem _tag = default!;
@@ -24,9 +24,11 @@ public sealed class SharedFakeMindShieldSystem : EntitySystem
SubscribeLocalEvent<FakeMindShieldComponent, ChameleonControllerOutfitSelectedEvent>(OnChameleonControllerOutfitSelected);
}
private void OnToggleMindshield(EntityUid uid, FakeMindShieldComponent comp, FakeMindShieldToggleEvent toggleEvent)
private void OnToggleMindshield(EntityUid uid, FakeMindShieldComponent comp, FakeMindShieldToggleEvent args)
{
comp.IsEnabled = !comp.IsEnabled;
args.Toggle = true;
args.Handled = true;
Dirty(uid, comp);
}

View File

@@ -1,45 +0,0 @@
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;
// TODO: is there a reason this cant set ev.Toggle = true;
_actionsSystem.SetToggled((ev.Action, 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);
}
}

View File

@@ -0,0 +1,9 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Morgue.Components;
/// <summary>
/// Used to track actively cooking crematoriums.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ActiveCrematoriumComponent : Component;

View File

@@ -0,0 +1,42 @@
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Morgue.Components;
/// <summary>
/// Allows an entity storage to dispose bodies by turning them into ash.
/// </summary>
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState, AutoGenerateComponentPause]
public sealed partial class CrematoriumComponent : Component
{
/// <summary>
/// The entity to spawn when something was burned.
/// </summary>
[DataField, AutoNetworkedField]
public EntProtoId LeftOverProtoId = "Ash";
/// <summary>
/// The time it takes to cremate something.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan CookTime = TimeSpan.FromSeconds(5);
/// <summary>
/// The timestamp at which cremating is finished.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoNetworkedField, AutoPausedField]
public TimeSpan ActiveUntil = TimeSpan.Zero;
[DataField]
public SoundSpecifier CremateStartSound = new SoundPathSpecifier("/Audio/Items/Lighters/lighter1.ogg");
[DataField]
public SoundSpecifier CrematingSound = new SoundPathSpecifier("/Audio/Effects/burning.ogg");
[DataField]
public SoundSpecifier CremateFinishSound = new SoundPathSpecifier("/Audio/Machines/ding.ogg");
}

View File

@@ -1,6 +1,10 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Morgue.Components;
[RegisterComponent]
public sealed partial class EntityStorageLayingDownOverrideComponent : Component
{
}
/// <summary>
/// Makes an entity storage only accept entities that are laying down.
/// This is true for mobs that are crit, dead or crawling.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class EntityStorageLayingDownOverrideComponent : Component;

View File

@@ -1,26 +1,38 @@
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Morgue.Components;
/// <summary>
/// When added to an entity storage this component will keep track of the mind status of the player inside.
/// </summary>
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState, AutoGenerateComponentPause]
public sealed partial class MorgueComponent : Component
{
/// <summary>
/// Whether or not the morgue beeps if a living player is inside.
/// Whether or not the morgue beeps if a living player is inside.
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public bool DoSoulBeep = true;
[DataField]
public float AccumulatedFrameTime = 0f;
/// <summary>
/// The timestamp for the next beep.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan NextBeep = TimeSpan.Zero;
/// <summary>
/// The amount of time between each beep.
/// The amount of time between each beep.
/// </summary>
[DataField]
public float BeepTime = 10f;
public TimeSpan BeepTime = TimeSpan.FromSeconds(10);
/// <summary>
/// The beep sound to play.
/// </summary>
[DataField]
public SoundSpecifier OccupantHasSoulAlarmSound = new SoundPathSpecifier("/Audio/Weapons/Guns/EmptyAlarm/smg_empty_alarm.ogg");
}

View File

@@ -21,7 +21,7 @@ public sealed class EntityStorageLayingDownOverrideSystem : EntitySystem
{
// Explicitly check for standing state component, as entities without it will return false for IsDown()
// which prevents inserting any kind of non-mobs into this container (which is unintended)
if (TryComp<StandingStateComponent>(ent, out var standingState) && !_standing.IsDown(ent, standingState))
if (TryComp<StandingStateComponent>(ent, out var standingState) && !_standing.IsDown((ent, standingState)))
args.Contents.Remove(ent);
}
}

View File

@@ -0,0 +1,170 @@
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.Mind;
using Content.Shared.Morgue.Components;
using Content.Shared.Popups;
using Content.Shared.Standing;
using Content.Shared.Storage;
using Content.Shared.Storage.Components;
using Content.Shared.Storage.EntitySystems;
using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Network;
using Robust.Shared.Timing;
namespace Content.Shared.Morgue;
public abstract class SharedCrematoriumSystem : EntitySystem
{
[Dependency] protected readonly SharedEntityStorageSystem EntityStorage = default!;
[Dependency] protected readonly SharedPopupSystem Popup = default!;
[Dependency] protected readonly StandingStateSystem Standing = default!;
[Dependency] protected readonly SharedMindSystem Mind = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CrematoriumComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<CrematoriumComponent, GetVerbsEvent<AlternativeVerb>>(AddCremateVerb);
SubscribeLocalEvent<ActiveCrematoriumComponent, StorageOpenAttemptEvent>(OnAttemptOpen);
}
private void OnExamine(Entity<CrematoriumComponent> ent, ref ExaminedEvent args)
{
if (!TryComp<AppearanceComponent>(ent, out var appearance))
return;
using (args.PushGroup(nameof(CrematoriumComponent)))
{
if (_appearance.TryGetData<bool>(ent.Owner, CrematoriumVisuals.Burning, out var isBurning, appearance) &&
isBurning)
{
args.PushMarkup(Loc.GetString("crematorium-entity-storage-component-on-examine-details-is-burning",
("owner", ent.Owner)));
}
if (_appearance.TryGetData<bool>(ent.Owner, StorageVisuals.HasContents, out var hasContents, appearance) &&
hasContents)
{
args.PushMarkup(Loc.GetString("crematorium-entity-storage-component-on-examine-details-has-contents"));
}
else
{
args.PushMarkup(Loc.GetString("crematorium-entity-storage-component-on-examine-details-empty"));
}
}
}
private void OnAttemptOpen(Entity<ActiveCrematoriumComponent> ent, ref StorageOpenAttemptEvent args)
{
args.Cancelled = true;
}
private void AddCremateVerb(EntityUid uid, CrematoriumComponent component, GetVerbsEvent<AlternativeVerb> args)
{
if (!TryComp<EntityStorageComponent>(uid, out var storage))
return;
if (!args.CanAccess || !args.CanInteract || args.Hands == null || storage.Open)
return;
if (HasComp<ActiveCrematoriumComponent>(uid))
return;
AlternativeVerb verb = new()
{
Text = Loc.GetString("cremate-verb-get-data-text"),
// TODO VERB ICON add flame/burn symbol?
Act = () => TryCremate((uid, component, storage), args.User),
Impact = LogImpact.High // could be a body? or evidence? I dunno.
};
args.Verbs.Add(verb);
}
/// <summary>
/// Start the cremation.
/// </summary>
public bool Cremate(Entity<CrematoriumComponent?> ent, EntityUid? user = null)
{
if (!Resolve(ent, ref ent.Comp))
return false;
if (HasComp<ActiveCrematoriumComponent>(ent))
return false;
_audio.PlayPredicted(ent.Comp.CremateStartSound, ent.Owner, user);
_audio.PlayPredicted(ent.Comp.CrematingSound, ent.Owner, user);
_appearance.SetData(ent.Owner, CrematoriumVisuals.Burning, true);
AddComp<ActiveCrematoriumComponent>(ent);
ent.Comp.ActiveUntil = _timing.CurTime + ent.Comp.CookTime;
Dirty(ent);
return true;
}
/// <summary>
/// Try to start to start the cremation.
/// Only works when the crematorium is closed and there are entities inside.
/// </summary>
public bool TryCremate(Entity<CrematoriumComponent?, EntityStorageComponent?> ent, EntityUid? user = null)
{
if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2))
return false;
if (ent.Comp2.Open || ent.Comp2.Contents.ContainedEntities.Count < 1)
return false;
return Cremate((ent.Owner, ent.Comp1), user);
}
/// <summary>
/// Finish the cremation process.
/// This will delete the entities inside and spawn ash.
/// </summary>
private void FinishCooking(Entity<CrematoriumComponent?, EntityStorageComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2))
return;
_appearance.SetData(ent.Owner, CrematoriumVisuals.Burning, false);
RemComp<ActiveCrematoriumComponent>(ent);
if (ent.Comp2.Contents.ContainedEntities.Count > 0)
{
for (var i = ent.Comp2.Contents.ContainedEntities.Count - 1; i >= 0; i--)
{
var item = ent.Comp2.Contents.ContainedEntities[i];
_container.Remove(item, ent.Comp2.Contents);
PredictedDel(item);
}
PredictedTrySpawnInContainer(ent.Comp1.LeftOverProtoId, ent.Owner, ent.Comp2.Contents.ID, out _);
}
EntityStorage.OpenStorage(ent.Owner, ent.Comp2);
if (_net.IsServer) // can't predict without the user
_audio.PlayPvs(ent.Comp1.CremateFinishSound, ent.Owner);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var curTime = _timing.CurTime;
var query = EntityQueryEnumerator<ActiveCrematoriumComponent, CrematoriumComponent>();
while (query.MoveNext(out var uid, out _, out var crematorium))
{
if (curTime < crematorium.ActiveUntil)
continue;
FinishCooking((uid, crematorium, null));
}
}
}

View File

@@ -0,0 +1,83 @@
using Content.Shared.Mobs.Components;
using Content.Shared.Storage.Components;
using Content.Shared.Examine;
using Content.Shared.Morgue.Components;
using Robust.Shared.Player;
namespace Content.Shared.Morgue;
public abstract class SharedMorgueSystem : EntitySystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MorgueComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<MorgueComponent, StorageAfterCloseEvent>(OnClosed);
SubscribeLocalEvent<MorgueComponent, StorageAfterOpenEvent>(OnOpened);
}
/// <summary>
/// Handles the examination text for looking at a morgue.
/// </summary>
private void OnExamine(Entity<MorgueComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
_appearance.TryGetData<MorgueContents>(ent.Owner, MorgueVisuals.Contents, out var contents);
var text = contents switch
{
MorgueContents.HasSoul => "morgue-entity-storage-component-on-examine-details-body-has-soul",
MorgueContents.HasContents => "morgue-entity-storage-component-on-examine-details-has-contents",
MorgueContents.HasMob => "morgue-entity-storage-component-on-examine-details-body-has-no-soul",
_ => "morgue-entity-storage-component-on-examine-details-empty"
};
args.PushMarkup(Loc.GetString(text));
}
private void OnClosed(Entity<MorgueComponent> ent, ref StorageAfterCloseEvent args)
{
CheckContents(ent.Owner, ent.Comp);
}
private void OnOpened(Entity<MorgueComponent> ent, ref StorageAfterOpenEvent args)
{
CheckContents(ent.Owner, ent.Comp);
}
/// <summary>
/// Updates data in case something died/got deleted in the morgue.
/// </summary>
public void CheckContents(EntityUid uid, MorgueComponent? morgue = null, EntityStorageComponent? storage = null, AppearanceComponent? app = null)
{
if (!Resolve(uid, ref morgue, ref storage, ref app))
return;
if (storage.Contents.ContainedEntities.Count == 0)
{
_appearance.SetData(uid, MorgueVisuals.Contents, MorgueContents.Empty, app);
return;
}
var hasMob = false;
foreach (var ent in storage.Contents.ContainedEntities)
{
if (!hasMob && HasComp<MobStateComponent>(ent))
hasMob = true;
if (HasComp<ActorComponent>(ent))
{
_appearance.SetData(uid, MorgueVisuals.Contents, MorgueContents.HasSoul, app);
return;
}
}
_appearance.SetData(uid, MorgueVisuals.Contents, hasMob ? MorgueContents.HasMob : MorgueContents.HasContents, app);
}
}

View File

@@ -0,0 +1,16 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Movement.Components;
/// <summary>
/// Marker component given to the users of the <see cref="JumpAbilityComponent"/> if they are meant to collide with environment.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ActiveLeaperComponent : Component
{
/// <summary>
/// The duration to stun the owner on collide with environment.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan KnockdownDuration;
}

View File

@@ -2,6 +2,7 @@ using Content.Shared.Actions;
using Content.Shared.Movement.Systems;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Movement.Components;
@@ -13,6 +14,18 @@ namespace Content.Shared.Movement.Components;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedJumpAbilitySystem))]
public sealed partial class JumpAbilityComponent : Component
{
/// <summary>
/// The action prototype that allows you to jump.
/// </summary>
[DataField]
public EntProtoId Action = "ActionGravityJump";
/// <summary>
/// Entity to hold the action prototype.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? ActionEntity;
/// <summary>
/// How far you will jump (in tiles).
/// </summary>
@@ -25,11 +38,29 @@ public sealed partial class JumpAbilityComponent : Component
[DataField, AutoNetworkedField]
public float JumpThrowSpeed = 10f;
/// <summary>
/// Whether this entity can collide with another entity, leading to it getting knocked down.
/// </summary>
[DataField, AutoNetworkedField]
public bool CanCollide = false;
/// <summary>
/// The duration of the knockdown in case of a collision from CanCollide.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan CollideKnockdown = TimeSpan.FromSeconds(2);
/// <summary>
/// This gets played whenever the jump action is used.
/// </summary>
[DataField, AutoNetworkedField]
public SoundSpecifier? JumpSound;
/// <summary>
/// The popup to show if the entity is unable to perform a jump.
/// </summary>
[DataField, AutoNetworkedField]
public LocId? JumpFailedPopup = "jump-ability-failure";
}
public sealed partial class GravityJumpEvent : InstantActionEvent;

View File

@@ -1,34 +1,17 @@
using Content.Shared.Clothing;
using Content.Shared.Gravity;
using Content.Shared.Inventory;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Serialization;
namespace Content.Shared.Movement.Components
{
/// <summary>
/// Ignores gravity entirely.
/// </summary>
[RegisterComponent, NetworkedComponent]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class MovementIgnoreGravityComponent : Component
{
/// <summary>
/// Whether or not gravity is on or off for this object.
/// Whether gravity is on or off for this object. This will always override the current Gravity State.
/// </summary>
[DataField("gravityState")] public bool Weightless = false;
}
[NetSerializable, Serializable]
public sealed class MovementIgnoreGravityComponentState : ComponentState
{
[DataField, AutoNetworkedField]
public bool Weightless;
public MovementIgnoreGravityComponentState(MovementIgnoreGravityComponent component)
{
Weightless = component.Weightless;
}
}
}

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