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

# Conflicts:
#	.github/CODEOWNERS
#	Content.Client/UserInterface/Systems/Actions/Controls/ActionButton.cs
#	Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs
#	Content.Server/Chat/Systems/ChatSystem.cs
#	Content.Server/Explosion/EntitySystems/TriggerSystem.cs
#	Content.Server/Nutrition/EntitySystems/SliceableFoodSystem.cs
#	Content.Shared/Lock/LockSystem.cs
#	Content.Shared/Nutrition/Components/FoodComponent.cs
#	Content.Shared/Speech/ListenEvent.cs
#	Resources/Prototypes/Entities/Effects/admin_triggers.yml
This commit is contained in:
Ed
2025-08-25 16:22:32 +03:00
1069 changed files with 42396 additions and 29527 deletions

View File

@@ -13,6 +13,7 @@ using Content.Server.Clothing.Systems;
using Content.Server.Implants;
using Content.Shared.Implants;
using Content.Shared.Inventory;
using Content.Shared.Lock;
using Content.Shared.PDA;
namespace Content.Server.Access.Systems
@@ -25,6 +26,7 @@ namespace Content.Server.Access.Systems
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ChameleonClothingSystem _chameleon = default!;
[Dependency] private readonly ChameleonControllerSystem _chamController = default!;
[Dependency] private readonly LockSystem _lock = default!;
public override void Initialize()
{
@@ -79,7 +81,8 @@ namespace Content.Server.Access.Systems
private void OnAfterInteract(EntityUid uid, AgentIDCardComponent component, AfterInteractEvent args)
{
if (args.Target == null || !args.CanReach || !TryComp<AccessComponent>(args.Target, out var targetAccess) || !HasComp<IdCardComponent>(args.Target))
if (args.Target == null || !args.CanReach || _lock.IsLocked(uid) ||
!TryComp<AccessComponent>(args.Target, out var targetAccess) || !HasComp<IdCardComponent>(args.Target))
return;
if (!TryComp<AccessComponent>(uid, out var access) || !HasComp<IdCardComponent>(uid))

View File

@@ -44,7 +44,7 @@ public sealed class VariantizeCommand : IConsoleCommand
foreach (var tile in mapsSystem.GetAllTiles(euid.Value, gridComp))
{
var def = turfSystem.GetContentTileDefinition(tile);
var newTile = new Tile(tile.Tile.TypeId, tile.Tile.Flags, tileSystem.PickVariant(def));
var newTile = new Tile(tile.Tile.TypeId, tile.Tile.Flags, tileSystem.PickVariant(def), tile.Tile.RotationMirroring);
mapsSystem.SetTile(euid.Value, gridComp, tile.GridIndices, newTile);
}
}

View File

@@ -1,6 +1,5 @@
using System.Linq;
using System.Numerics;
using Content.Server.Warps;
using Content.Shared.Administration;
using Content.Shared.Follower;
using Content.Shared.Ghost;

View File

@@ -1,10 +0,0 @@
using Content.Shared.Administration.Components;
using Robust.Shared.GameStates;
namespace Content.Server.Administration.Components;
[RegisterComponent]
public sealed partial class HeadstandComponent : SharedHeadstandComponent
{
}

View File

@@ -1,7 +1,6 @@
using System.Linq;
using Content.Server.Administration.Managers;
using Content.Server.Chat.Managers;
using Content.Server.Forensics;
using Content.Server.GameTicking;
using Content.Server.Hands.Systems;
using Content.Server.Mind;
@@ -21,6 +20,7 @@ using Content.Shared.PDA;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Popups;
using Content.Shared.Roles;
using Content.Shared.Roles.Components;
using Content.Shared.Roles.Jobs;
using Content.Shared.StationRecords;
using Content.Shared.Throwing;

View File

@@ -29,15 +29,16 @@ public sealed partial class AdminVerbSystem
private static readonly EntProtoId DefaultNukeOpRule = "LoneOpsSpawn";
private static readonly EntProtoId DefaultRevsRule = "Revolutionary";
private static readonly EntProtoId DefaultThiefRule = "Thief";
private static readonly EntProtoId DefaultChangelingRule = "Changeling";
private static readonly EntProtoId ParadoxCloneRuleId = "ParadoxCloneSpawn";
private static readonly ProtoId<StartingGearPrototype> PirateGearId = "PirateGear";
//CP14
private static readonly EntProtoId CP14VampireUnnameable = "CP14GameRuleVampireClanUnnameable";
private static readonly EntProtoId CP14VampireDevourers = "CP14GameRuleVampireClanDevourers";
private static readonly EntProtoId CP14VampireNightChildrens = "CP14GameRuleVampireClanNightChildrens";
//CP14 end
private static readonly EntProtoId ParadoxCloneRuleId = "ParadoxCloneSpawn";
// All antag verbs have names so invokeverb works.
private void AddAntagVerbs(GetVerbsEvent<Verb> args)
{
@@ -108,7 +109,7 @@ public sealed partial class AdminVerbSystem
_antag.ForceMakeAntag<TraitorRuleComponent>(targetPlayer, DefaultTraitorRule);
},
Impact = LogImpact.High,
Message = string.Join(": ", traitorName, Loc.GetString("admin-verb-make-traitor")),
Message = string.Join(": ", traitorName, Loc.GetString("admin-verb-make-traitor")),
};
args.Verbs.Add(traitor);
@@ -203,6 +204,21 @@ public sealed partial class AdminVerbSystem
};
args.Verbs.Add(thief);
var changelingName = Loc.GetString("admin-verb-text-make-changeling");
Verb changeling = new()
{
Text = changelingName,
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Objects/Weapons/Melee/armblade.rsi"), "icon"),
Act = () =>
{
_antag.ForceMakeAntag<ChangelingRuleComponent>(targetPlayer, DefaultChangelingRule);
},
Impact = LogImpact.High,
Message = string.Join(": ", changelingName, Loc.GetString("admin-verb-make-changeling")),
};
args.Verbs.Add(changeling);
var paradoxCloneName = Loc.GetString("admin-verb-text-make-paradox-clone");
Verb paradox = new()
{

View File

@@ -2,15 +2,12 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using Content.Server.Administration.Components;
using Content.Server.Atmos;
using Content.Server.Atmos.Components;
using Content.Server.Cargo.Components;
using Content.Server.Doors.Systems;
using Content.Server.Hands.Systems;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Stack;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Server.Weapons.Ranged.Systems;
using Content.Shared.Access;
@@ -28,6 +25,7 @@ using Content.Shared.Hands.Components;
using Content.Shared.Inventory;
using Content.Shared.PDA;
using Content.Shared.Stacks;
using Content.Shared.Station.Components;
using Content.Shared.Verbs;
using Content.Shared.Weapons.Ranged.Components;
using Robust.Server.Physics;

View File

@@ -1,33 +0,0 @@
using Content.Server.AlertLevel.Systems;
namespace Content.Server.AlertLevel;
/// <summary>
/// This component is for changing the alert level of the station when triggered.
/// </summary>
[RegisterComponent, Access(typeof(AlertLevelChangeOnTriggerSystem))]
public sealed partial class AlertLevelChangeOnTriggerComponent : Component
{
///<summary>
///The alert level to change to when triggered.
///</summary>
[DataField]
public string Level = "blue";
/// <summary>
///Whether to play the sound when the alert level changes.
/// </summary>
[DataField]
public bool PlaySound = true;
/// <summary>
///Whether to say the announcement when the alert level changes.
/// </summary>
[DataField]
public bool Announce = true;
/// <summary>
///Force the alert change. This applies if the alert level is not selectable or not.
/// </summary>
[DataField]
public bool Force = false;
}

View File

@@ -1,57 +0,0 @@
using Robust.Shared.Network;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Animals.Components;
/// <summary>
/// Makes an entity able to memorize chat/radio messages
/// </summary>
[RegisterComponent]
[AutoGenerateComponentPause]
public sealed partial class ParrotMemoryComponent : Component
{
/// <summary>
/// List of SpeechMemory records this entity has learned
/// </summary>
[DataField]
public List<SpeechMemory> SpeechMemories = [];
/// <summary>
/// The % chance an entity with this component learns a phrase when learning is off cooldown
/// </summary>
[DataField]
public float LearnChance = 0.6f;
/// <summary>
/// Time after which another attempt can be made at learning a phrase
/// </summary>
[DataField]
public TimeSpan LearnCooldown = TimeSpan.FromMinutes(1);
/// <summary>
/// Next time at which the parrot can attempt to learn something
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan NextLearnInterval = TimeSpan.Zero;
/// <summary>
/// The number of speech entries that are remembered
/// </summary>
[DataField]
public int MaxSpeechMemory = 50;
/// <summary>
/// Minimum length of a speech entry
/// </summary>
[DataField]
public int MinEntryLength = 4;
/// <summary>
/// Maximum length of a speech entry
/// </summary>
[DataField]
public int MaxEntryLength = 50;
}
public record struct SpeechMemory(NetUserId? NetUserId, string Message);

View File

@@ -5,13 +5,13 @@ using Content.Server.Animals.Components;
using Content.Server.Mind;
using Content.Server.Popups;
using Content.Server.Radio;
using Content.Server.Speech;
using Content.Server.Speech.Components;
using Content.Server.Vocalization.Systems;
using Content.Shared.Animals.Components;
using Content.Shared.Animals.Systems;
using Content.Shared.Database;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Content.Shared.Speech;
using Content.Shared.Speech.Components;
using Content.Shared.Whitelist;
using Robust.Shared.Network;
using Robust.Shared.Random;
@@ -25,7 +25,7 @@ namespace Content.Server.Animals.Systems;
/// (radiovocalizer) and stores them in a list. When an entity with a VocalizerComponent attempts to vocalize, this will
/// try to set the message from memory.
/// </summary>
public sealed partial class ParrotMemorySystem : EntitySystem
public sealed partial class ParrotMemorySystem : SharedParrotMemorySystem
{
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
@@ -42,8 +42,6 @@ public sealed partial class ParrotMemorySystem : EntitySystem
SubscribeLocalEvent<EraseEvent>(OnErase);
SubscribeLocalEvent<ParrotMemoryComponent, GetVerbsEvent<Verb>>(OnGetVerbs);
SubscribeLocalEvent<ParrotListenerComponent, MapInitEvent>(ListenerOnMapInit);
SubscribeLocalEvent<ParrotListenerComponent, ListenEvent>(OnListen);
@@ -57,30 +55,6 @@ public sealed partial class ParrotMemorySystem : EntitySystem
DeletePlayerMessages(args.PlayerNetUserId);
}
private void OnGetVerbs(Entity<ParrotMemoryComponent> entity, ref GetVerbsEvent<Verb> args)
{
var user = args.User;
// limit this to admins
if (!_admin.IsAdmin(user))
return;
// simple verb that just clears the memory list
var clearMemoryVerb = new Verb()
{
Text = Loc.GetString("parrot-verb-clear-memory"),
Category = VerbCategory.Admin,
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/clear-parrot.png")),
Act = () =>
{
entity.Comp.SpeechMemories.Clear();
_popup.PopupEntity(Loc.GetString("parrot-popup-memory-cleared"), entity, user, PopupType.Medium);
},
};
args.Verbs.Add(clearMemoryVerb);
}
private void ListenerOnMapInit(Entity<ParrotListenerComponent> entity, ref MapInitEvent args)
{
// If an entity has a ParrotListenerComponent it really ought to have an ActiveListenerComponent

View File

@@ -1,238 +0,0 @@
using System.Linq;
using System.Numerics;
using Content.Server.Anomaly.Components;
using Content.Server.DeviceLinking.Systems;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Anomaly.Components;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Power;
using Robust.Shared.Audio.Systems;
using Content.Shared.Verbs;
using Robust.Shared.Timing;
namespace Content.Server.Anomaly;
/// <summary>
/// a device that allows you to translate anomaly activity into multitool signals.
/// </summary>
public sealed partial class AnomalySynchronizerSystem : EntitySystem
{
[Dependency] private readonly AnomalySystem _anomaly = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly EntityLookupSystem _entityLookup = default!;
[Dependency] private readonly DeviceLinkSystem _signal = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly PowerReceiverSystem _power = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AnomalySynchronizerComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<AnomalySynchronizerComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<AnomalySynchronizerComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<AnomalySynchronizerComponent, GetVerbsEvent<InteractionVerb>>(OnGetInteractionVerbs);
SubscribeLocalEvent<AnomalyPulseEvent>(OnAnomalyPulse);
SubscribeLocalEvent<AnomalySeverityChangedEvent>(OnAnomalySeverityChanged);
SubscribeLocalEvent<AnomalyStabilityChangedEvent>(OnAnomalyStabilityChanged);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<AnomalySynchronizerComponent, TransformComponent>();
while (query.MoveNext(out var uid, out var sync, out var xform))
{
if (sync.ConnectedAnomaly is null)
continue;
if (_timing.CurTime < sync.NextCheckTime)
continue;
sync.NextCheckTime += sync.CheckFrequency;
if (Transform(sync.ConnectedAnomaly.Value).MapUid != Transform(uid).MapUid)
{
DisconnectFromAnomaly((uid, sync), sync.ConnectedAnomaly.Value);
continue;
}
if (!xform.Coordinates.TryDistance(EntityManager, Transform(sync.ConnectedAnomaly.Value).Coordinates, out var distance))
continue;
if (distance > sync.AttachRange)
DisconnectFromAnomaly((uid, sync), sync.ConnectedAnomaly.Value);
}
}
/// <summary>
/// If powered, try to attach a nearby anomaly.
/// </summary>
public bool TryAttachNearbyAnomaly(Entity<AnomalySynchronizerComponent> ent, EntityUid? user = null)
{
if (!_power.IsPowered(ent))
{
if (user is not null)
_popup.PopupEntity(Loc.GetString("base-computer-ui-component-not-powered", ("machine", ent)), ent, user.Value);
return false;
}
var coords = _transform.GetMapCoordinates(ent);
var anomaly = _entityLookup.GetEntitiesInRange<AnomalyComponent>(coords, ent.Comp.AttachRange).FirstOrDefault();
if (anomaly.Owner is { Valid: false }) // no anomaly in range
{
if (user is not null)
_popup.PopupEntity(Loc.GetString("anomaly-sync-no-anomaly"), ent, user.Value);
return false;
}
ConnectToAnomaly(ent, anomaly);
return true;
}
private void OnPowerChanged(Entity<AnomalySynchronizerComponent> ent, ref PowerChangedEvent args)
{
if (args.Powered)
return;
if (ent.Comp.ConnectedAnomaly is null)
return;
DisconnectFromAnomaly(ent, ent.Comp.ConnectedAnomaly.Value);
}
private void OnExamined(Entity<AnomalySynchronizerComponent> ent, ref ExaminedEvent args)
{
args.PushMarkup(Loc.GetString(ent.Comp.ConnectedAnomaly.HasValue ? "anomaly-sync-examine-connected" : "anomaly-sync-examine-not-connected"));
}
private void OnGetInteractionVerbs(Entity<AnomalySynchronizerComponent> ent, ref GetVerbsEvent<InteractionVerb> args)
{
if (!args.CanAccess || !args.CanInteract || args.Hands is null || ent.Comp.ConnectedAnomaly.HasValue)
return;
var user = args.User;
args.Verbs.Add(new()
{
Act = () =>
{
TryAttachNearbyAnomaly(ent, user);
},
Message = Loc.GetString("anomaly-sync-connect-verb-message", ("machine", ent)),
Text = Loc.GetString("anomaly-sync-connect-verb-text"),
});
}
private void OnInteractHand(Entity<AnomalySynchronizerComponent> ent, ref InteractHandEvent args)
{
TryAttachNearbyAnomaly(ent, args.User);
}
private void ConnectToAnomaly(Entity<AnomalySynchronizerComponent> ent, Entity<AnomalyComponent> anomaly)
{
if (ent.Comp.ConnectedAnomaly == anomaly)
return;
ent.Comp.ConnectedAnomaly = anomaly;
//move the anomaly to the center of the synchronizer, for aesthetics.
var targetXform = _transform.GetWorldPosition(ent);
_transform.SetWorldPosition(anomaly, targetXform);
if (ent.Comp.PulseOnConnect)
_anomaly.DoAnomalyPulse(anomaly, anomaly);
_popup.PopupEntity(Loc.GetString("anomaly-sync-connected"), ent, PopupType.Medium);
_audio.PlayPvs(ent.Comp.ConnectedSound, ent);
}
//TODO: disconnection from the anomaly should also be triggered if the anomaly is far away from the synchronizer.
//Currently only bluespace anomaly can do this, but for some reason it is the only one that cannot be connected to the synchronizer.
private void DisconnectFromAnomaly(Entity<AnomalySynchronizerComponent> ent, EntityUid other)
{
if (ent.Comp.ConnectedAnomaly == null)
return;
if (TryComp<AnomalyComponent>(other, out var anomaly))
{
if (ent.Comp.PulseOnDisconnect)
_anomaly.DoAnomalyPulse(ent.Comp.ConnectedAnomaly.Value, anomaly);
}
_popup.PopupEntity(Loc.GetString("anomaly-sync-disconnected"), ent, PopupType.Large);
_audio.PlayPvs(ent.Comp.ConnectedSound, ent);
ent.Comp.ConnectedAnomaly = null;
}
private void OnAnomalyPulse(ref AnomalyPulseEvent args)
{
var query = EntityQueryEnumerator<AnomalySynchronizerComponent>();
while (query.MoveNext(out var uid, out var component))
{
if (args.Anomaly != component.ConnectedAnomaly)
continue;
if (!_power.IsPowered(uid))
continue;
_signal.InvokePort(uid, component.PulsePort);
}
}
private void OnAnomalySeverityChanged(ref AnomalySeverityChangedEvent args)
{
var query = EntityQueryEnumerator<AnomalySynchronizerComponent>();
while (query.MoveNext(out var ent, out var component))
{
if (args.Anomaly != component.ConnectedAnomaly)
continue;
if (!_power.IsPowered(ent))
continue;
//The superscritical port is invoked not at the AnomalySupercriticalEvent,
//but at the moment the growth animation starts. Otherwise, there is no point in this port.
//ATTENTION! the console command supercriticalanomaly does not work here,
//as it forcefully causes growth to start without increasing severity.
if (args.Severity >= 1)
_signal.InvokePort(ent, component.SupercritPort);
}
}
private void OnAnomalyStabilityChanged(ref AnomalyStabilityChangedEvent args)
{
Entity<AnomalyComponent> anomaly = (args.Anomaly, Comp<AnomalyComponent>(args.Anomaly));
var query = EntityQueryEnumerator<AnomalySynchronizerComponent>();
while (query.MoveNext(out var ent, out var component))
{
if (component.ConnectedAnomaly != anomaly)
continue;
if (!_power.IsPowered(ent))
continue;
if (args.Stability < anomaly.Comp.DecayThreshold)
{
_signal.InvokePort(ent, component.DecayingPort);
}
else if (args.Stability > anomaly.Comp.GrowthThreshold)
{
_signal.InvokePort(ent, component.GrowingPort);
}
else
{
_signal.InvokePort(ent, component.StabilizePort);
}
}
}
}

View File

@@ -1,6 +1,5 @@
using Content.Server.Anomaly.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Station.Components;
using Content.Shared.Anomaly;
using Content.Shared.CCVar;
using Content.Shared.Materials;
@@ -164,8 +163,7 @@ public sealed partial class AnomalySystem
var xform = Transform(uid);
if (_station.GetStationInMap(xform.MapID) is not { } station ||
!TryComp<StationDataComponent>(station, out var data) ||
_station.GetLargestGrid(data) is not { } grid)
_station.GetLargestGrid(station) is not { } grid)
{
if (xform.GridUid == null)
return;

View File

@@ -1,66 +0,0 @@
using Content.Shared.DeviceLinking;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Server.Anomaly.Components;
/// <summary>
/// a device that allows you to translate anomaly activity into multitool signals.
/// </summary>
[RegisterComponent, AutoGenerateComponentPause, Access(typeof(AnomalySynchronizerSystem))]
public sealed partial class AnomalySynchronizerComponent : Component
{
/// <summary>
/// The uid of the anomaly to which the synchronizer is connected.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public EntityUid? ConnectedAnomaly;
/// <summary>
/// Should the anomaly pulse when connected to the synchronizer?
/// </summary>
[DataField]
public bool PulseOnConnect = true;
/// <summary>
/// Should the anomaly pulse when disconnected from synchronizer?
/// </summary>
[DataField]
public bool PulseOnDisconnect = false;
/// <summary>
/// minimum distance from the synchronizer to the anomaly to be attached
/// </summary>
[DataField]
public float AttachRange = 0.4f;
/// <summary>
/// Periodicheski checks to see if the anomaly has moved to disconnect it.
/// </summary>
[DataField]
public TimeSpan CheckFrequency = TimeSpan.FromSeconds(1f);
[DataField, AutoPausedField]
public TimeSpan NextCheckTime = TimeSpan.Zero;
[DataField]
public ProtoId<SourcePortPrototype> DecayingPort = "Decaying";
[DataField]
public ProtoId<SourcePortPrototype> StabilizePort = "Stabilize";
[DataField]
public ProtoId<SourcePortPrototype> GrowingPort = "Growing";
[DataField]
public ProtoId<SourcePortPrototype> PulsePort = "Pulse";
[DataField]
public ProtoId<SourcePortPrototype> SupercritPort = "Supercritical";
[DataField, ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier ConnectedSound = new SoundPathSpecifier("/Audio/Machines/anomaly_sync_connect.ogg");
[DataField, ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier DisconnectedSound = new SoundPathSpecifier("/Audio/Machines/anomaly_sync_connect.ogg");
}

View File

@@ -0,0 +1,24 @@
using Content.Server.Atmos.Piping.EntitySystems;
using JetBrains.Annotations;
namespace Content.Server.Atmos.Piping.Components;
[RegisterComponent]
public sealed partial class AtmosPipeColorComponent : Component
{
[DataField]
public Color Color { get; set; } = Color.White;
[ViewVariables(VVAccess.ReadWrite), UsedImplicitly]
public Color ColorVV
{
get => Color;
set => IoCManager.Resolve<IEntityManager>().System<AtmosPipeColorSystem>().SetColor(Owner, this, value);
}
}
[ByRefEvent]
public record struct AtmosPipeColorChangedEvent(Color color)
{
public Color Color = color;
}

View File

@@ -0,0 +1,48 @@
using Content.Server.Atmos.Piping.Components;
using Content.Shared.Atmos.Piping;
using Robust.Server.GameObjects;
namespace Content.Server.Atmos.Piping.EntitySystems
{
public sealed class AtmosPipeColorSystem : EntitySystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AtmosPipeColorComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<AtmosPipeColorComponent, ComponentShutdown>(OnShutdown);
}
private void OnStartup(EntityUid uid, AtmosPipeColorComponent component, ComponentStartup args)
{
if (!TryComp(uid, out AppearanceComponent? appearance))
return;
_appearance.SetData(uid, PipeColorVisuals.Color, component.Color, appearance);
}
private void OnShutdown(EntityUid uid, AtmosPipeColorComponent component, ComponentShutdown args)
{
if (!TryComp(uid, out AppearanceComponent? appearance))
return;
_appearance.SetData(uid, PipeColorVisuals.Color, Color.White, appearance);
}
public void SetColor(EntityUid uid, AtmosPipeColorComponent component, Color color)
{
component.Color = color;
if (!TryComp(uid, out AppearanceComponent? appearance))
return;
_appearance.SetData(uid, PipeColorVisuals.Color, color, appearance);
var ev = new AtmosPipeColorChangedEvent(color);
RaiseLocalEvent(uid, ref ev);
}
}
}

View File

@@ -1,47 +1,46 @@
using Content.Server.Body.Components;
using Content.Server.Ghost.Components;
using Content.Shared.Body.Components;
using Content.Shared.Body.Events;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Pointing;
namespace Content.Server.Body.Systems
namespace Content.Server.Body.Systems;
public sealed class BrainSystem : EntitySystem
{
public sealed class BrainSystem : EntitySystem
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
public override void Initialize()
{
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
base.Initialize();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BrainComponent, OrganAddedToBodyEvent>((uid, _, args) => HandleMind(args.Body, uid));
SubscribeLocalEvent<BrainComponent, OrganRemovedFromBodyEvent>((uid, _, args) => HandleMind(uid, args.OldBody));
SubscribeLocalEvent<BrainComponent, PointAttemptEvent>(OnPointAttempt);
}
SubscribeLocalEvent<BrainComponent, OrganAddedToBodyEvent>((uid, _, args) => HandleMind(args.Body, uid));
SubscribeLocalEvent<BrainComponent, OrganRemovedFromBodyEvent>((uid, _, args) => HandleMind(uid, args.OldBody));
SubscribeLocalEvent<BrainComponent, PointAttemptEvent>(OnPointAttempt);
}
private void HandleMind(EntityUid newEntity, EntityUid oldEntity)
{
if (TerminatingOrDeleted(newEntity) || TerminatingOrDeleted(oldEntity))
return;
private void HandleMind(EntityUid newEntity, EntityUid oldEntity)
{
if (TerminatingOrDeleted(newEntity) || TerminatingOrDeleted(oldEntity))
return;
EnsureComp<MindContainerComponent>(newEntity);
EnsureComp<MindContainerComponent>(oldEntity);
EnsureComp<MindContainerComponent>(newEntity);
EnsureComp<MindContainerComponent>(oldEntity);
var ghostOnMove = EnsureComp<GhostOnMoveComponent>(newEntity);
ghostOnMove.MustBeDead = HasComp<MobStateComponent>(newEntity); // Don't ghost living players out of their bodies.
var ghostOnMove = EnsureComp<GhostOnMoveComponent>(newEntity);
if (HasComp<BodyComponent>(newEntity))
ghostOnMove.MustBeDead = true;
if (!_mindSystem.TryGetMind(oldEntity, out var mindId, out var mind))
return;
if (!_mindSystem.TryGetMind(oldEntity, out var mindId, out var mind))
return;
_mindSystem.TransferTo(mindId, newEntity, mind: mind);
}
_mindSystem.TransferTo(mindId, newEntity, mind: mind);
}
private void OnPointAttempt(Entity<BrainComponent> ent, ref PointAttemptEvent args)
{
args.Cancel();
}
private void OnPointAttempt(Entity<BrainComponent> ent, ref PointAttemptEvent args)
{
args.Cancel();
}
}

View File

@@ -1,8 +1,8 @@
using System.Linq;
using Content.Server.Station.Components;
using Content.Shared.Cargo;
using Content.Shared.Cargo.Components;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.Station.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.Cargo.Components;

View File

@@ -1,7 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Cargo.Components;
using Content.Server.Station.Components;
using Content.Shared.Cargo;
using Content.Shared.Cargo.BUI;
using Content.Shared.Cargo.Components;
@@ -13,8 +12,8 @@ using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Labels.Components;
using Content.Shared.Paper;
using Content.Shared.Station.Components;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;

View File

@@ -3,11 +3,11 @@ using System.Linq;
using Content.Server.Cargo.Components;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Station.Components;
using Content.Shared.Cargo;
using Content.Shared.Cargo.Components;
using Content.Shared.DeviceLinking;
using Content.Shared.Power;
using Content.Shared.Station.Components;
using Robust.Shared.Audio;
using Robust.Shared.Random;
using Robust.Shared.Utility;

View File

@@ -92,19 +92,22 @@ public sealed class PricingSystem : EntitySystem
if (args.Handled)
return;
if (!TryComp<BodyComponent>(uid, out var body) || !TryComp<MobStateComponent>(uid, out var state))
if (!TryComp<MobStateComponent>(uid, out var state))
{
Log.Error($"Tried to get the mob price of {ToPrettyString(uid)}, which has no {nameof(BodyComponent)} and no {nameof(MobStateComponent)}.");
Log.Error($"Tried to get the mob price of {ToPrettyString(uid)}, which has no {nameof(MobStateComponent)}.");
return;
}
// TODO: Better handling of missing.
var partList = _bodySystem.GetBodyChildren(uid, body).ToList();
var totalPartsPresent = partList.Sum(_ => 1);
var totalParts = partList.Count;
var partPenalty = 0.0;
if (TryComp<BodyComponent>(uid, out var body))
{
var partList = _bodySystem.GetBodyChildren(uid, body).ToList();
var totalPartsPresent = partList.Sum(_ => 1);
var totalParts = partList.Count;
var partRatio = totalPartsPresent / (double) totalParts;
var partPenalty = component.Price * (1 - partRatio) * component.MissingBodyPartPenalty;
var partRatio = totalPartsPresent / (double) totalParts;
partPenalty = component.Price * (1 - partRatio) * component.MissingBodyPartPenalty;
}
args.Price += (component.Price - partPenalty) * (_mobStateSystem.IsAlive(uid, state) ? 1.0 : component.DeathPenalty);
}

View File

@@ -1,17 +0,0 @@
using Content.Shared.Dataset;
using Robust.Shared.Prototypes;
namespace Content.Server.Chat;
/// <summary>
/// Makes the entity speak when triggered. If the item has UseDelay component, the system will respect that cooldown.
/// </summary>
[RegisterComponent]
public sealed partial class SpeakOnTriggerComponent : Component
{
/// <summary>
/// The identifier for the dataset prototype containing messages to be spoken by this entity.
/// </summary>
[DataField(required: true)]
public ProtoId<LocalizedDatasetPrototype> Pack = string.Empty;
}

View File

@@ -67,6 +67,9 @@ public sealed class SuicideSystem : EntitySystem
if (!suicideGhostEvent.Handled || _tagSystem.HasTag(victim, CannotSuicideTag))
return false;
// TODO: fix this
// This is a handled event, but the result is never used
// It looks like TriggerOnMobstateChange is supposed to prevent you from suiciding
var suicideEvent = new SuicideEvent(victim);
RaiseLocalEvent(victim, suicideEvent);

View File

@@ -0,0 +1,105 @@
using Content.Server.Chat.Managers;
using Content.Shared.Chat;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Mind;
using Content.Shared.Roles;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Server.Chat.Systems;
/// <summary>
/// This system is used to notify specific players of the occurance of predefined events.
/// </summary>
public sealed partial class ChatNotificationSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly IChatManager _chats = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private ISawmill _sawmill = default!;
// The following data does not need to be saved
// Local cache for rate limiting chat notifications by source
// (Recipient, ChatNotification) -> Dictionary<Source, next allowed TOA>
private readonly Dictionary<(EntityUid, ProtoId<ChatNotificationPrototype>), Dictionary<EntityUid, TimeSpan>> _chatNotificationsBySource = new();
// Local cache for rate limiting chat notifications by type
// (Recipient, ChatNotification) -> next allowed TOA
private readonly Dictionary<(EntityUid, ProtoId<ChatNotificationPrototype>), TimeSpan> _chatNotificationsByType = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ActorComponent, ChatNotificationEvent>(OnChatNotification);
_sawmill = _logManager.GetSawmill("chatnotification");
}
/// <summary>
/// Triggered when the specified player recieves a chat notification event.
/// </summary>
/// <param name="ent">The player receiving the chat notification.</param>
/// <param name="args">The chat notification event</param>
public void OnChatNotification(Entity<ActorComponent> ent, ref ChatNotificationEvent args)
{
if (!_proto.TryIndex(args.ChatNotification, out var chatNotification))
{
_sawmill.Warning("Attempted to index ChatNotificationPrototype " + args.ChatNotification + " but the prototype does not exist.");
return;
}
var source = args.Source;
var playerNotification = (ent, args.ChatNotification);
// Exit without notifying the player if we received a notification before the appropriate time has elasped
if (chatNotification.NotifyBySource)
{
if (!_chatNotificationsBySource.TryGetValue(playerNotification, out var trackedSources))
trackedSources = new();
trackedSources.TryGetValue(source, out var timeSpan);
trackedSources[source] = _timing.CurTime + chatNotification.NextDelay;
_chatNotificationsBySource[playerNotification] = trackedSources;
if (_timing.CurTime < timeSpan)
return;
}
else
{
_chatNotificationsByType.TryGetValue(playerNotification, out var timeSpan);
_chatNotificationsByType[playerNotification] = _timing.CurTime + chatNotification.NextDelay;
if (_timing.CurTime < timeSpan)
return;
}
var sourceName = args.SourceNameOverride ?? Name(source);
var userName = args.UserNameOverride ?? (args.User.HasValue ? Name(args.User.Value) : string.Empty);
var targetName = Name(ent);
var message = Loc.GetString(chatNotification.Message, ("source", sourceName), ("user", userName), ("target", targetName));
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message));
_chats.ChatMessageToOne(
ChatChannel.Notifications,
message,
wrappedMessage,
default,
false,
ent.Comp.PlayerSession.Channel,
colorOverride: chatNotification.Color
);
if (chatNotification.Sound != null && _mind.TryGetMind(ent, out var mindId, out _))
_roles.MindPlaySound(mindId, chatNotification.Sound);
}
}

View File

@@ -9,7 +9,6 @@ using Content.Server.GameTicking;
using Content.Server.Speech.Prototypes;
using Content.Server.Speech.EntitySystems;
using Content.Server.Speech.Prototypes;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Shared.ActionBlocker;
using Content.Shared.Administration;
@@ -23,6 +22,7 @@ using Content.Shared.Mobs.Systems;
using Content.Shared.Players;
using Content.Shared.Players.RateLimiting;
using Content.Shared.Radio;
using Content.Shared.Station.Components;
using Content.Shared.Whitelist;
using Robust.Server.Player;
using Robust.Shared.Audio;
@@ -62,11 +62,6 @@ public sealed partial class ChatSystem : SharedChatSystem
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly ExamineSystemShared _examineSystem = default!;
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 sound
private bool _loocEnabled = true;
private bool _deadLoocEnabled;
private bool _critLoocEnabled;
@@ -194,13 +189,6 @@ public sealed partial class ChatSystem : SharedChatSystem
if (!CanSendInGame(message, shell, player))
return;
//CP14 Prevent god from default speaking. In waiting of chatcode refactor
var ev = new CP14SpokeAttemptEvent(message, desiredType, player);
RaiseLocalEvent(source, ev);
if (ev.Cancelled)
return;
//CP14 end
ignoreActionBlocker = CheckIgnoreSpeechBlocker(source, ignoreActionBlocker);
// this method is a disaster

View File

@@ -1,11 +1,16 @@
using Content.Server.Forensics;
using Content.Server.Speech.EntitySystems;
using Content.Shared.Cloning.Events;
using Content.Shared.Clothing.Components;
using Content.Shared.FixedPoint;
using Content.Shared.Inventory;
using Content.Shared.Labels.Components;
using Content.Shared.Labels.EntitySystems;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.Paper;
using Content.Shared.Stacks;
using Content.Shared.Speech.Components;
using Content.Shared.Storage;
using Content.Shared.Store;
using Content.Shared.Store.Components;
using Robust.Shared.Prototypes;
@@ -13,47 +18,58 @@ using Robust.Shared.Prototypes;
namespace Content.Server.Cloning;
/// <summary>
/// The part of item cloning responsible for copying over important components.
/// This is used for <see cref="CopyItem"/>.
/// Anything not copied over here gets reverted to the values the item had in its prototype.
/// The part of item cloning responsible for copying over important components.
/// </summary>
/// <remarks>
/// This method of copying items is of course not perfect as we cannot clone every single component, which would be pretty much impossible with our ECS.
/// We only consider the most important components so the paradox clone gets similar equipment.
/// This method of using subscriptions was chosen to make it easy for forks to add their own custom components that need to be copied.
/// These are all not part of their corresponding systems because we don't want systems every system to depend on a CloningSystem namespace import, which is still heavily coupled to med code.
/// TODO: Create a more generic "CopyEntity" method/event (probably in RT) that doesn't have this problem and then move all these subscriptions.
/// </remarks>
public sealed partial class CloningSystem : EntitySystem
public sealed partial class CloningSystem
{
[Dependency] private readonly SharedStackSystem _stack = default!;
[Dependency] private readonly LabelSystem _label = default!;
[Dependency] private readonly ForensicsSystem _forensics = default!;
[Dependency] private readonly PaperSystem _paper = default!;
[Dependency] private readonly VocalSystem _vocal = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StackComponent, CloningItemEvent>(OnCloneStack);
SubscribeLocalEvent<LabelComponent, CloningItemEvent>(OnCloneLabel);
SubscribeLocalEvent<PaperComponent, CloningItemEvent>(OnClonePaper);
SubscribeLocalEvent<ForensicsComponent, CloningItemEvent>(OnCloneForensics);
SubscribeLocalEvent<StoreComponent, CloningItemEvent>(OnCloneStore);
// These are used for <see cref="CopyItem"/>.
// Anything not copied over here gets reverted to the values the item had in its prototype.
// This method of copying items is of course not perfect as we cannot clone every single component, which would be pretty much impossible with our ECS.
// We only consider the most important components so the paradox clone gets similar equipment.
// This method of using subscriptions was chosen to make it easy for forks to add their own custom components that need to be copied.
SubscribeLocalEvent<StackComponent, CloningItemEvent>(OnCloneItemStack);
SubscribeLocalEvent<LabelComponent, CloningItemEvent>(OnCloneItemLabel);
SubscribeLocalEvent<PaperComponent, CloningItemEvent>(OnCloneItemPaper);
SubscribeLocalEvent<ForensicsComponent, CloningItemEvent>(OnCloneItemForensics);
SubscribeLocalEvent<StoreComponent, CloningItemEvent>(OnCloneItemStore);
// These are for cloning components that cannot be cloned using CopyComp.
// Put them into CloningSettingsPrototype.EventComponents to have them be applied to the clone.
SubscribeLocalEvent<VocalComponent, CloningEvent>(OnCloneVocal);
SubscribeLocalEvent<StorageComponent, CloningEvent>(OnCloneStorage);
SubscribeLocalEvent<InventoryComponent, CloningEvent>(OnCloneInventory);
SubscribeLocalEvent<MovementSpeedModifierComponent, CloningEvent>(OnCloneInventory);
}
private void OnCloneStack(Entity<StackComponent> ent, ref CloningItemEvent args)
private void OnCloneItemStack(Entity<StackComponent> ent, ref CloningItemEvent args)
{
// if the clone is a stack as well, adjust the count of the copy
if (TryComp<StackComponent>(args.CloneUid, out var cloneStackComp))
_stack.SetCount(args.CloneUid, ent.Comp.Count, cloneStackComp);
}
private void OnCloneLabel(Entity<LabelComponent> ent, ref CloningItemEvent args)
private void OnCloneItemLabel(Entity<LabelComponent> ent, ref CloningItemEvent args)
{
// copy the label
_label.Label(args.CloneUid, ent.Comp.CurrentLabel);
}
private void OnClonePaper(Entity<PaperComponent> ent, ref CloningItemEvent args)
private void OnCloneItemPaper(Entity<PaperComponent> ent, ref CloningItemEvent args)
{
// copy the text and any stamps
if (TryComp<PaperComponent>(args.CloneUid, out var clonePaperComp))
@@ -63,13 +79,13 @@ public sealed partial class CloningSystem : EntitySystem
}
}
private void OnCloneForensics(Entity<ForensicsComponent> ent, ref CloningItemEvent args)
private void OnCloneItemForensics(Entity<ForensicsComponent> ent, ref CloningItemEvent args)
{
// copy any forensics to the cloned item
_forensics.CopyForensicsFrom(ent.Comp, args.CloneUid);
}
private void OnCloneStore(Entity<StoreComponent> ent, ref CloningItemEvent args)
private void OnCloneItemStore(Entity<StoreComponent> ent, ref CloningItemEvent args)
{
// copy the current amount of currency in the store
// at the moment this takes care of uplink implants and the portable nukie uplinks
@@ -80,4 +96,35 @@ public sealed partial class CloningSystem : EntitySystem
}
}
private void OnCloneVocal(Entity<VocalComponent> ent, ref CloningEvent args)
{
if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name))
return;
_vocal.CopyComponent(ent.AsNullable(), args.CloneUid);
}
private void OnCloneStorage(Entity<StorageComponent> ent, ref CloningEvent args)
{
if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name))
return;
_storage.CopyComponent(ent.AsNullable(), args.CloneUid);
}
private void OnCloneInventory(Entity<InventoryComponent> ent, ref CloningEvent args)
{
if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name))
return;
_inventory.CopyComponent(ent.AsNullable(), args.CloneUid);
}
private void OnCloneInventory(Entity<MovementSpeedModifierComponent> ent, ref CloningEvent args)
{
if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name))
return;
_movementSpeedModifier.CopyComponent(ent.AsNullable(), args.CloneUid);
}
}

View File

@@ -24,7 +24,7 @@ namespace Content.Server.Cloning;
/// System responsible for making a copy of a humanoid's body.
/// For the cloning machines themselves look at CloningPodSystem, CloningConsoleSystem and MedicalScannerSystem instead.
/// </summary>
public sealed partial class CloningSystem : EntitySystem
public sealed partial class CloningSystem : SharedCloningSystem
{
[Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
@@ -84,13 +84,7 @@ public sealed partial class CloningSystem : EntitySystem
return true;
}
/// <summary>
/// Copy components from one entity to another based on a CloningSettingsPrototype.
/// </summary>
/// <param name="original">The orignal Entity to clone components from.</param>
/// <param name="clone">The target Entity to clone components to.</param>
/// <param name="settings">The clone settings prototype containing the list of components to clone.</param>
public void CloneComponents(EntityUid original, EntityUid clone, CloningSettingsPrototype settings)
public override void CloneComponents(EntityUid original, EntityUid clone, CloningSettingsPrototype settings)
{
var componentsToCopy = settings.Components;
var componentsToEvent = settings.EventComponents;
@@ -128,7 +122,8 @@ public sealed partial class CloningSystem : EntitySystem
}
// If the original does not have the component, then the clone shouldn't have it either.
RemComp(clone, componentRegistration.Type);
if (!HasComp(original, componentRegistration.Type))
RemComp(clone, componentRegistration.Type);
}
var cloningEv = new CloningEvent(settings, clone);

View File

@@ -1,4 +1,4 @@
using Content.Server.Station.Components;
using Content.Shared.Station.Components;
using Robust.Shared.Console;
namespace Content.Server.Commands;

View File

@@ -276,8 +276,8 @@ namespace Content.Server.Construction
if(!insertStep.EntityValid(insert, EntityManager, Factory))
return HandleResult.False;
// Unremovable items can't be inserted, unless they are a lingering stack
if(HasComp<UnremoveableComponent>(insert) && (!TryComp<StackComponent>(insert, out var comp) || !comp.Lingering))
// Unremovable items can't be inserted
if(HasComp<UnremoveableComponent>(insert))
return HandleResult.False;
// If we're only testing whether this step would be handled by the given event, then we're done.

View File

@@ -1,7 +1,6 @@
using System.Linq;
using Content.Server.Administration;
using Content.Server.EUI;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Server.StationRecords;
using Content.Server.StationRecords.Systems;
@@ -10,12 +9,12 @@ using Content.Shared.CCVar;
using Content.Shared.CrewManifest;
using Content.Shared.GameTicking;
using Content.Shared.Roles;
using Content.Shared.Station.Components;
using Content.Shared.StationRecords;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.CrewManifest;

View File

@@ -1,50 +0,0 @@
using Content.Server.Explosion.EntitySystems;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
namespace Content.Server.Damage.Systems;
// System for damage that occurs on specific trigger, towards the user..
public sealed class DamageUserOnTriggerSystem : EntitySystem
{
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
public override void Initialize()
{
SubscribeLocalEvent<DamageUserOnTriggerComponent, TriggerEvent>(OnTrigger);
}
private void OnTrigger(EntityUid uid, DamageUserOnTriggerComponent component, TriggerEvent args)
{
if (args.User is null)
return;
args.Handled |= OnDamageTrigger(uid, args.User.Value, component);
}
private bool OnDamageTrigger(EntityUid source, EntityUid target, DamageUserOnTriggerComponent? component = null)
{
if (!Resolve(source, ref component))
{
return false;
}
var damage = new DamageSpecifier(component.Damage);
var ev = new BeforeDamageUserOnTriggerEvent(damage, target);
RaiseLocalEvent(source, ev);
return _damageableSystem.TryChangeDamage(target, ev.Damage, component.IgnoreResistances, origin: source) is not null;
}
}
public sealed class BeforeDamageUserOnTriggerEvent : EntityEventArgs
{
public DamageSpecifier Damage { get; set; }
public EntityUid Tripper { get; }
public BeforeDamageUserOnTriggerEvent(DamageSpecifier damage, EntityUid target)
{
Damage = damage;
Tripper = target;
}
}

View File

@@ -1,5 +1,4 @@
using Content.Server.Defusable.Components;
using Content.Server.Explosion.Components;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Popups;
using Content.Server.Wires;
@@ -8,13 +7,13 @@ using Content.Shared.Construction.Components;
using Content.Shared.Database;
using Content.Shared.Defusable;
using Content.Shared.Examine;
using Content.Shared.Explosion.Components;
using Content.Shared.Explosion.Components.OnTrigger;
using Content.Shared.Popups;
using Content.Shared.Trigger.Components;
using Content.Shared.Trigger.Components.Effects;
using Content.Shared.Trigger.Systems;
using Content.Shared.Verbs;
using Content.Shared.Wires;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
namespace Content.Server.Defusable.Systems;
@@ -74,12 +73,13 @@ public sealed class DefusableSystem : SharedDefusableSystem
{
args.PushMarkup(Loc.GetString("defusable-examine-defused", ("name", uid)));
}
else if (comp.Activated && TryComp<ActiveTimerTriggerComponent>(uid, out var activeComp))
else if (comp.Activated)
{
if (comp.DisplayTime)
var remaining = _trigger.GetRemainingTime(uid);
if (comp.DisplayTime && remaining != null)
{
args.PushMarkup(Loc.GetString("defusable-examine-live", ("name", uid),
("time", MathF.Floor(activeComp.TimeRemaining))));
("time", Math.Floor(remaining.Value.TotalSeconds))));
}
else
{
@@ -139,16 +139,9 @@ public sealed class DefusableSystem : SharedDefusableSystem
SetActivated(comp, true);
_popup.PopupEntity(Loc.GetString("defusable-popup-begun", ("name", uid)), uid);
if (TryComp<OnUseTimerTriggerComponent>(uid, out var timerTrigger))
if (TryComp<TimerTriggerComponent>(uid, out var timerTrigger))
{
_trigger.HandleTimerTrigger(
uid,
user,
timerTrigger.Delay,
timerTrigger.BeepInterval,
timerTrigger.InitialBeepDelay,
timerTrigger.BeepSound
);
_trigger.ActivateTimerTrigger((uid, timerTrigger));
}
RaiseLocalEvent(uid, new BombArmedEvent(uid));
@@ -168,7 +161,7 @@ public sealed class DefusableSystem : SharedDefusableSystem
RaiseLocalEvent(uid, new BombDetonatedEvent(uid));
_explosion.TriggerExplosive(uid, user:detonator);
_explosion.TriggerExplosive(uid, user: detonator);
QueueDel(uid);
_appearance.SetData(uid, DefusableVisuals.Active, comp.Activated);
@@ -188,7 +181,7 @@ public sealed class DefusableSystem : SharedDefusableSystem
{
SetUsable(comp, false);
RemComp<ExplodeOnTriggerComponent>(uid);
RemComp<OnUseTimerTriggerComponent>(uid);
RemComp<TimerTriggerComponent>(uid);
}
RemComp<ActiveTimerTriggerComponent>(uid);
@@ -246,7 +239,7 @@ public sealed class DefusableSystem : SharedDefusableSystem
if (comp is not { Activated: true, DelayWireUsed: false })
return;
_trigger.TryDelay(wire.Owner, 30f);
_trigger.TryDelay(wire.Owner, TimeSpan.FromSeconds(30));
_popup.PopupEntity(Loc.GetString("defusable-popup-wire-chirp", ("name", wire.Owner)), wire.Owner);
comp.DelayWireUsed = true;
}
@@ -268,7 +261,7 @@ public sealed class DefusableSystem : SharedDefusableSystem
if (comp is { Activated: true, ProceedWireUsed: false })
{
comp.ProceedWireUsed = true;
_trigger.TryDelay(wire.Owner, -15f);
_trigger.TryDelay(wire.Owner, TimeSpan.FromSeconds(-15));
}
_popup.PopupEntity(Loc.GetString("defusable-popup-wire-proceed-pulse", ("name", wire.Owner)), wire.Owner);
@@ -298,7 +291,7 @@ public sealed class DefusableSystem : SharedDefusableSystem
{
if (!comp.ActivatedWireUsed)
{
_trigger.TryDelay(wire.Owner, 30f);
_trigger.TryDelay(wire.Owner, TimeSpan.FromSeconds(30));
_popup.PopupEntity(Loc.GetString("defusable-popup-wire-chirp", ("name", wire.Owner)), wire.Owner);
comp.ActivatedWireUsed = true;
}

View File

@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Systems;
@@ -14,15 +15,13 @@ using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.Destructible;
using Content.Shared.FixedPoint;
using Content.Shared.Humanoid;
using Content.Shared.Trigger.Systems;
using JetBrains.Annotations;
using Robust.Server.Audio;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using System.Linq;
using Content.Shared.Humanoid;
using Robust.Shared.Player;
namespace Content.Server.Destructible
{

View File

@@ -5,6 +5,6 @@ public sealed partial class TimerStartBehavior : IThresholdBehavior
{
public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null)
{
system.TriggerSystem.StartTimer(owner, cause);
system.TriggerSystem.ActivateTimerTrigger(owner, cause);
}
}

View File

@@ -1,10 +1,18 @@
namespace Content.Server.Destructible.Thresholds.Behaviors;
using Content.Shared.Trigger.Systems;
namespace Content.Server.Destructible.Thresholds.Behaviors;
[DataDefinition]
public sealed partial class TriggerBehavior : IThresholdBehavior
{
/// <summary>
/// The trigger key to use when triggering.
/// </summary>
[DataField]
public string? KeyOut { get; set; } = TriggerSystem.DefaultTriggerKey;
public void Execute(EntityUid owner, DestructibleSystem system, EntityUid? cause = null)
{
system.TriggerSystem.Trigger(owner, cause);
system.TriggerSystem.Trigger(owner, cause, KeyOut);
}
}

View File

@@ -1,4 +1,6 @@
using Content.Server.DeviceLinking.Components;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Shared.DeviceLinking;
using Content.Shared.DeviceLinking.Events;

View File

@@ -1,5 +1,4 @@
using Content.Server.DeviceLinking.Components;
using Content.Server.DeviceNetwork;
using Content.Shared.DeviceLinking;
using Content.Shared.DeviceLinking.Events;
using Content.Shared.DeviceNetwork;

View File

@@ -1,6 +1,5 @@
using Content.Server.Administration.Logs;
using Content.Server.DeviceLinking.Components;
using Content.Server.Explosion.EntitySystems;
using Content.Shared.Database;
using Content.Shared.Interaction.Events;
using Content.Shared.Timing;
@@ -10,7 +9,6 @@ namespace Content.Server.DeviceLinking.Systems;
public sealed class SignallerSystem : EntitySystem
{
[Dependency] private readonly DeviceLinkSystem _link = default!;
[Dependency] private readonly UseDelaySystem _useDelay = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
public override void Initialize()
@@ -19,7 +17,6 @@ public sealed class SignallerSystem : EntitySystem
SubscribeLocalEvent<SignallerComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<SignallerComponent, UseInHandEvent>(OnUseInHand);
SubscribeLocalEvent<SignallerComponent, TriggerEvent>(OnTrigger);
}
private void OnInit(EntityUid uid, SignallerComponent component, ComponentInit args)
@@ -36,16 +33,4 @@ public sealed class SignallerSystem : EntitySystem
_link.InvokePort(uid, component.Port);
args.Handled = true;
}
private void OnTrigger(EntityUid uid, SignallerComponent component, TriggerEvent args)
{
if (!TryComp(uid, out UseDelayComponent? useDelay)
// if on cooldown, do nothing
// and set cooldown to prevent clocks
|| !_useDelay.TryResetDelay((uid, useDelay), true))
return;
_link.InvokePort(uid, component.Port);
args.Handled = true;
}
}

View File

@@ -1,5 +1,4 @@
using Content.Server.Administration.Logs;
using Content.Server.Light.Components;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.NodeGroups;
@@ -16,6 +15,7 @@ using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Jittering;
using Content.Shared.Light.Components;
using Content.Shared.Maps;
using Content.Shared.NodeContainer;
using Content.Shared.NodeContainer.NodeGroups;
@@ -58,7 +58,7 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly TurfSystem _turf = default!;
private static readonly ProtoId<StatusEffectPrototype> StatusEffectKey = "Electrocution";
private static readonly ProtoId<StatusEffectPrototype> StatusKeyIn = "Electrocution";
private static readonly ProtoId<DamageTypePrototype> DamageType = "Shock";
private static readonly ProtoId<TagPrototype> WindowTag = "Window";
@@ -386,12 +386,12 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
}
if (!Resolve(uid, ref statusEffects, false) ||
!_statusEffects.CanApplyEffect(uid, StatusEffectKey, statusEffects))
!_statusEffects.CanApplyEffect(uid, StatusKeyIn, statusEffects))
{
return false;
}
if (!_statusEffects.TryAddStatusEffect<ElectrocutedComponent>(uid, StatusEffectKey, time, refresh, statusEffects))
if (!_statusEffects.TryAddStatusEffect<ElectrocutedComponent>(uid, StatusKeyIn, time, refresh, statusEffects))
return false;
var shouldStun = siemensCoefficient > 0.5f;

View File

@@ -1,6 +1,6 @@
using Content.Server.Emoting.Systems;
using Content.Shared.Chat.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Prototypes;
namespace Content.Server.Emoting.Components;
@@ -14,11 +14,6 @@ public sealed partial class BodyEmotesComponent : Component
/// <summary>
/// Emote sounds prototype id for body emotes.
/// </summary>
[DataField("soundsId", customTypeSerializer: typeof(PrototypeIdSerializer<EmoteSoundsPrototype>))]
public string? SoundsId;
/// <summary>
/// Loaded emote sounds prototype used for body emotes.
/// </summary>
public EmoteSoundsPrototype? Sounds;
[DataField]
public ProtoId<EmoteSoundsPrototype>? SoundsId;
}

View File

@@ -14,15 +14,8 @@ public sealed class BodyEmotesSystem : EntitySystem
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BodyEmotesComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<BodyEmotesComponent, EmoteEvent>(OnEmote);
}
private void OnStartup(EntityUid uid, BodyEmotesComponent component, ComponentStartup args)
{
if (component.SoundsId == null)
return;
_proto.TryIndex(component.SoundsId, out component.Sounds);
SubscribeLocalEvent<BodyEmotesComponent, EmoteEvent>(OnEmote);
}
private void OnEmote(EntityUid uid, BodyEmotesComponent component, ref EmoteEvent args)
@@ -43,6 +36,9 @@ public sealed class BodyEmotesSystem : EntitySystem
if (!TryComp(uid, out HandsComponent? hands) || hands.Count <= 0)
return false;
return _chat.TryPlayEmoteSound(uid, component.Sounds, emote);
if (!_proto.Resolve(component.SoundsId, out var sounds))
return false;
return _chat.TryPlayEmoteSound(uid, sounds, emote);
}
}

View File

@@ -1,24 +0,0 @@
namespace Content.Server.Emp;
/// <summary>
/// Upon being triggered will EMP area around it.
/// </summary>
[RegisterComponent]
[Access(typeof(EmpSystem))]
public sealed partial class EmpOnTriggerComponent : Component
{
[DataField("range"), ViewVariables(VVAccess.ReadWrite)]
public float Range = 1.0f;
/// <summary>
/// How much energy will be consumed per battery in range
/// </summary>
[DataField("energyConsumption"), ViewVariables(VVAccess.ReadWrite)]
public float EnergyConsumption;
/// <summary>
/// How long it disables targets in seconds
/// </summary>
[DataField("disableDuration"), ViewVariables(VVAccess.ReadWrite)]
public float DisableDuration = 60f;
}

View File

@@ -1,10 +1,7 @@
using Content.Server.Explosion.EntitySystems;
using Content.Server.Power.EntitySystems;
using Content.Server.Radio;
using Content.Server.SurveillanceCamera;
using Content.Shared.Emp;
using Content.Shared.Examine;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
namespace Content.Server.Emp;
@@ -12,15 +9,12 @@ namespace Content.Server.Emp;
public sealed class EmpSystem : SharedEmpSystem
{
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly TransformSystem _transform = default!;
public const string EmpPulseEffectPrototype = "EffectEmpPulse";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EmpDisabledComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<EmpOnTriggerComponent, TriggerEvent>(HandleEmpTrigger);
SubscribeLocalEvent<EmpDisabledComponent, RadioSendAttemptEvent>(OnRadioSendAttempt);
SubscribeLocalEvent<EmpDisabledComponent, RadioReceiveAttemptEvent>(OnRadioReceiveAttempt);
@@ -28,14 +22,7 @@ public sealed class EmpSystem : SharedEmpSystem
SubscribeLocalEvent<EmpDisabledComponent, SurveillanceCameraSetActiveAttemptEvent>(OnCameraSetActive);
}
/// <summary>
/// Triggers an EMP pulse at the given location, by first raising an <see cref="EmpAttemptEvent"/>, then a raising <see cref="EmpPulseEvent"/> on all entities in range.
/// </summary>
/// <param name="coordinates">The location to trigger the EMP pulse at.</param>
/// <param name="range">The range of the EMP pulse.</param>
/// <param name="energyConsumption">The amount of energy consumed by the EMP pulse.</param>
/// <param name="duration">The duration of the EMP effects.</param>
public void EmpPulse(MapCoordinates coordinates, float range, float energyConsumption, float duration)
public override void EmpPulse(MapCoordinates coordinates, float range, float energyConsumption, float duration)
{
foreach (var uid in _lookup.GetEntitiesInRange(coordinates, range))
{
@@ -86,15 +73,15 @@ public sealed class EmpSystem : SharedEmpSystem
{
var ev = new EmpPulseEvent(energyConsumption, false, false, TimeSpan.FromSeconds(duration));
RaiseLocalEvent(uid, ref ev);
if (ev.Affected)
{
Spawn(EmpDisabledEffectPrototype, Transform(uid).Coordinates);
}
if (ev.Disabled)
{
var disabled = EnsureComp<EmpDisabledComponent>(uid);
disabled.DisabledUntil = Timing.CurTime + TimeSpan.FromSeconds(duration);
}
if (!ev.Disabled)
return;
var disabled = EnsureComp<EmpDisabledComponent>(uid);
disabled.DisabledUntil = Timing.CurTime + TimeSpan.FromSeconds(duration);
}
public override void Update(float frameTime)
@@ -113,17 +100,6 @@ public sealed class EmpSystem : SharedEmpSystem
}
}
private void OnExamine(EntityUid uid, EmpDisabledComponent component, ExaminedEvent args)
{
args.PushMarkup(Loc.GetString("emp-disabled-comp-on-examine"));
}
private void HandleEmpTrigger(EntityUid uid, EmpOnTriggerComponent comp, TriggerEvent args)
{
EmpPulse(_transform.GetMapCoordinates(uid), comp.Range, comp.EnergyConsumption, comp.DisableDuration);
args.Handled = true;
}
private void OnRadioSendAttempt(EntityUid uid, EmpDisabledComponent component, ref RadioSendAttemptEvent args)
{
args.Cancelled = true;
@@ -148,9 +124,7 @@ public sealed class EmpSystem : SharedEmpSystem
/// <summary>
/// Raised on an entity before <see cref="EmpPulseEvent"/>. Cancel this to prevent the emp event being raised.
/// </summary>
public sealed partial class EmpAttemptEvent : CancellableEntityEventArgs
{
}
public sealed partial class EmpAttemptEvent : CancellableEntityEventArgs;
[ByRefEvent]
public record struct EmpPulseEvent(float EnergyConsumption, bool Affected, bool Disabled, TimeSpan Duration);

View File

@@ -1,4 +0,0 @@
namespace Content.Server.Explosion.Components;
[RegisterComponent]
public sealed partial class ActiveTriggerOnTimedCollideComponent : Component { }

View File

@@ -1,9 +0,0 @@
namespace Content.Server.Explosion.Components;
/// <summary>
/// Disallows starting the timer by hand, must be stuck or triggered by a system using <c>StartTimer</c>.
/// </summary>
[RegisterComponent]
public sealed partial class AutomatedTimerComponent : Component
{
}

View File

@@ -1,13 +0,0 @@
using Content.Server.Explosion.EntitySystems;
namespace Content.Server.Explosion.Components;
/// <summary>
/// Will anchor the attached entity upon a <see cref="TriggerEvent"/>.
/// </summary>
[RegisterComponent]
public sealed partial class AnchorOnTriggerComponent : Component
{
[DataField("removeOnTrigger")]
public bool RemoveOnTrigger = true;
}

View File

@@ -1,11 +0,0 @@
using Content.Server.Explosion.EntitySystems;
namespace Content.Server.Explosion.Components;
/// <summary>
/// Will delete the attached entity upon a <see cref="TriggerEvent"/>.
/// </summary>
[RegisterComponent]
public sealed partial class DeleteOnTriggerComponent : Component
{
}

View File

@@ -1,16 +0,0 @@
namespace Content.Server.Explosion.Components;
/// <summary>
/// Gibs on trigger, self explanatory.
/// Also in case of an implant using this, gibs the implant user instead.
/// </summary>
[RegisterComponent]
public sealed partial class GibOnTriggerComponent : Component
{
/// <summary>
/// Should gibbing also delete the owners items?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("deleteItems")]
public bool DeleteItems = false;
}

View File

@@ -1,17 +0,0 @@
using Content.Server.Explosion.EntitySystems;
using Robust.Shared.Audio;
namespace Content.Server.Explosion.Components;
/// <summary>
/// Will play sound from the attached entity upon a <see cref="TriggerEvent"/>.
/// </summary>
[RegisterComponent]
public sealed partial class SoundOnTriggerComponent : Component
{
[DataField("removeOnTrigger")]
public bool RemoveOnTrigger = true;
[DataField("sound")]
public SoundSpecifier? Sound = new SoundPathSpecifier("/Audio/Effects/Grenades/supermatter_start.ogg");
}

View File

@@ -1,34 +0,0 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Explosion.Components.OnTrigger;
/// <summary>
/// After being triggered applies the specified components and runs triggers again.
/// </summary>
[RegisterComponent, AutoGenerateComponentPause]
public sealed partial class TwoStageTriggerComponent : Component
{
/// <summary>
/// How long it takes for the second stage to be triggered.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("triggerDelay")]
public TimeSpan TriggerDelay = TimeSpan.FromSeconds(10);
/// <summary>
/// This list of components that will be added for the second trigger.
/// </summary>
[DataField("components", required: true)]
public ComponentRegistry SecondStageComponents = new();
[DataField("nextTriggerTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan? NextTriggerTime;
[DataField("triggered")]
public bool Triggered = false;
[DataField("ComponentsIsLoaded")]
public bool ComponentsIsLoaded = false;
}

View File

@@ -45,4 +45,10 @@ public sealed partial class ProjectileGrenadeComponent : Component
/// </summary>
[DataField]
public float MaxVelocity = 6f;
/// <summary>
/// The trigger key that will activate the grenade.
/// </summary>
[DataField]
public string TriggerKey = "timer";
}

View File

@@ -1,22 +0,0 @@
using Content.Server.Explosion.EntitySystems;
namespace Content.Server.Explosion.Components;
/// <summary>
/// This is used for randomizing a <see cref="RandomTimerTriggerComponent"/> on MapInit
/// </summary>
[RegisterComponent, Access(typeof(TriggerSystem))]
public sealed partial class RandomTimerTriggerComponent : Component
{
/// <summary>
/// The minimum random trigger time.
/// </summary>
[DataField]
public float Min;
/// <summary>
/// The maximum random trigger time.
/// </summary>
[DataField]
public float Max;
}

View File

@@ -1,25 +0,0 @@
using Content.Server.Explosion.EntitySystems;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Explosion.Components;
/// <summary>
/// Constantly triggers after being added to an entity.
/// </summary>
[RegisterComponent, Access(typeof(TriggerSystem))]
[AutoGenerateComponentPause]
public sealed partial class RepeatingTriggerComponent : Component
{
/// <summary>
/// How long to wait between triggers.
/// The first trigger starts this long after the component is added.
/// </summary>
[DataField]
public TimeSpan Delay = TimeSpan.FromSeconds(1);
/// <summary>
/// When the next trigger will be.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan NextTrigger;
}

View File

@@ -1,37 +0,0 @@
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Content.Server.Explosion.EntitySystems;
namespace Content.Server.Explosion.Components;
/// <summary>
/// A component that electrocutes an entity having this component when a trigger is triggered.
/// </summary>
[RegisterComponent, AutoGenerateComponentPause]
[Access(typeof(TriggerSystem))]
public sealed partial class ShockOnTriggerComponent : Component
{
/// <summary>
/// The force of an electric shock when the trigger is triggered.
/// </summary>
[DataField]
public int Damage = 5;
/// <summary>
/// Duration of electric shock when the trigger is triggered.
/// </summary>
[DataField]
public TimeSpan Duration = TimeSpan.FromSeconds(2);
/// <summary>
/// The minimum delay between repeating triggers.
/// </summary>
[DataField]
public TimeSpan Cooldown = TimeSpan.FromSeconds(4);
/// <summary>
/// When can the trigger run again?
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan NextTrigger = TimeSpan.Zero;
}

View File

@@ -1,24 +0,0 @@
using Content.Server.Explosion.EntitySystems;
using Robust.Shared.Prototypes;
namespace Content.Server.Explosion.Components;
/// <summary>
/// Spawns a protoype when triggered.
/// </summary>
[RegisterComponent, Access(typeof(TriggerSystem))]
public sealed partial class SpawnOnTriggerComponent : Component
{
/// <summary>
/// The prototype to spawn.
/// </summary>
[DataField(required: true)]
public EntProtoId Proto = string.Empty;
/// <summary>
/// Use MapCoordinates for spawning?
/// Set to true if you don't want the new entity parented to the spawner.
/// </summary>
[DataField]
public bool mapCoords;
}

View File

@@ -1,15 +0,0 @@
using Content.Shared.DeviceLinking;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Explosion.Components
{
/// <summary>
/// Sends a trigger when signal is received.
/// </summary>
[RegisterComponent]
public sealed partial class TimerStartOnSignalComponent : Component
{
[DataField("port", customTypeSerializer: typeof(PrototypeIdSerializer<SinkPortPrototype>))]
public string Port = "Timer";
}
}

View File

@@ -1,7 +0,0 @@
namespace Content.Server.Explosion.Components;
/// <summary>
/// Triggers on click.
/// </summary>
[RegisterComponent]
public sealed partial class TriggerOnActivateComponent : Component { }

View File

@@ -1,20 +0,0 @@
namespace Content.Server.Explosion.Components;
/// <summary>
/// Triggers when colliding with another entity.
/// </summary>
[RegisterComponent]
public sealed partial class TriggerOnCollideComponent : Component
{
/// <summary>
/// The fixture with which to collide.
/// </summary>
[DataField(required: true)]
public string FixtureID = string.Empty;
/// <summary>
/// Doesn't trigger if the other colliding fixture is nonhard.
/// </summary>
[DataField]
public bool IgnoreOtherNonHard = true;
}

View File

@@ -1,24 +0,0 @@
using Content.Shared.Mobs;
namespace Content.Server.Explosion.Components;
/// <summary>
/// Use where you want something to trigger on mobstate change
/// </summary>
[RegisterComponent]
public sealed partial class TriggerOnMobstateChangeComponent : Component
{
/// <summary>
/// What state should trigger this?
/// </summary>
[ViewVariables]
[DataField("mobState", required: true)]
public List<MobState> MobState = new();
/// <summary>
/// If true, prevents suicide attempts for the trigger to prevent cheese.
/// </summary>
[ViewVariables]
[DataField("preventSuicide")]
public bool PreventSuicide = false;
}

View File

@@ -1,93 +0,0 @@
using Content.Server.Explosion.EntitySystems;
using Content.Shared.Explosion;
using Content.Shared.Explosion.Components;
using Content.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Server.Explosion.Components
{
/// <summary>
/// Raises a <see cref="TriggerEvent"/> whenever an entity collides with a fixture attached to the owner of this component.
/// </summary>
[RegisterComponent, AutoGenerateComponentPause]
public sealed partial class TriggerOnProximityComponent : SharedTriggerOnProximityComponent
{
public const string FixtureID = "trigger-on-proximity-fixture";
[ViewVariables]
public readonly Dictionary<EntityUid, PhysicsComponent> Colliding = new();
/// <summary>
/// What is the shape of the proximity fixture?
/// </summary>
[ViewVariables]
[DataField("shape")]
public IPhysShape Shape = new PhysShapeCircle(2f);
/// <summary>
/// How long the the proximity trigger animation plays for.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("animationDuration")]
public TimeSpan AnimationDuration = TimeSpan.FromSeconds(0.6f);
/// <summary>
/// Whether the entity needs to be anchored for the proximity to work.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("requiresAnchored")]
public bool RequiresAnchored = true;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("enabled")]
public bool Enabled = true;
/// <summary>
/// The minimum delay between repeating triggers.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("cooldown")]
public TimeSpan Cooldown = TimeSpan.FromSeconds(5);
/// <summary>
/// When can the trigger run again?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("nextTrigger", customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan NextTrigger = TimeSpan.Zero;
/// <summary>
/// When will the visual state be updated again after activation?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("nextVisualUpdate", customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan NextVisualUpdate = TimeSpan.Zero;
/// <summary>
/// What speed should the other object be moving at to trigger the proximity fixture?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("triggerSpeed")]
public float TriggerSpeed = 3.5f;
/// <summary>
/// If this proximity is triggered should we continually repeat it?
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("repeating")]
public bool Repeating = true;
/// <summary>
/// What layer is the trigger fixture on?
/// </summary>
[ViewVariables]
[DataField("layer", customTypeSerializer: typeof(FlagSerializer<CollisionLayer>))]
public int Layer = (int) (CollisionGroup.MidImpassable | CollisionGroup.LowImpassable | CollisionGroup.HighImpassable);
}
}

View File

@@ -1,15 +0,0 @@
using Content.Shared.DeviceLinking;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Explosion.Components
{
/// <summary>
/// Sends a trigger when signal is received.
/// </summary>
[RegisterComponent]
public sealed partial class TriggerOnSignalComponent : Component
{
[DataField("port", customTypeSerializer: typeof(PrototypeIdSerializer<SinkPortPrototype>))]
public string Port = "Trigger";
}
}

View File

@@ -1,6 +0,0 @@
namespace Content.Server.Explosion.Components;
[RegisterComponent]
public sealed partial class TriggerOnSlipComponent : Component
{
}

View File

@@ -1,9 +0,0 @@
namespace Content.Server.Explosion.Components;
/// <summary>
/// calls the trigger when the object is initialized
/// </summary>
[RegisterComponent]
public sealed partial class TriggerOnSpawnComponent : Component
{
}

View File

@@ -1,10 +0,0 @@
namespace Content.Server.Explosion.Components;
/// <summary>
/// This is used for entities that want the more generic 'trigger' behavior after a step trigger occurs.
/// Not done by default, since it's not useful for everything and might cause weird behavior. But it is useful for a lot of stuff like mousetraps.
/// </summary>
[RegisterComponent]
public sealed partial class TriggerOnStepTriggerComponent : Component
{
}

View File

@@ -1,18 +0,0 @@
namespace Content.Server.Explosion.Components;
/// <summary>
/// Triggers when the entity is overlapped for the specified duration.
/// </summary>
[RegisterComponent]
public sealed partial class TriggerOnTimedCollideComponent : Component
{
[ViewVariables(VVAccess.ReadWrite)]
[DataField("threshold")]
public float Threshold;
/// <summary>
/// A collection of entities that are colliding with this, and their own unique accumulator.
/// </summary>
[ViewVariables]
public readonly Dictionary<EntityUid, float> Colliding = new();
}

View File

@@ -1,7 +0,0 @@
namespace Content.Server.Explosion.Components;
/// <summary>
/// Triggers on use in hand.
/// </summary>
[RegisterComponent]
public sealed partial class TriggerOnUseComponent : Component { }

View File

@@ -1,28 +0,0 @@
namespace Content.Server.Explosion.Components
{
/// <summary>
/// Sends a trigger when the keyphrase is heard
/// </summary>
[RegisterComponent]
public sealed partial class TriggerOnVoiceComponent : Component
{
public bool IsListening => IsRecording || !string.IsNullOrWhiteSpace(KeyPhrase);
[ViewVariables(VVAccess.ReadWrite)]
[DataField("keyPhrase")]
public string? KeyPhrase;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("listenRange")]
public int ListenRange { get; private set; } = 4;
[DataField("isRecording")]
public bool IsRecording = false;
[DataField("minLength")]
public int MinLength = 3;
[DataField("maxLength")]
public int MaxLength = 50;
}
}

View File

@@ -1,9 +0,0 @@
namespace Content.Server.Explosion.Components;
/// <summary>
/// Triggers a gun when attempting to shoot while it's empty
/// </summary>
[RegisterComponent]
public sealed partial class TriggerWhenEmptyComponent : Component
{
}

View File

@@ -1,23 +0,0 @@
using Content.Shared.Whitelist;
namespace Content.Server.Explosion.Components;
/// <summary>
/// Checks if the user of a Trigger satisfies a whitelist and blacklist condition.
/// Cancels the trigger otherwise.
/// </summary>
[RegisterComponent]
public sealed partial class TriggerWhitelistComponent : Component
{
/// <summary>
/// Whitelist for what entites can cause this trigger.
/// </summary>
[DataField]
public EntityWhitelist? Whitelist;
/// <summary>
/// Blacklist for what entites can cause this trigger.
/// </summary>
[DataField]
public EntityWhitelist? Blacklist;
}

View File

@@ -1,5 +1,6 @@
using Content.Server.Explosion.Components;
using Content.Server.Weapons.Ranged.Systems;
using Content.Shared.Trigger;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.Map;
@@ -45,6 +46,9 @@ public sealed class ProjectileGrenadeSystem : EntitySystem
/// </summary>
private void OnFragTrigger(Entity<ProjectileGrenadeComponent> entity, ref TriggerEvent args)
{
if (args.Key != entity.Comp.TriggerKey)
return;
FragmentIntoProjectiles(entity.Owner, entity.Comp);
args.Handled = true;
}

View File

@@ -1,79 +0,0 @@
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Explosion.Components.OnTrigger;
using Content.Shared.Explosion.EntitySystems;
using Robust.Shared.Timing;
namespace Content.Server.Explosion.EntitySystems;
/// <summary>
/// Releases a gas mixture to the atmosphere when triggered.
/// Can also release gas over a set timespan to prevent trolling people
/// with the instant-wall-of-pressure-inator.
/// </summary>
public sealed partial class ReleaseGasOnTriggerSystem : SharedReleaseGasOnTriggerSystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ReleaseGasOnTriggerComponent, TriggerEvent>(OnTrigger);
}
/// <summary>
/// Shrimply sets the component to active when triggered, allowing it to release over time.
/// </summary>
private void OnTrigger(Entity<ReleaseGasOnTriggerComponent> ent, ref TriggerEvent args)
{
ent.Comp.Active = true;
ent.Comp.NextReleaseTime = _timing.CurTime;
ent.Comp.StartingTotalMoles = ent.Comp.Air.TotalMoles;
UpdateAppearance(ent.Owner, true);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var curTime = _timing.CurTime;
var query = EntityQueryEnumerator<ReleaseGasOnTriggerComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (!comp.Active || comp.NextReleaseTime > curTime)
continue;
var giverGasMix = comp.Air.Remove(comp.StartingTotalMoles * comp.RemoveFraction);
var environment = _atmosphereSystem.GetContainingMixture(uid, false, true);
if (environment == null)
{
UpdateAppearance(uid, false);
RemCompDeferred<ReleaseGasOnTriggerComponent>(uid);
continue;
}
_atmosphereSystem.Merge(environment, giverGasMix);
comp.NextReleaseTime += comp.ReleaseInterval;
if (comp.PressureLimit != 0 && environment.Pressure >= comp.PressureLimit ||
comp.Air.TotalMoles <= 0)
{
UpdateAppearance(uid, false);
RemCompDeferred<ReleaseGasOnTriggerComponent>(uid);
continue;
}
}
}
private void UpdateAppearance(Entity<AppearanceComponent?> entity, bool state)
{
if (!Resolve(entity, ref entity.Comp, false))
return;
_appearance.SetData(entity, ReleaseGasOnTriggerVisuals.Key, state);
}
}

View File

@@ -1,29 +0,0 @@
using Content.Shared.Explosion.Components.OnTrigger;
using Content.Shared.Explosion.EntitySystems;
using Content.Shared.RepulseAttract;
using Content.Shared.Timing;
namespace Content.Server.Explosion.EntitySystems;
public sealed class RepulseAttractOnTriggerSystem : SharedRepulseAttractOnTriggerSystem
{
[Dependency] private readonly RepulseAttractSystem _repulse = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly UseDelaySystem _delay = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SharedRepulseAttractOnTriggerComponent, TriggerEvent>(OnTrigger);
}
private void OnTrigger(Entity<SharedRepulseAttractOnTriggerComponent> ent, ref TriggerEvent args)
{
if (_delay.IsDelayed(ent.Owner))
return;
var position = _transform.GetMapCoordinates(ent);
_repulse.TryRepulseAttract(position, args.User, ent.Comp.Speed, ent.Comp.Range, ent.Comp.Whitelist, ent.Comp.CollisionMask);
}
}

View File

@@ -1,5 +1,8 @@
using Content.Shared.Explosion.Components;
using Content.Shared.Throwing;
using Content.Shared.Trigger;
using Content.Shared.Trigger.Systems;
using Content.Shared.Trigger.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
using Robust.Shared.Map;
@@ -15,6 +18,7 @@ public sealed class ScatteringGrenadeSystem : SharedScatteringGrenadeSystem
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ThrowingSystem _throwingSystem = default!;
[Dependency] private readonly TransformSystem _transformSystem = default!;
[Dependency] private readonly TriggerSystem _trigger = default!;
public override void Initialize()
{
@@ -30,6 +34,9 @@ public sealed class ScatteringGrenadeSystem : SharedScatteringGrenadeSystem
/// </summary>
private void OnScatteringTrigger(Entity<ScatteringGrenadeComponent> entity, ref TriggerEvent args)
{
if (args.Key != entity.Comp.TriggerKey)
return;
entity.Comp.IsTriggered = true;
args.Handled = true;
}
@@ -76,13 +83,12 @@ public sealed class ScatteringGrenadeSystem : SharedScatteringGrenadeSystem
_throwingSystem.TryThrow(contentUid, direction, component.Velocity);
if (component.TriggerContents)
if (component.TriggerContents && TryComp<TimerTriggerComponent>(contentUid, out var contentTimer))
{
additionalIntervalDelay += _random.NextFloat(component.IntervalBetweenTriggersMin, component.IntervalBetweenTriggersMax);
var contentTimer = EnsureComp<ActiveTimerTriggerComponent>(contentUid);
contentTimer.TimeRemaining = component.DelayBeforeTriggerContents + additionalIntervalDelay;
var ev = new ActiveTimerTriggerEvent(contentUid, uid);
RaiseLocalEvent(contentUid, ref ev);
_trigger.SetDelay((contentUid, contentTimer), TimeSpan.FromSeconds(component.DelayBeforeTriggerContents + additionalIntervalDelay));
_trigger.ActivateTimerTrigger((contentUid, contentTimer));
}
}

View File

@@ -1,57 +0,0 @@
using Content.Shared.Explosion.Components;
using Content.Shared.Explosion.EntitySystems;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Spreader;
using Content.Shared.Chemistry.Components;
using Content.Shared.Coordinates.Helpers;
using Content.Shared.Maps;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
namespace Content.Server.Explosion.EntitySystems;
/// <summary>
/// Handles creating smoke when <see cref="SmokeOnTriggerComponent"/> is triggered.
/// </summary>
public sealed class SmokeOnTriggerSystem : SharedSmokeOnTriggerSystem
{
[Dependency] private readonly IMapManager _mapMan = default!;
[Dependency] private readonly SharedMapSystem _map = default!;
[Dependency] private readonly SmokeSystem _smoke = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly SpreaderSystem _spreader = default!;
[Dependency] private readonly TurfSystem _turf = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SmokeOnTriggerComponent, TriggerEvent>(OnTrigger);
}
private void OnTrigger(EntityUid uid, SmokeOnTriggerComponent comp, TriggerEvent args)
{
var xform = Transform(uid);
var mapCoords = _transform.GetMapCoordinates(uid, xform);
if (!_mapMan.TryFindGridAt(mapCoords, out var gridUid, out var grid) ||
!_map.TryGetTileRef(gridUid, grid, xform.Coordinates, out var tileRef) ||
tileRef.Tile.IsEmpty)
{
return;
}
if (_spreader.RequiresFloorToSpread(comp.SmokePrototype.ToString()) && _turf.IsSpace(tileRef))
return;
var coords = _map.MapToGrid(gridUid, mapCoords);
var ent = Spawn(comp.SmokePrototype, coords.SnapToGrid());
if (!TryComp<SmokeComponent>(ent, out var smoke))
{
Log.Error($"Smoke prototype {comp.SmokePrototype} was missing SmokeComponent");
Del(ent);
return;
}
_smoke.StartSmoke(ent, comp.Solution, comp.Duration, comp.SpreadAmount, smoke);
}
}

View File

@@ -1,66 +0,0 @@
using Content.Server.Explosion.Components;
using Content.Shared.Explosion.Components;
using Content.Shared.Implants;
using Content.Shared.Interaction.Events;
using Content.Shared.Mobs;
namespace Content.Server.Explosion.EntitySystems;
public sealed partial class TriggerSystem
{
private void InitializeMobstate()
{
SubscribeLocalEvent<TriggerOnMobstateChangeComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<TriggerOnMobstateChangeComponent, SuicideEvent>(OnSuicide);
SubscribeLocalEvent<TriggerOnMobstateChangeComponent, ImplantRelayEvent<SuicideEvent>>(OnSuicideRelay);
SubscribeLocalEvent<TriggerOnMobstateChangeComponent, ImplantRelayEvent<MobStateChangedEvent>>(OnMobStateRelay);
}
private void OnMobStateChanged(EntityUid uid, TriggerOnMobstateChangeComponent component, MobStateChangedEvent args)
{
if (!component.MobState.Contains(args.NewMobState))
return;
//This chains Mobstate Changed triggers with OnUseTimerTrigger if they have it
//Very useful for things that require a mobstate change and a timer
if (TryComp<OnUseTimerTriggerComponent>(uid, out var timerTrigger))
{
HandleTimerTrigger(
uid,
args.Origin,
timerTrigger.Delay,
timerTrigger.BeepInterval,
timerTrigger.InitialBeepDelay,
timerTrigger.BeepSound);
}
else
Trigger(uid);
}
/// <summary>
/// Checks if the user has any implants that prevent suicide to avoid some cheesy strategies
/// Prevents suicide by handling the event without killing the user
/// </summary>
private void OnSuicide(EntityUid uid, TriggerOnMobstateChangeComponent component, SuicideEvent args)
{
if (args.Handled)
return;
if (!component.PreventSuicide)
return;
_popupSystem.PopupEntity(Loc.GetString("suicide-prevented"), args.Victim, args.Victim);
args.Handled = true;
}
private void OnSuicideRelay(EntityUid uid, TriggerOnMobstateChangeComponent component, ImplantRelayEvent<SuicideEvent> args)
{
OnSuicide(uid, component, args.Event);
}
private void OnMobStateRelay(EntityUid uid, TriggerOnMobstateChangeComponent component, ImplantRelayEvent<MobStateChangedEvent> args)
{
OnMobStateChanged(uid, component, args.Event);
}
}

View File

@@ -1,170 +0,0 @@
using Content.Server.Explosion.Components;
using Content.Shared.Examine;
using Content.Shared.Explosion.Components;
using Content.Shared.Interaction.Events;
using Content.Shared.Popups;
using Content.Shared.Sticky;
using Content.Shared.Verbs;
namespace Content.Server.Explosion.EntitySystems;
public sealed partial class TriggerSystem
{
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
private void InitializeOnUse()
{
SubscribeLocalEvent<OnUseTimerTriggerComponent, UseInHandEvent>(OnTimerUse);
SubscribeLocalEvent<OnUseTimerTriggerComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<OnUseTimerTriggerComponent, GetVerbsEvent<AlternativeVerb>>(OnGetAltVerbs);
SubscribeLocalEvent<OnUseTimerTriggerComponent, EntityStuckEvent>(OnStuck);
SubscribeLocalEvent<RandomTimerTriggerComponent, MapInitEvent>(OnRandomTimerTriggerMapInit);
}
private void OnStuck(EntityUid uid, OnUseTimerTriggerComponent component, ref EntityStuckEvent args)
{
if (!component.StartOnStick)
return;
StartTimer((uid, component), args.User);
}
private void OnExamined(EntityUid uid, OnUseTimerTriggerComponent component, ExaminedEvent args)
{
if (args.IsInDetailsRange && component.Examinable)
args.PushText(Loc.GetString("examine-trigger-timer", ("time", component.Delay)));
}
/// <summary>
/// Add an alt-click interaction that cycles through delays.
/// </summary>
private void OnGetAltVerbs(EntityUid uid, OnUseTimerTriggerComponent component, GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanInteract || !args.CanAccess || args.Hands == null)
return;
if (component.UseVerbInstead)
{
args.Verbs.Add(new AlternativeVerb()
{
Text = Loc.GetString("verb-start-detonation"),
Act = () => StartTimer((uid, component), args.User),
Priority = 2
});
}
if (component.AllowToggleStartOnStick)
{
args.Verbs.Add(new AlternativeVerb()
{
Text = Loc.GetString("verb-toggle-start-on-stick"),
Act = () => ToggleStartOnStick(uid, args.User, component)
});
}
if (component.DelayOptions == null || component.DelayOptions.Count == 1)
return;
args.Verbs.Add(new AlternativeVerb()
{
Category = TimerOptions,
Text = Loc.GetString("verb-trigger-timer-cycle"),
Act = () => CycleDelay(component, args.User),
Priority = 1
});
foreach (var option in component.DelayOptions)
{
if (MathHelper.CloseTo(option, component.Delay))
{
args.Verbs.Add(new AlternativeVerb()
{
Category = TimerOptions,
Text = Loc.GetString("verb-trigger-timer-set-current", ("time", option)),
Disabled = true,
Priority = (int) (-100 * option)
});
continue;
}
args.Verbs.Add(new AlternativeVerb()
{
Category = TimerOptions,
Text = Loc.GetString("verb-trigger-timer-set", ("time", option)),
Priority = (int) (-100 * option),
Act = () =>
{
component.Delay = option;
_popupSystem.PopupEntity(Loc.GetString("popup-trigger-timer-set", ("time", option)), args.User, args.User);
},
});
}
}
private void OnRandomTimerTriggerMapInit(Entity<RandomTimerTriggerComponent> ent, ref MapInitEvent args)
{
var (_, comp) = ent;
if (!TryComp<OnUseTimerTriggerComponent>(ent, out var timerTriggerComp))
return;
timerTriggerComp.Delay = _random.NextFloat(comp.Min, comp.Max);
}
private void CycleDelay(OnUseTimerTriggerComponent component, EntityUid user)
{
if (component.DelayOptions == null || component.DelayOptions.Count == 1)
return;
// This is somewhat inefficient, but its good enough. This is run rarely, and the lists should be short.
component.DelayOptions.Sort();
if (component.DelayOptions[^1] <= component.Delay)
{
component.Delay = component.DelayOptions[0];
_popupSystem.PopupEntity(Loc.GetString("popup-trigger-timer-set", ("time", component.Delay)), user, user);
return;
}
foreach (var option in component.DelayOptions)
{
if (option > component.Delay)
{
component.Delay = option;
_popupSystem.PopupEntity(Loc.GetString("popup-trigger-timer-set", ("time", option)), user, user);
return;
}
}
}
private void ToggleStartOnStick(EntityUid grenade, EntityUid user, OnUseTimerTriggerComponent comp)
{
if (comp.StartOnStick)
{
comp.StartOnStick = false;
_popupSystem.PopupEntity(Loc.GetString("popup-start-on-stick-off"), grenade, user);
}
else
{
comp.StartOnStick = true;
_popupSystem.PopupEntity(Loc.GetString("popup-start-on-stick-on"), grenade, user);
}
}
private void OnTimerUse(EntityUid uid, OnUseTimerTriggerComponent component, UseInHandEvent args)
{
if (args.Handled || HasComp<AutomatedTimerComponent>(uid) || component.UseVerbInstead)
return;
if (component.DoPopup)
_popupSystem.PopupEntity(Loc.GetString("trigger-activated", ("device", uid)), args.User, args.User);
StartTimer((uid, component), args.User);
args.Handled = true;
}
public static VerbCategory TimerOptions = new("verb-categories-timer", "/Textures/Interface/VerbIcons/clock.svg.192dpi.png");
}

View File

@@ -1,154 +0,0 @@
using Content.Server.Explosion.Components;
using Content.Shared.Trigger;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events;
using Robust.Shared.Utility;
using Robust.Shared.Timing;
namespace Content.Server.Explosion.EntitySystems;
public sealed partial class TriggerSystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
private void InitializeProximity()
{
SubscribeLocalEvent<TriggerOnProximityComponent, StartCollideEvent>(OnProximityStartCollide);
SubscribeLocalEvent<TriggerOnProximityComponent, EndCollideEvent>(OnProximityEndCollide);
SubscribeLocalEvent<TriggerOnProximityComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<TriggerOnProximityComponent, ComponentShutdown>(OnProximityShutdown);
// Shouldn't need re-anchoring.
SubscribeLocalEvent<TriggerOnProximityComponent, AnchorStateChangedEvent>(OnProximityAnchor);
}
private void OnProximityAnchor(EntityUid uid, TriggerOnProximityComponent component, ref AnchorStateChangedEvent args)
{
component.Enabled = !component.RequiresAnchored ||
args.Anchored;
SetProximityAppearance(uid, component);
if (!component.Enabled)
{
component.Colliding.Clear();
}
// Re-check for contacts as we cleared them.
else if (TryComp<PhysicsComponent>(uid, out var body))
{
_broadphase.RegenerateContacts((uid, body));
}
}
private void OnProximityShutdown(EntityUid uid, TriggerOnProximityComponent component, ComponentShutdown args)
{
component.Colliding.Clear();
}
private void OnMapInit(EntityUid uid, TriggerOnProximityComponent component, MapInitEvent args)
{
component.Enabled = !component.RequiresAnchored ||
Transform(uid).Anchored;
SetProximityAppearance(uid, component);
if (!TryComp<PhysicsComponent>(uid, out var body))
return;
_fixtures.TryCreateFixture(
uid,
component.Shape,
TriggerOnProximityComponent.FixtureID,
hard: false,
body: body,
collisionLayer: component.Layer);
}
private void OnProximityStartCollide(EntityUid uid, TriggerOnProximityComponent component, ref StartCollideEvent args)
{
if (args.OurFixtureId != TriggerOnProximityComponent.FixtureID)
return;
component.Colliding[args.OtherEntity] = args.OtherBody;
}
private static void OnProximityEndCollide(EntityUid uid, TriggerOnProximityComponent component, ref EndCollideEvent args)
{
if (args.OurFixtureId != TriggerOnProximityComponent.FixtureID)
return;
component.Colliding.Remove(args.OtherEntity);
}
private void SetProximityAppearance(EntityUid uid, TriggerOnProximityComponent component)
{
if (TryComp(uid, out AppearanceComponent? appearance))
{
_appearance.SetData(uid, ProximityTriggerVisualState.State, component.Enabled ? ProximityTriggerVisuals.Inactive : ProximityTriggerVisuals.Off, appearance);
}
}
private void Activate(EntityUid uid, EntityUid user, TriggerOnProximityComponent component)
{
DebugTools.Assert(component.Enabled);
var curTime = _timing.CurTime;
if (!component.Repeating)
{
component.Enabled = false;
component.Colliding.Clear();
}
else
{
component.NextTrigger = curTime + component.Cooldown;
}
// Queue a visual update for when the animation is complete.
component.NextVisualUpdate = curTime + component.AnimationDuration;
if (TryComp(uid, out AppearanceComponent? appearance))
{
_appearance.SetData(uid, ProximityTriggerVisualState.State, ProximityTriggerVisuals.Active, appearance);
}
Trigger(uid, user);
}
private void UpdateProximity()
{
var curTime = _timing.CurTime;
var query = EntityQueryEnumerator<TriggerOnProximityComponent>();
while (query.MoveNext(out var uid, out var trigger))
{
if (curTime >= trigger.NextVisualUpdate)
{
// Update the visual state once the animation is done.
trigger.NextVisualUpdate = TimeSpan.MaxValue;
SetProximityAppearance(uid, trigger);
}
if (!trigger.Enabled)
continue;
if (curTime < trigger.NextTrigger)
// The trigger's on cooldown.
continue;
// Check for anything colliding and moving fast enough.
foreach (var (collidingUid, colliding) in trigger.Colliding)
{
if (Deleted(collidingUid))
continue;
if (colliding.LinearVelocity.Length() < trigger.TriggerSpeed)
continue;
// Trigger!
Activate(uid, collidingUid, trigger);
break;
}
}
}
}

View File

@@ -1,43 +0,0 @@
using Content.Server.DeviceLinking.Systems;
using Content.Server.Explosion.Components;
using Content.Shared.DeviceLinking.Events;
namespace Content.Server.Explosion.EntitySystems
{
public sealed partial class TriggerSystem
{
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
private void InitializeSignal()
{
SubscribeLocalEvent<TriggerOnSignalComponent,SignalReceivedEvent>(OnSignalReceived);
SubscribeLocalEvent<TriggerOnSignalComponent,ComponentInit>(OnInit);
SubscribeLocalEvent<TimerStartOnSignalComponent,SignalReceivedEvent>(OnTimerSignalReceived);
SubscribeLocalEvent<TimerStartOnSignalComponent,ComponentInit>(OnTimerSignalInit);
}
private void OnSignalReceived(EntityUid uid, TriggerOnSignalComponent component, ref SignalReceivedEvent args)
{
if (args.Port != component.Port)
return;
Trigger(uid, args.Trigger);
}
private void OnInit(EntityUid uid, TriggerOnSignalComponent component, ComponentInit args)
{
_signalSystem.EnsureSinkPorts(uid, component.Port);
}
private void OnTimerSignalReceived(EntityUid uid, TimerStartOnSignalComponent component, ref SignalReceivedEvent args)
{
if (args.Port != component.Port)
return;
StartTimer(uid, args.Trigger);
}
private void OnTimerSignalInit(EntityUid uid, TimerStartOnSignalComponent component, ComponentInit args)
{
_signalSystem.EnsureSinkPorts(uid, component.Port);
}
}
}

View File

@@ -1,59 +0,0 @@
using System.Linq;
using Content.Server.Explosion.Components;
using Content.Server.Explosion.EntitySystems;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Events;
namespace Content.Server.Explosion.EntitySystems;
public sealed partial class TriggerSystem
{
private void InitializeTimedCollide()
{
SubscribeLocalEvent<TriggerOnTimedCollideComponent, StartCollideEvent>(OnTimerCollide);
SubscribeLocalEvent<TriggerOnTimedCollideComponent, EndCollideEvent>(OnTimerEndCollide);
SubscribeLocalEvent<TriggerOnTimedCollideComponent, ComponentRemove>(OnComponentRemove);
}
private void OnTimerCollide(EntityUid uid, TriggerOnTimedCollideComponent component, ref StartCollideEvent args)
{
//Ensures the entity trigger will have an active component
EnsureComp<ActiveTriggerOnTimedCollideComponent>(uid);
var otherUID = args.OtherEntity;
if (component.Colliding.ContainsKey(otherUID))
return;
component.Colliding.Add(otherUID, 0);
}
private void OnTimerEndCollide(EntityUid uid, TriggerOnTimedCollideComponent component, ref EndCollideEvent args)
{
var otherUID = args.OtherEntity;
component.Colliding.Remove(otherUID);
if (component.Colliding.Count == 0 && HasComp<ActiveTriggerOnTimedCollideComponent>(uid))
RemComp<ActiveTriggerOnTimedCollideComponent>(uid);
}
private void OnComponentRemove(EntityUid uid, TriggerOnTimedCollideComponent component, ComponentRemove args)
{
if (HasComp<ActiveTriggerOnTimedCollideComponent>(uid))
RemComp<ActiveTriggerOnTimedCollideComponent>(uid);
}
private void UpdateTimedCollide(float frameTime)
{
var query = EntityQueryEnumerator<ActiveTriggerOnTimedCollideComponent, TriggerOnTimedCollideComponent>();
while (query.MoveNext(out var uid, out _, out var triggerOnTimedCollide))
{
foreach (var (collidingEntity, collidingTimer) in triggerOnTimedCollide.Colliding)
{
triggerOnTimedCollide.Colliding[collidingEntity] += frameTime;
if (collidingTimer > triggerOnTimedCollide.Threshold)
{
RaiseLocalEvent(uid, new TriggerEvent(uid, collidingEntity), true);
triggerOnTimedCollide.Colliding[collidingEntity] -= triggerOnTimedCollide.Threshold;
}
}
}
}
}

View File

@@ -1,154 +0,0 @@
using Content.Server.Explosion.Components;
using Content.Server.Speech;
using Content.Server.Speech.Components;
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.Verbs;
namespace Content.Server.Explosion.EntitySystems
{
public sealed partial class TriggerSystem
{
private void InitializeVoice()
{
SubscribeLocalEvent<TriggerOnVoiceComponent, ComponentInit>(OnVoiceInit);
SubscribeLocalEvent<TriggerOnVoiceComponent, ExaminedEvent>(OnVoiceExamine);
SubscribeLocalEvent<TriggerOnVoiceComponent, GetVerbsEvent<AlternativeVerb>>(OnVoiceGetAltVerbs);
SubscribeLocalEvent<TriggerOnVoiceComponent, ListenEvent>(OnListen);
}
private void OnVoiceInit(EntityUid uid, TriggerOnVoiceComponent component, ComponentInit args)
{
if (component.IsListening)
EnsureComp<ActiveListenerComponent>(uid).Range = component.ListenRange;
else
RemCompDeferred<ActiveListenerComponent>(uid);
}
private void OnListen(Entity<TriggerOnVoiceComponent> ent, ref ListenEvent args)
{
var component = ent.Comp;
var message = args.Message.Trim();
if (component.IsRecording)
{
var ev = new ListenAttemptEvent(args.Source);
RaiseLocalEvent(ent, ev);
if (ev.Cancelled)
return;
if (message.Length >= component.MinLength && message.Length <= component.MaxLength)
FinishRecording(ent, args.Source, args.Message);
else if (message.Length > component.MaxLength)
_popupSystem.PopupEntity(Loc.GetString("popup-trigger-voice-record-failed-too-long"), ent);
else if (message.Length < component.MinLength)
_popupSystem.PopupEntity(Loc.GetString("popup-trigger-voice-record-failed-too-short"), ent);
return;
}
if (!string.IsNullOrWhiteSpace(component.KeyPhrase) && message.IndexOf(component.KeyPhrase, StringComparison.InvariantCultureIgnoreCase) is var index and >= 0 )
{
_adminLogger.Add(LogType.Trigger, LogImpact.Medium,
$"A voice-trigger on {ToPrettyString(ent):entity} was triggered by {ToPrettyString(args.Source):speaker} speaking the key-phrase {component.KeyPhrase}.");
Trigger(ent, args.Source);
var messageWithoutPhrase = message.Remove(index, component.KeyPhrase.Length).Trim();
var voice = new VoiceTriggeredEvent(args.Source, message, messageWithoutPhrase);
RaiseLocalEvent(ent, ref voice);
}
}
private void OnVoiceGetAltVerbs(Entity<TriggerOnVoiceComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanInteract || !args.CanAccess)
return;
var component = ent.Comp;
var @event = args;
args.Verbs.Add(new AlternativeVerb()
{
Text = Loc.GetString(component.IsRecording ? "verb-trigger-voice-stop" : "verb-trigger-voice-record"),
Act = () =>
{
if (component.IsRecording)
StopRecording(ent);
else
StartRecording(ent, @event.User);
},
Priority = 1
});
if (string.IsNullOrWhiteSpace(component.KeyPhrase))
return;
args.Verbs.Add(new AlternativeVerb()
{
Text = Loc.GetString("verb-trigger-voice-clear"),
Act = () =>
{
component.KeyPhrase = null;
component.IsRecording = false;
RemComp<ActiveListenerComponent>(ent);
}
});
}
public void StartRecording(Entity<TriggerOnVoiceComponent> ent, EntityUid user)
{
var component = ent.Comp;
component.IsRecording = true;
EnsureComp<ActiveListenerComponent>(ent).Range = component.ListenRange;
_adminLogger.Add(LogType.Trigger, LogImpact.Low,
$"A voice-trigger on {ToPrettyString(ent):entity} has started recording. User: {ToPrettyString(user):user}");
_popupSystem.PopupEntity(Loc.GetString("popup-trigger-voice-start-recording"), ent);
}
public void StopRecording(Entity<TriggerOnVoiceComponent> ent)
{
var component = ent.Comp;
component.IsRecording = false;
if (string.IsNullOrWhiteSpace(component.KeyPhrase))
RemComp<ActiveListenerComponent>(ent);
_popupSystem.PopupEntity(Loc.GetString("popup-trigger-voice-stop-recording"), ent);
}
public void FinishRecording(Entity<TriggerOnVoiceComponent> ent, EntityUid source, string message)
{
var component = ent.Comp;
component.KeyPhrase = message;
component.IsRecording = false;
_adminLogger.Add(LogType.Trigger, LogImpact.Low,
$"A voice-trigger on {ToPrettyString(ent):entity} has recorded a new keyphrase: '{component.KeyPhrase}'. Recorded from {ToPrettyString(source):speaker}");
_popupSystem.PopupEntity(Loc.GetString("popup-trigger-voice-recorded", ("keyphrase", component.KeyPhrase!)), ent);
}
private void OnVoiceExamine(EntityUid uid, TriggerOnVoiceComponent component, ExaminedEvent args)
{
if (args.IsInDetailsRange)
{
args.PushText(string.IsNullOrWhiteSpace(component.KeyPhrase)
? Loc.GetString("trigger-voice-uninitialized")
: Loc.GetString("examine-trigger-voice", ("keyphrase", component.KeyPhrase)));
}
}
}
}
/// <summary>
/// Raised when a voice trigger is activated, containing the message that triggered it.
/// </summary>
/// <param name="Source"> The EntityUid of the entity sending the message</param>
/// <param name="Message"> The contents of the message</param>
/// <param name="MessageWithoutPhrase"> The message without the phrase that triggered it.</param>
[ByRefEvent]
public readonly record struct VoiceTriggeredEvent(EntityUid Source, string Message, string MessageWithoutPhrase);

View File

@@ -1,455 +0,0 @@
using Content.Server.Administration.Logs;
using Content.Server.Body.Systems;
using Content.Server.Explosion.Components;
using Content.Shared.Flash;
using Content.Server.Electrocution;
using Content.Server.Pinpointer;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Flash.Components;
using Content.Server.Radio.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Database;
using Content.Shared.Explosion.Components;
using Content.Shared.Explosion.Components.OnTrigger;
using Content.Shared.Implants.Components;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Inventory;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Payload.Components;
using Content.Shared.Radio;
using Content.Shared.Slippery;
using Content.Shared.StepTrigger.Systems;
using Content.Shared.Trigger;
using Content.Shared.Weapons.Ranged.Events;
using Content.Shared.Whitelist;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Physics.Events;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Explosion.EntitySystems
{
/// <summary>
/// Raised whenever something is Triggered on the entity.
/// </summary>
public sealed class TriggerEvent : HandledEntityEventArgs
{
public EntityUid Triggered { get; }
public EntityUid? User { get; }
public TriggerEvent(EntityUid triggered, EntityUid? user = null)
{
Triggered = triggered;
User = user;
}
}
/// <summary>
/// Raised before a trigger is activated.
/// </summary>
[ByRefEvent]
public record struct BeforeTriggerEvent(EntityUid Triggered, EntityUid? User, bool Cancelled = false);
/// <summary>
/// Raised when timer trigger becomes active.
/// </summary>
[ByRefEvent]
public readonly record struct ActiveTimerTriggerEvent(EntityUid Triggered, EntityUid? User);
[UsedImplicitly]
public sealed partial class TriggerSystem : EntitySystem
{
[Dependency] private readonly ExplosionSystem _explosions = default!;
[Dependency] private readonly FixtureSystem _fixtures = default!;
[Dependency] private readonly SharedFlashSystem _flashSystem = default!;
[Dependency] private readonly SharedBroadphaseSystem _broadphase = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly BodySystem _body = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
[Dependency] private readonly NavMapSystem _navMap = default!;
[Dependency] private readonly RadioSystem _radioSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
[Dependency] private readonly ElectrocutionSystem _electrocution = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
public override void Initialize()
{
base.Initialize();
InitializeProximity();
InitializeOnUse();
InitializeSignal();
InitializeTimedCollide();
InitializeVoice();
InitializeMobstate();
InitializeDamageReceived(); //CP14 trigger on damage partial
SubscribeLocalEvent<TriggerOnSpawnComponent, MapInitEvent>(OnSpawnTriggered);
SubscribeLocalEvent<TriggerOnCollideComponent, StartCollideEvent>(OnTriggerCollide);
SubscribeLocalEvent<TriggerOnActivateComponent, ActivateInWorldEvent>(OnActivate);
SubscribeLocalEvent<TriggerOnUseComponent, UseInHandEvent>(OnUse);
SubscribeLocalEvent<TriggerImplantActionComponent, ActivateImplantEvent>(OnImplantTrigger);
SubscribeLocalEvent<TriggerOnStepTriggerComponent, StepTriggeredOffEvent>(OnStepTriggered);
SubscribeLocalEvent<TriggerOnSlipComponent, SlipEvent>(OnSlipTriggered);
SubscribeLocalEvent<TriggerWhenEmptyComponent, OnEmptyGunShotEvent>(OnEmptyTriggered);
SubscribeLocalEvent<RepeatingTriggerComponent, MapInitEvent>(OnRepeatInit);
SubscribeLocalEvent<SpawnOnTriggerComponent, TriggerEvent>(OnSpawnTrigger);
SubscribeLocalEvent<DeleteOnTriggerComponent, TriggerEvent>(HandleDeleteTrigger);
SubscribeLocalEvent<ExplodeOnTriggerComponent, TriggerEvent>(HandleExplodeTrigger);
SubscribeLocalEvent<FlashOnTriggerComponent, TriggerEvent>(HandleFlashTrigger);
SubscribeLocalEvent<GibOnTriggerComponent, TriggerEvent>(HandleGibTrigger);
SubscribeLocalEvent<AnchorOnTriggerComponent, TriggerEvent>(OnAnchorTrigger);
SubscribeLocalEvent<SoundOnTriggerComponent, TriggerEvent>(OnSoundTrigger);
SubscribeLocalEvent<ShockOnTriggerComponent, TriggerEvent>(HandleShockTrigger);
SubscribeLocalEvent<RattleComponent, TriggerEvent>(HandleRattleTrigger);
SubscribeLocalEvent<TriggerWhitelistComponent, BeforeTriggerEvent>(HandleWhitelist);
}
private void HandleWhitelist(Entity<TriggerWhitelistComponent> ent, ref BeforeTriggerEvent args)
{
args.Cancelled = !_whitelist.CheckBoth(args.User, ent.Comp.Blacklist, ent.Comp.Whitelist);
}
private void OnSoundTrigger(EntityUid uid, SoundOnTriggerComponent component, TriggerEvent args)
{
if (component.RemoveOnTrigger) // if the component gets removed when it's triggered
{
var xform = Transform(uid);
_audio.PlayPvs(component.Sound, xform.Coordinates); // play the sound at its last known coordinates
}
else // if the component doesn't get removed when triggered
{
_audio.PlayPvs(component.Sound, uid); // have the sound follow the entity itself
}
}
private void HandleShockTrigger(Entity<ShockOnTriggerComponent> shockOnTrigger, ref TriggerEvent args)
{
if (!_container.TryGetContainingContainer(shockOnTrigger.Owner, out var container))
return;
var containerEnt = container.Owner;
var curTime = _timing.CurTime;
if (curTime < shockOnTrigger.Comp.NextTrigger)
{
// The trigger's on cooldown.
return;
}
_electrocution.TryDoElectrocution(containerEnt, null, shockOnTrigger.Comp.Damage, shockOnTrigger.Comp.Duration, true, ignoreInsulation: true);
shockOnTrigger.Comp.NextTrigger = curTime + shockOnTrigger.Comp.Cooldown;
}
private void OnAnchorTrigger(EntityUid uid, AnchorOnTriggerComponent component, TriggerEvent args)
{
var xform = Transform(uid);
if (xform.Anchored)
return;
_transformSystem.AnchorEntity(uid, xform);
if (component.RemoveOnTrigger)
RemCompDeferred<AnchorOnTriggerComponent>(uid);
}
private void OnSpawnTrigger(Entity<SpawnOnTriggerComponent> ent, ref TriggerEvent args)
{
var xform = Transform(ent);
if (ent.Comp.mapCoords)
{
var mapCoords = _transformSystem.GetMapCoordinates(ent, xform);
Spawn(ent.Comp.Proto, mapCoords);
}
else
{
var coords = xform.Coordinates;
if (!coords.IsValid(EntityManager))
return;
Spawn(ent.Comp.Proto, coords);
}
}
private void HandleExplodeTrigger(EntityUid uid, ExplodeOnTriggerComponent component, TriggerEvent args)
{
_explosions.TriggerExplosive(uid, user: args.User);
args.Handled = true;
}
private void HandleFlashTrigger(EntityUid uid, FlashOnTriggerComponent component, TriggerEvent args)
{
_flashSystem.FlashArea(uid, args.User, component.Range, component.Duration, probability: component.Probability);
args.Handled = true;
}
private void HandleDeleteTrigger(EntityUid uid, DeleteOnTriggerComponent component, TriggerEvent args)
{
QueueDel(uid);
args.Handled = true;
}
private void HandleGibTrigger(EntityUid uid, GibOnTriggerComponent component, TriggerEvent args)
{
if (!TryComp(uid, out TransformComponent? xform))
return;
if (component.DeleteItems)
{
var items = _inventory.GetHandOrInventoryEntities(xform.ParentUid);
foreach (var item in items)
{
Del(item);
}
}
_body.GibBody(xform.ParentUid, true);
args.Handled = true;
}
private void HandleRattleTrigger(EntityUid uid, RattleComponent component, TriggerEvent args)
{
if (!TryComp<SubdermalImplantComponent>(uid, out var implanted))
return;
if (implanted.ImplantedEntity == null)
return;
// Gets location of the implant
var posText = FormattedMessage.RemoveMarkupOrThrow(_navMap.GetNearestBeaconString(uid));
var critMessage = Loc.GetString(component.CritMessage, ("user", implanted.ImplantedEntity.Value), ("position", posText));
var deathMessage = Loc.GetString(component.DeathMessage, ("user", implanted.ImplantedEntity.Value), ("position", posText));
if (!TryComp<MobStateComponent>(implanted.ImplantedEntity, out var mobstate))
return;
// Sends a message to the radio channel specified by the implant
if (mobstate.CurrentState == MobState.Critical)
_radioSystem.SendRadioMessage(uid, critMessage, _prototypeManager.Index<RadioChannelPrototype>(component.RadioChannel), uid);
if (mobstate.CurrentState == MobState.Dead)
_radioSystem.SendRadioMessage(uid, deathMessage, _prototypeManager.Index<RadioChannelPrototype>(component.RadioChannel), uid);
args.Handled = true;
}
private void OnTriggerCollide(EntityUid uid, TriggerOnCollideComponent component, ref StartCollideEvent args)
{
if (args.OurFixtureId == component.FixtureID && (!component.IgnoreOtherNonHard || args.OtherFixture.Hard))
Trigger(uid, args.OtherEntity);
}
private void OnSpawnTriggered(EntityUid uid, TriggerOnSpawnComponent component, MapInitEvent args)
{
Trigger(uid);
}
private void OnActivate(EntityUid uid, TriggerOnActivateComponent component, ActivateInWorldEvent args)
{
if (args.Handled || !args.Complex)
return;
Trigger(uid, args.User);
args.Handled = true;
}
private void OnUse(Entity<TriggerOnUseComponent> ent, ref UseInHandEvent args)
{
if (args.Handled)
return;
Trigger(ent.Owner, args.User);
args.Handled = true;
}
private void OnImplantTrigger(EntityUid uid, TriggerImplantActionComponent component, ActivateImplantEvent args)
{
args.Handled = Trigger(uid);
}
private void OnStepTriggered(EntityUid uid, TriggerOnStepTriggerComponent component, ref StepTriggeredOffEvent args)
{
Trigger(uid, args.Tripper);
}
private void OnSlipTriggered(EntityUid uid, TriggerOnSlipComponent component, ref SlipEvent args)
{
Trigger(uid, args.Slipped);
}
private void OnEmptyTriggered(EntityUid uid, TriggerWhenEmptyComponent component, ref OnEmptyGunShotEvent args)
{
Trigger(uid, args.EmptyGun);
}
private void OnRepeatInit(Entity<RepeatingTriggerComponent> ent, ref MapInitEvent args)
{
ent.Comp.NextTrigger = _timing.CurTime + ent.Comp.Delay;
}
public bool Trigger(EntityUid trigger, EntityUid? user = null)
{
var beforeTriggerEvent = new BeforeTriggerEvent(trigger, user);
RaiseLocalEvent(trigger, ref beforeTriggerEvent);
if (beforeTriggerEvent.Cancelled)
return false;
var triggerEvent = new TriggerEvent(trigger, user);
EntityManager.EventBus.RaiseLocalEvent(trigger, triggerEvent, true);
return triggerEvent.Handled;
}
public void TryDelay(EntityUid uid, float amount, ActiveTimerTriggerComponent? comp = null)
{
if (!Resolve(uid, ref comp, false))
return;
comp.TimeRemaining += amount;
}
/// <summary>
/// Start the timer for triggering the device.
/// </summary>
public void StartTimer(Entity<OnUseTimerTriggerComponent?> ent, EntityUid? user)
{
if (!Resolve(ent, ref ent.Comp, false))
return;
var comp = ent.Comp;
HandleTimerTrigger(ent, user, comp.Delay, comp.BeepInterval, comp.InitialBeepDelay, comp.BeepSound);
}
public void HandleTimerTrigger(EntityUid uid, EntityUid? user, float delay, float beepInterval, float? initialBeepDelay, SoundSpecifier? beepSound)
{
if (delay <= 0)
{
RemComp<ActiveTimerTriggerComponent>(uid);
Trigger(uid, user);
return;
}
if (HasComp<ActiveTimerTriggerComponent>(uid))
return;
if (user != null)
{
// Check if entity is bomb/mod. grenade/etc
if (_container.TryGetContainer(uid, "payload", out BaseContainer? container) &&
container.ContainedEntities.Count > 0 &&
TryComp(container.ContainedEntities[0], out ChemicalPayloadComponent? chemicalPayloadComponent))
{
// If a beaker is missing, the entity won't explode, so no reason to log it
if (chemicalPayloadComponent?.BeakerSlotA.Item is not { } beakerA ||
chemicalPayloadComponent?.BeakerSlotB.Item is not { } beakerB ||
!TryComp(beakerA, out SolutionContainerManagerComponent? containerA) ||
!TryComp(beakerB, out SolutionContainerManagerComponent? containerB) ||
!TryComp(beakerA, out FitsInDispenserComponent? fitsA) ||
!TryComp(beakerB, out FitsInDispenserComponent? fitsB) ||
!_solutionContainerSystem.TryGetSolution((beakerA, containerA), fitsA.Solution, out _, out var solutionA) ||
!_solutionContainerSystem.TryGetSolution((beakerB, containerB), fitsB.Solution, out _, out var solutionB))
return;
_adminLogger.Add(LogType.Trigger,
$"{ToPrettyString(user.Value):user} started a {delay} second timer trigger on entity {ToPrettyString(uid):timer}, which contains {SharedSolutionContainerSystem.ToPrettyString(solutionA)} in one beaker and {SharedSolutionContainerSystem.ToPrettyString(solutionB)} in the other.");
}
else
{
_adminLogger.Add(LogType.Trigger,
$"{ToPrettyString(user.Value):user} started a {delay} second timer trigger on entity {ToPrettyString(uid):timer}");
}
}
else
{
_adminLogger.Add(LogType.Trigger,
$"{delay} second timer trigger started on entity {ToPrettyString(uid):timer}");
}
var active = AddComp<ActiveTimerTriggerComponent>(uid);
active.TimeRemaining = delay;
active.User = user;
active.BeepSound = beepSound;
active.BeepInterval = beepInterval;
active.TimeUntilBeep = initialBeepDelay == null ? active.BeepInterval : initialBeepDelay.Value;
var ev = new ActiveTimerTriggerEvent(uid, user);
RaiseLocalEvent(uid, ref ev);
if (TryComp<AppearanceComponent>(uid, out var appearance))
_appearance.SetData(uid, TriggerVisuals.VisualState, TriggerVisualState.Primed, appearance);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
UpdateProximity();
UpdateTimer(frameTime);
UpdateTimedCollide(frameTime);
UpdateRepeat();
}
private void UpdateTimer(float frameTime)
{
HashSet<EntityUid> toRemove = new();
var query = EntityQueryEnumerator<ActiveTimerTriggerComponent>();
while (query.MoveNext(out var uid, out var timer))
{
timer.TimeRemaining -= frameTime;
timer.TimeUntilBeep -= frameTime;
if (timer.TimeRemaining <= 0)
{
Trigger(uid, timer.User);
toRemove.Add(uid);
continue;
}
if (timer.BeepSound == null || timer.TimeUntilBeep > 0)
continue;
timer.TimeUntilBeep += timer.BeepInterval;
_audio.PlayPvs(timer.BeepSound, uid, timer.BeepSound.Params);
}
foreach (var uid in toRemove)
{
RemComp<ActiveTimerTriggerComponent>(uid);
// In case this is a re-usable grenade, un-prime it.
if (TryComp<AppearanceComponent>(uid, out var appearance))
_appearance.SetData(uid, TriggerVisuals.VisualState, TriggerVisualState.Unprimed, appearance);
}
}
private void UpdateRepeat()
{
var now = _timing.CurTime;
var query = EntityQueryEnumerator<RepeatingTriggerComponent>();
while (query.MoveNext(out var uid, out var comp))
{
if (comp.NextTrigger > now)
continue;
comp.NextTrigger = now + comp.Delay;
Trigger(uid);
}
}
}
}

View File

@@ -1,64 +0,0 @@
using Robust.Shared.Timing;
using Robust.Shared.Serialization.Manager;
using Content.Server.Explosion.Components.OnTrigger;
namespace Content.Server.Explosion.EntitySystems;
public sealed class TwoStageTriggerSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly ISerializationManager _serializationManager = default!;
[Dependency] private readonly TriggerSystem _triggerSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<TwoStageTriggerComponent, TriggerEvent>(OnTrigger);
}
private void OnTrigger(EntityUid uid, TwoStageTriggerComponent component, TriggerEvent args)
{
if (component.Triggered)
return;
component.Triggered = true;
component.NextTriggerTime = _timing.CurTime + component.TriggerDelay;
}
private void LoadComponents(EntityUid uid, TwoStageTriggerComponent component)
{
foreach (var (name, entry) in component.SecondStageComponents)
{
var comp = (Component) Factory.GetComponent(name);
var temp = (object)comp;
if (EntityManager.TryGetComponent(uid, entry.Component.GetType(), out var c))
RemComp(uid, c);
_serializationManager.CopyTo(entry.Component, ref temp);
AddComp(uid, comp);
}
component.ComponentsIsLoaded = true;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var enumerator = EntityQueryEnumerator<TwoStageTriggerComponent>();
while (enumerator.MoveNext(out var uid, out var component))
{
if (!component.Triggered)
continue;
if (!component.ComponentsIsLoaded)
LoadComponents(uid, component);
if (_timing.CurTime < component.NextTriggerTime)
continue;
component.NextTriggerTime = null;
_triggerSystem.Trigger(uid);
}
}
}

View File

@@ -12,6 +12,7 @@ using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.DoAfter;
using Content.Shared.Forensics;
using Content.Shared.Forensics.Components;
using Content.Shared.Forensics.Systems;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Inventory;
@@ -23,7 +24,7 @@ using Content.Shared.Hands.Components;
namespace Content.Server.Forensics
{
public sealed class ForensicsSystem : EntitySystem
public sealed class ForensicsSystem : SharedForensicsSystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly InventorySystem _inventory = default!;
@@ -317,12 +318,7 @@ namespace Content.Server.Forensics
}
#region Public API
/// <summary>
/// Give the entity a new, random DNA string and call an event to notify other systems like the bloodstream that it has been changed.
/// Does nothing if it does not have the DnaComponent.
/// </summary>
public void RandomizeDNA(Entity<DnaComponent?> ent)
public override void RandomizeDNA(Entity<DnaComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return;
@@ -334,11 +330,7 @@ namespace Content.Server.Forensics
RaiseLocalEvent(ent.Owner, ref ev);
}
/// <summary>
/// Give the entity a new, random fingerprint string.
/// Does nothing if it does not have the FingerprintComponent.
/// </summary>
public void RandomizeFingerprint(Entity<FingerprintComponent?> ent)
public override void RandomizeFingerprint(Entity<FingerprintComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return;

View File

@@ -3,7 +3,6 @@ using System.Numerics;
using Content.Server.Announcements;
using Content.Server.Discord;
using Content.Server.GameTicking.Events;
using Content.Server.Ghost;
using Content.Server.Maps;
using Content.Server.Roles;
using Content.Shared.CCVar;
@@ -12,6 +11,7 @@ using Content.Shared.GameTicking;
using Content.Shared.Mind;
using Content.Shared.Players;
using Content.Shared.Preferences;
using Content.Shared.Roles.Components;
using JetBrains.Annotations;
using Prometheus;
using Robust.Shared.Asynchronous;
@@ -19,7 +19,6 @@ using Robust.Shared.Audio;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Random;

View File

@@ -0,0 +1,7 @@
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
/// Gamerule component for handling a changeling antagonist.
/// </summary>
[RegisterComponent]
public sealed partial class ChangelingRuleComponent : Component;

View File

@@ -14,7 +14,7 @@ public sealed partial class ParadoxCloneRuleComponent : Component
/// Cloning settings to be used.
/// </summary>
[DataField]
public ProtoId<CloningSettingsPrototype> Settings = "Antag";
public ProtoId<CloningSettingsPrototype> Settings = "ParadoxCloningSettings";
/// <summary>
/// Visual effect spawned when gibbing at round end.

View File

@@ -1,12 +1,10 @@
using Content.Server.Antag;
using Content.Server.Dragon;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind;
using Content.Server.Roles;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Shared.CharacterInfo;
using Content.Shared.Localizations;
using Content.Shared.Roles.Components;
using Robust.Server.GameObjects;
namespace Content.Server.GameTicking.Rules;
@@ -56,10 +54,9 @@ public sealed class DragonRuleSystem : GameRuleSystem<DragonRuleComponent>
var dragonXform = Transform(dragon);
var station = _station.GetStationInMap(dragonXform.MapID);
EntityUid? stationGrid = null;
if (TryComp<StationDataComponent>(station, out var stationData))
stationGrid = _station.GetLargestGrid(stationData);
if (_station.GetStationInMap(dragonXform.MapID) is { } station)
stationGrid = _station.GetLargestGrid(station);
if (stationGrid is not null)
{

View File

@@ -1,14 +1,12 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Station.Components;
using Content.Shared.GameTicking.Components;
using Content.Shared.Random.Helpers;
using Robust.Server.GameObjects;
using Content.Shared.Station.Components;
using Robust.Shared.Collections;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Random;
namespace Content.Server.GameTicking.Rules;

View File

@@ -17,6 +17,7 @@ using Content.Shared.NPC.Components;
using Content.Shared.NPC.Systems;
using Content.Shared.Nuke;
using Content.Shared.NukeOps;
using Content.Shared.Roles.Components;
using Content.Shared.Store;
using Content.Shared.Tag;
using Content.Shared.Zombies;
@@ -24,6 +25,7 @@ using Robust.Shared.Map;
using Robust.Shared.Random;
using Robust.Shared.Utility;
using System.Linq;
using Content.Shared.Station.Components;
using Content.Shared.Store.Components;
using Robust.Shared.Prototypes;

View File

@@ -23,6 +23,7 @@ using Content.Shared.Mobs.Systems;
using Content.Shared.NPC.Prototypes;
using Content.Shared.NPC.Systems;
using Content.Shared.Revolutionary.Components;
using Content.Shared.Roles.Components;
using Content.Shared.Stunnable;
using Content.Shared.Zombies;
using Robust.Shared.Prototypes;
@@ -161,7 +162,10 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
if (_mind.TryGetMind(ev.User.Value, out var revMindId, out _))
{
if (_role.MindHasRole<RevolutionaryRoleComponent>(revMindId, out var role))
{
role.Value.Comp2.ConvertedCount++;
Dirty(role.Value.Owner, role.Value.Comp2);
}
}
}

View File

@@ -1,11 +1,10 @@
using System.Linq;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Shuttles.Systems;
using Content.Server.Station.Components;
using Content.Server.Station.Events;
using Content.Shared.GameTicking.Components;
using Content.Shared.Station.Components;
using Content.Shared.Storage;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.GameTicking.Rules;

View File

@@ -6,6 +6,7 @@ using Content.Server.Shuttles.Systems;
using Content.Shared.GameTicking.Components;
using Content.Shared.Mind;
using Content.Shared.Mobs.Systems;
using Content.Shared.Roles.Components;
using Content.Shared.Survivor.Components;
using Content.Shared.Tag;
using Robust.Server.GameObjects;

View File

@@ -2,6 +2,7 @@ using Content.Server.Antag;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Roles;
using Content.Shared.Humanoid;
using Content.Shared.Roles.Components;
namespace Content.Server.GameTicking.Rules;

View File

@@ -1,19 +1,16 @@
using Content.Server.Administration.Logs;
using Content.Server.Antag;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind;
using Content.Server.Objectives;
using Content.Server.PDA.Ringer;
using Content.Server.Roles;
using Content.Server.Traitor.Uplink;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.GameTicking.Components;
using Content.Shared.Mind;
using Content.Shared.NPC.Systems;
using Content.Shared.PDA;
using Content.Shared.Random.Helpers;
using Content.Shared.Roles;
using Content.Shared.Roles.Components;
using Content.Shared.Roles.Jobs;
using Content.Shared.Roles.RoleCodeword;
using Robust.Shared.Prototypes;

View File

@@ -58,7 +58,7 @@ public abstract class BaseEntityReplaceVariationPassSystem<TEntComp, TGameRuleCo
Transform(newEnt).LocalRotation = rot;
}
Log.Debug($"Entity replacement took {stopwatch.Elapsed} with {Stations.GetTileCount(args.Station)} tiles");
Log.Debug($"Entity replacement took {stopwatch.Elapsed} with {Stations.GetTileCount(args.Station.AsNullable())} tiles");
}
private void QueueReplace(Entity<TransformComponent> ent, List<EntitySpawnEntry> replacements)

View File

@@ -9,7 +9,7 @@ public sealed class EntitySpawnVariationPassSystem : VariationPassSystem<EntityS
{
protected override void ApplyVariation(Entity<EntitySpawnVariationPassComponent> ent, ref StationVariationPassEvent args)
{
var totalTiles = Stations.GetTileCount(args.Station);
var totalTiles = Stations.GetTileCount(args.Station.AsNullable());
var dirtyMod = Random.NextGaussian(ent.Comp.TilesPerEntityAverage, ent.Comp.TilesPerEntityStdDev);
var trashTiles = Math.Max((int) (totalTiles * (1 / dirtyMod)), 0);

View File

@@ -15,7 +15,7 @@ public sealed class PuddleMessVariationPassSystem : VariationPassSystem<PuddleMe
protected override void ApplyVariation(Entity<PuddleMessVariationPassComponent> ent, ref StationVariationPassEvent args)
{
var totalTiles = Stations.GetTileCount(args.Station);
var totalTiles = Stations.GetTileCount(args.Station.AsNullable());
if (!_proto.TryIndex(ent.Comp.RandomPuddleSolutionFill, out var proto))
return;

View File

@@ -4,7 +4,6 @@ using Content.Server.GameTicking.Rules.Components;
using Content.Server.Popups;
using Content.Server.Roles;
using Content.Server.RoundEnd;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Server.Zombies;
using Content.Shared.GameTicking.Components;
@@ -14,6 +13,7 @@ using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Roles;
using Content.Shared.Roles.Components;
using Content.Shared.Zombies;
using Robust.Shared.Player;
using Robust.Shared.Timing;
@@ -190,7 +190,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
{
foreach (var station in _station.GetStationsSet())
{
if (TryComp<StationDataComponent>(station, out var data) && _station.GetLargestGrid(data) is { } grid)
if (_station.GetLargestGrid(station) is { } grid)
stationGrids.Add(grid);
}
}

View File

@@ -6,7 +6,6 @@ using Content.Server.GameTicking;
using Content.Server.Ghost.Components;
using Content.Server.Mind;
using Content.Server.Roles.Jobs;
using Content.Server.Warps;
using Content.Shared.Actions;
using Content.Shared.CCVar;
using Content.Shared.Damage;
@@ -30,7 +29,6 @@ using Content.Shared.Storage.Components;
using Content.Shared.Tag;
using Content.Shared.Warps;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Map;
using Robust.Shared.Physics.Components;

View File

@@ -1,12 +0,0 @@
using Content.Shared.Roles;
namespace Content.Server.Ghost;
/// <summary>
/// This is used to mark Observers properly, as they get Minds
/// </summary>
[RegisterComponent]
public sealed partial class ObserverRoleComponent : BaseMindRoleComponent
{
public string Name => Loc.GetString("observer-role-name");
}

View File

@@ -1,15 +0,0 @@
using Content.Shared.Roles;
namespace Content.Server.Ghost.Roles;
/// <summary>
/// Added to mind role entities to tag that they are a ghostrole.
/// It also holds the name for the round end display
/// </summary>
[RegisterComponent]
public sealed partial class GhostRoleMarkerRoleComponent : BaseMindRoleComponent
{
//TODO does anything still use this? It gets populated by GhostRolesystem but I don't see anything ever reading it
[DataField] public string? Name;
}

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