Merge pull request #1613 from crystallpunk-14/ed-04-08-2025-upstream-sync

Crawling upstream sync
This commit is contained in:
Red
2025-08-06 12:52:51 +03:00
committed by GitHub
510 changed files with 12045 additions and 3125 deletions

View File

@@ -1,70 +0,0 @@
using Content.Shared.Alert;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Abilities.Mime
{
/// <summary>
/// Lets its owner entity use mime powers, like placing invisible walls.
/// </summary>
[RegisterComponent]
public sealed partial class MimePowersComponent : Component
{
/// <summary>
/// Whether this component is active or not.
/// </summarY>
[DataField("enabled")]
public bool Enabled = true;
/// <summary>
/// The wall prototype to use.
/// </summary>
[DataField("wallPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string WallPrototype = "WallInvisible";
[DataField("invisibleWallAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? InvisibleWallAction = "ActionMimeInvisibleWall";
[DataField("invisibleWallActionEntity")] public EntityUid? InvisibleWallActionEntity;
// The vow zone lies below
public bool VowBroken = false;
/// <summary>
/// Whether this mime is ready to take the vow again.
/// Note that if they already have the vow, this is also false.
/// </summary>
public bool ReadyToRepent = false;
/// <summary>
/// Time when the mime can repent their vow
/// </summary>
[DataField("vowRepentTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan VowRepentTime = TimeSpan.Zero;
/// <summary>
/// How long it takes the mime to get their powers back
/// </summary>
[DataField("vowCooldown")]
public TimeSpan VowCooldown = TimeSpan.FromMinutes(5);
[DataField]
public ProtoId<AlertPrototype> VowAlert = "VowOfSilence";
[DataField]
public ProtoId<AlertPrototype> VowBrokenAlert = "VowBroken";
/// <summary>
/// Does this component prevent the mime from writing on paper while their vow is active?
/// </summary>
[DataField]
public bool PreventWriting = false;
/// <summary>
/// What message is displayed when the mime fails to write?
/// </summary>
[DataField]
public LocId FailWriteMessage = "paper-component-illiterate-mime";
}
}

View File

@@ -1,171 +0,0 @@
using Content.Server.Popups;
using Content.Shared.Abilities.Mime;
using Content.Shared.Actions;
using Content.Shared.Actions.Events;
using Content.Shared.Alert;
using Content.Shared.Coordinates.Helpers;
using Content.Shared.Maps;
using Content.Shared.Paper;
using Content.Shared.Physics;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Timing;
using Content.Shared.Speech.Muting;
namespace Content.Server.Abilities.Mime
{
public sealed class MimePowersSystem : EntitySystem
{
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
[Dependency] private readonly TurfSystem _turf = default!;
[Dependency] private readonly IMapManager _mapMan = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MimePowersComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<MimePowersComponent, InvisibleWallActionEvent>(OnInvisibleWall);
SubscribeLocalEvent<MimePowersComponent, BreakVowAlertEvent>(OnBreakVowAlert);
SubscribeLocalEvent<MimePowersComponent, RetakeVowAlertEvent>(OnRetakeVowAlert);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
// Queue to track whether mimes can retake vows yet
var query = EntityQueryEnumerator<MimePowersComponent>();
while (query.MoveNext(out var uid, out var mime))
{
if (!mime.VowBroken || mime.ReadyToRepent)
continue;
if (_timing.CurTime < mime.VowRepentTime)
continue;
mime.ReadyToRepent = true;
_popupSystem.PopupEntity(Loc.GetString("mime-ready-to-repent"), uid, uid);
}
}
private void OnComponentInit(EntityUid uid, MimePowersComponent component, ComponentInit args)
{
EnsureComp<MutedComponent>(uid);
if (component.PreventWriting)
{
EnsureComp<BlockWritingComponent>(uid, out var illiterateComponent);
illiterateComponent.FailWriteMessage = component.FailWriteMessage;
Dirty(uid, illiterateComponent);
}
_alertsSystem.ShowAlert(uid, component.VowAlert);
_actionsSystem.AddAction(uid, ref component.InvisibleWallActionEntity, component.InvisibleWallAction, uid);
}
/// <summary>
/// Creates an invisible wall in a free space after some checks.
/// </summary>
private void OnInvisibleWall(EntityUid uid, MimePowersComponent component, InvisibleWallActionEvent args)
{
if (!component.Enabled)
return;
if (_container.IsEntityOrParentInContainer(uid))
return;
var xform = Transform(uid);
// Get the tile in front of the mime
var offsetValue = xform.LocalRotation.ToWorldVec();
var coords = xform.Coordinates.Offset(offsetValue).SnapToGrid(EntityManager, _mapMan);
var tile = _turf.GetTileRef(coords);
if (tile == null)
return;
// Check if the tile is blocked by a wall or mob, and don't create the wall if so
if (_turf.IsTileBlocked(tile.Value, CollisionGroup.Impassable | CollisionGroup.Opaque))
{
_popupSystem.PopupEntity(Loc.GetString("mime-invisible-wall-failed"), uid, uid);
return;
}
_popupSystem.PopupEntity(Loc.GetString("mime-invisible-wall-popup", ("mime", uid)), uid);
// Make sure we set the invisible wall to despawn properly
Spawn(component.WallPrototype, _turf.GetTileCenter(tile.Value));
// Handle args so cooldown works
args.Handled = true;
}
private void OnBreakVowAlert(Entity<MimePowersComponent> ent, ref BreakVowAlertEvent args)
{
if (args.Handled)
return;
BreakVow(ent, ent);
args.Handled = true;
}
private void OnRetakeVowAlert(Entity<MimePowersComponent> ent, ref RetakeVowAlertEvent args)
{
if (args.Handled)
return;
RetakeVow(ent, ent);
args.Handled = true;
}
/// <summary>
/// Break this mime's vow to not speak.
/// </summary>
public void BreakVow(EntityUid uid, MimePowersComponent? mimePowers = null)
{
if (!Resolve(uid, ref mimePowers))
return;
if (mimePowers.VowBroken)
return;
mimePowers.Enabled = false;
mimePowers.VowBroken = true;
mimePowers.VowRepentTime = _timing.CurTime + mimePowers.VowCooldown;
RemComp<MutedComponent>(uid);
if (mimePowers.PreventWriting)
RemComp<BlockWritingComponent>(uid);
_alertsSystem.ClearAlert(uid, mimePowers.VowAlert);
_alertsSystem.ShowAlert(uid, mimePowers.VowBrokenAlert);
_actionsSystem.RemoveAction(uid, mimePowers.InvisibleWallActionEntity);
}
/// <summary>
/// Retake this mime's vow to not speak.
/// </summary>
public void RetakeVow(EntityUid uid, MimePowersComponent? mimePowers = null)
{
if (!Resolve(uid, ref mimePowers))
return;
if (!mimePowers.ReadyToRepent)
{
_popupSystem.PopupEntity(Loc.GetString("mime-not-ready-repent"), uid, uid);
return;
}
mimePowers.Enabled = true;
mimePowers.ReadyToRepent = false;
mimePowers.VowBroken = false;
AddComp<MutedComponent>(uid);
if (mimePowers.PreventWriting)
{
EnsureComp<BlockWritingComponent>(uid, out var illiterateComponent);
illiterateComponent.FailWriteMessage = mimePowers.FailWriteMessage;
Dirty(uid, illiterateComponent);
}
_alertsSystem.ClearAlert(uid, mimePowers.VowBrokenAlert);
_alertsSystem.ShowAlert(uid, mimePowers.VowAlert);
_actionsSystem.AddAction(uid, ref mimePowers.InvisibleWallActionEntity, mimePowers.InvisibleWallAction, uid);
}
}
}

View File

@@ -0,0 +1,58 @@
using Content.Server.Administration.UI;
using Content.Server.EUI;
using Content.Shared.Administration;
using Robust.Server.Player;
using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Admin)]
public sealed class CameraCommand : LocalizedCommands
{
[Dependency] private readonly EuiManager _eui = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
public override string Command => "camera";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (shell.Player is not { } user)
{
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
return;
}
if (args.Length != 1)
{
shell.WriteError(Loc.GetString("shell-wrong-arguments-number"));
return;
}
if (!NetEntity.TryParse(args[0], out var targetNetId) || !_entManager.TryGetEntity(targetNetId, out var targetUid))
{
if (!_playerManager.TryGetSessionByUsername(args[0], out var player)
|| player.AttachedEntity == null)
{
shell.WriteError(Loc.GetString("cmd-camera-wrong-argument"));
return;
}
targetUid = player.AttachedEntity.Value;
}
var ui = new AdminCameraEui(targetUid.Value);
_eui.OpenEui(ui, user);
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHintOptions(
CompletionHelper.SessionNames(players: _playerManager),
Loc.GetString("cmd-camera-hint"));
}
return CompletionResult.Empty;
}
}

View File

@@ -39,6 +39,7 @@ using Content.Shared.Movement.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Popups;
using Content.Shared.Slippery;
using Content.Shared.Stunnable;
using Content.Shared.Tabletop.Components;
using Content.Shared.Tools.Systems;
using Content.Shared.Verbs;
@@ -48,6 +49,7 @@ using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Timer = Robust.Shared.Timing.Timer;
@@ -877,7 +879,7 @@ public sealed partial class AdminVerbSystem
if (!hadSlipComponent)
{
slipComponent.SlipData.SuperSlippery = true;
slipComponent.SlipData.ParalyzeTime = TimeSpan.FromSeconds(5);
slipComponent.SlipData.StunTime = TimeSpan.FromSeconds(5);
slipComponent.SlipData.LaunchForwardsMultiplier = 20;
}
@@ -922,5 +924,20 @@ public sealed partial class AdminVerbSystem
Message = string.Join(": ", omniaccentName, Loc.GetString("admin-smite-omni-accent-description"))
};
args.Verbs.Add(omniaccent);
var crawlerName = Loc.GetString("admin-smite-crawler-name").ToLowerInvariant();
Verb crawler = new()
{
Text = crawlerName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new("Mobs/Animals/snake.rsi"), "icon"),
Act = () =>
{
EnsureComp<WormComponent>(args.Target);
},
Impact = LogImpact.Extreme,
Message = string.Join(": ", crawlerName, Loc.GetString("admin-smite-crawler-description"))
};
args.Verbs.Add(crawler);
}
}

View File

@@ -4,7 +4,6 @@ using Content.Server.Administration.UI;
using Content.Server.Disposal.Tube;
using Content.Server.EUI;
using Content.Server.Ghost.Roles;
using Content.Server.Mind.Commands;
using Content.Server.Mind;
using Content.Server.Prayer;
using Content.Server.Silicons.Laws;
@@ -16,7 +15,6 @@ using Content.Shared.Configurable;
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.GameTicking;
using Content.Shared.Hands.Components;
using Content.Shared.Inventory;
using Content.Shared.Mind.Components;
using Content.Shared.Movement.Components;
@@ -390,6 +388,22 @@ namespace Content.Server.Administration.Systems
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Actions/actions_borg.rsi"), "state-laws"),
});
}
// open camera
args.Verbs.Add(new Verb()
{
Priority = 10,
Text = Loc.GetString("admin-verbs-camera"),
Message = Loc.GetString("admin-verbs-camera-description"),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/vv.svg.192dpi.png")),
Category = VerbCategory.Admin,
Act = () =>
{
var ui = new AdminCameraEui(args.Target);
_euiManager.OpenEui(ui, player);
},
Impact = LogImpact.Low
});
}
}
@@ -458,7 +472,7 @@ namespace Content.Server.Administration.Systems
Text = Loc.GetString("make-sentient-verb-get-data-text"),
Category = VerbCategory.Debug,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/sentient.svg.192dpi.png")),
Act = () => MakeSentientCommand.MakeSentient(args.Target, EntityManager),
Act = () => _mindSystem.MakeSentient(args.Target),
Impact = LogImpact.Medium
};
args.Verbs.Add(verb);

View File

@@ -0,0 +1,97 @@
using Content.Server.Administration.Managers;
using Content.Server.EUI;
using Content.Shared.Administration;
using Content.Shared.Eui;
using Content.Shared.Follower;
using Content.Shared.Coordinates;
using Robust.Server.GameStates;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using JetBrains.Annotations;
namespace Content.Server.Administration.UI;
/// <summary>
/// Admin Eui for opening a viewport window to observe entities.
/// Use the "Open Camera" admin verb or the "camera" command to open.
/// </summary>
[UsedImplicitly]
public sealed partial class AdminCameraEui : BaseEui
{
[Dependency] private readonly IAdminManager _admin = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly FollowerSystem _follower = default!;
private readonly PvsOverrideSystem _pvs = default!;
private readonly SharedViewSubscriberSystem _viewSubscriber = default!;
private static readonly EntProtoId CameraProtoId = "AdminCamera";
private readonly EntityUid _target;
private EntityUid? _camera;
public AdminCameraEui(EntityUid target)
{
IoCManager.InjectDependencies(this);
_follower = _entityManager.System<FollowerSystem>();
_pvs = _entityManager.System<PvsOverrideSystem>();
_viewSubscriber = _entityManager.System<SharedViewSubscriberSystem>();
_target = target;
}
public override void Opened()
{
base.Opened();
_camera = CreateCamera(_target, Player);
StateDirty();
}
public override void Closed()
{
base.Closed();
_entityManager.DeleteEntity(_camera);
}
public override void HandleMessage(EuiMessageBase msg)
{
base.HandleMessage(msg);
switch (msg)
{
case AdminCameraFollowMessage:
if (!_admin.HasAdminFlag(Player, AdminFlags.Admin) || Player.AttachedEntity == null)
return;
_follower.StartFollowingEntity(Player.AttachedEntity.Value, _target);
break;
default:
break;
}
}
public override EuiStateBase GetNewState()
{
var name = _entityManager.GetComponent<MetaDataComponent>(_target).EntityName;
var netEnt = _entityManager.GetNetEntity(_camera);
return new AdminCameraEuiState(netEnt, name, _timing.CurTick);
}
private EntityUid CreateCamera(EntityUid target, ICommonSession observer)
{
// Spawn a camera entity attached to the target.
var coords = target.ToCoordinates();
var camera = _entityManager.SpawnAttachedTo(CameraProtoId, coords);
// Allow the user to see the entities near the camera.
// This also force sends the camera entity to the user, overriding the visibility flags.
// (The camera entity has its visibility flags set to VisibilityFlags.Admin so that cheat clients can't see it)
_viewSubscriber.AddViewSubscriber(camera, observer);
return camera;
}
}

View File

@@ -96,7 +96,7 @@ public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
EntityManager.AddComponents(ent, injectedAnom.Components);
_stun.TryParalyze(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration), true);
_stun.TryUpdateParalyzeDuration(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration));
_jitter.DoJitter(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration), true);
if (ent.Comp.StartSound is not null)
@@ -125,7 +125,7 @@ public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
private void OnAnomalyPulse(Entity<InnerBodyAnomalyComponent> ent, ref AnomalyPulseEvent args)
{
_stun.TryParalyze(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration / 2 * args.Severity), true);
_stun.TryUpdateParalyzeDuration(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration / 2 * args.Severity));
_jitter.DoJitter(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration / 2 * args.Severity), true);
}
@@ -213,7 +213,7 @@ public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
if (_proto.TryIndex(ent.Comp.InjectionProto, out var injectedAnom))
EntityManager.RemoveComponents(ent, injectedAnom.Components);
_stun.TryParalyze(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration), true);
_stun.TryUpdateParalyzeDuration(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration));
if (ent.Comp.EndMessage is not null &&
_mind.TryGetMind(ent, out _, out var mindComponent) &&

View File

@@ -17,6 +17,7 @@ using Robust.Shared.Map.Components;
using Robust.Shared.Timing;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.NodeContainer;
@@ -53,6 +54,7 @@ public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleS
// Grid events
SubscribeLocalEvent<GridSplitEvent>(OnGridSplit);
SubscribeLocalEvent<PipeNodeGroupRemovedEvent>(OnPipeNodeGroupRemoved);
}
#region Event handling
@@ -295,6 +297,25 @@ public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleS
#region Pipe net functions
private void OnPipeNodeGroupRemoved(ref PipeNodeGroupRemovedEvent args)
{
// When a pipe node group is removed, we need to iterate over all of
// our pipe chunks and remove any entries with a matching net id.
// (We only need to check the chunks for the affected grid, though.)
if (!_gridAtmosPipeChunks.TryGetValue(args.Grid, out var chunkData))
return;
foreach (var chunk in chunkData.Values)
{
foreach (var key in chunk.AtmosPipeData.Keys)
{
if (key.NetId == args.NetId)
chunk.AtmosPipeData.Remove(key);
}
}
}
private void RebuildAtmosPipeGrid(EntityUid gridUid, MapGridComponent grid)
{
var allChunks = new Dictionary<Vector2i, AtmosPipeChunk>();
@@ -411,7 +432,7 @@ public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleS
continue;
var netId = GetPipeNodeNetId(pipeNode);
var subnet = new AtmosMonitoringConsoleSubnet(netId, pipeNode.CurrentPipeLayer, pipeColor.Color.ToHex());
var subnet = new AtmosMonitoringConsoleSubnet(netId, pipeNode.CurrentPipeLayer, pipeColor.Color);
var pipeDirection = pipeNode.CurrentPipeDirection;
chunk.AtmosPipeData.TryGetValue(subnet, out var atmosPipeData);

View File

@@ -279,6 +279,14 @@ public partial class AtmosphereSystem
public bool RemovePipeNet(Entity<GridAtmosphereComponent?> grid, PipeNet pipeNet)
{
// Technically this event can be fired even on grids that don't
// actually have grid atmospheres.
if (pipeNet.Grid is not null)
{
var ev = new PipeNodeGroupRemovedEvent(grid, pipeNet.NetId);
RaiseLocalEvent(ref ev);
}
return _atmosQuery.Resolve(grid, ref grid.Comp, false) && grid.Comp.PipeNets.Remove(pipeNet);
}
@@ -329,3 +337,12 @@ public partial class AtmosphereSystem
[ByRefEvent] private record struct IsHotspotActiveMethodEvent
(EntityUid Grid, Vector2i Tile, bool Result = false, bool Handled = false);
}
/// <summary>
/// Raised broadcasted when a pipe node group within a grid has been removed.
/// </summary>
/// <param name="Grid">The grid with the removed node group.</param>
/// <param name="NetId">The net id of the removed node group.</param>
[ByRefEvent]
public record struct PipeNodeGroupRemovedEvent(EntityUid Grid, int NetId);

View File

@@ -409,7 +409,7 @@ namespace Content.Server.Atmos.EntitySystems
flammable.Resisting = true;
_popup.PopupEntity(Loc.GetString("flammable-component-resist-message"), uid, uid);
_stunSystem.TryParalyze(uid, TimeSpan.FromSeconds(2f), true);
_stunSystem.TryUpdateParalyzeDuration(uid, TimeSpan.FromSeconds(2f));
// TODO FLAMMABLE: Make this not use TimerComponent...
uid.SpawnTimer(2000, () =>

View File

@@ -48,7 +48,7 @@ public sealed partial class AtmosAlarmableComponent : Component
public HashSet<string> SyncWithTags { get; private set; } = new();
[DataField("monitorAlertTypes")]
public HashSet<AtmosMonitorThresholdType>? MonitorAlertTypes { get; private set; }
public AtmosMonitorThresholdTypeFlags MonitorAlertTypes { get; private set; }
/// <summary>
/// If this device should receive only. If it can only

View File

@@ -59,7 +59,7 @@ public sealed partial class AtmosMonitorComponent : Component
public AtmosAlarmType LastAlarmState = AtmosAlarmType.Normal;
[DataField("trippedThresholds")]
public HashSet<AtmosMonitorThresholdType> TrippedThresholds = new();
public AtmosMonitorThresholdTypeFlags TrippedThresholds;
/// <summary>
/// Registered devices in this atmos monitor. Alerts will be sent directly

View File

@@ -108,9 +108,9 @@ public sealed class AtmosAlarmableSystem : EntitySystem
break;
}
if (args.Data.TryGetValue(AlertTypes, out HashSet<AtmosMonitorThresholdType>? types) && component.MonitorAlertTypes != null)
if (args.Data.TryGetValue(AlertTypes, out AtmosMonitorThresholdTypeFlags types) && component.MonitorAlertTypes != AtmosMonitorThresholdTypeFlags.None)
{
isValid = types.Any(type => component.MonitorAlertTypes.Contains(type));
isValid = (types & component.MonitorAlertTypes) != 0;
}
if (!component.NetworkAlarmStates.ContainsKey(args.SenderAddress))

View File

@@ -207,7 +207,7 @@ public sealed class AtmosMonitorSystem : EntitySystem
if (component.MonitorFire
&& component.LastAlarmState != AtmosAlarmType.Danger)
{
component.TrippedThresholds.Add(AtmosMonitorThresholdType.Temperature);
component.TrippedThresholds |= AtmosMonitorThresholdTypeFlags.Temperature;
Alert(uid, AtmosAlarmType.Danger, null, component); // technically???
}
@@ -218,7 +218,7 @@ public sealed class AtmosMonitorSystem : EntitySystem
&& component.TemperatureThreshold.CheckThreshold(args.Temperature, out var temperatureState)
&& temperatureState > component.LastAlarmState)
{
component.TrippedThresholds.Add(AtmosMonitorThresholdType.Temperature);
component.TrippedThresholds |= AtmosMonitorThresholdTypeFlags.Temperature;
Alert(uid, AtmosAlarmType.Danger, null, component);
}
}
@@ -259,7 +259,7 @@ public sealed class AtmosMonitorSystem : EntitySystem
if (!Resolve(uid, ref monitor)) return;
var state = AtmosAlarmType.Normal;
HashSet<AtmosMonitorThresholdType> alarmTypes = new(monitor.TrippedThresholds);
var alarmTypes = monitor.TrippedThresholds;
if (monitor.TemperatureThreshold != null
&& monitor.TemperatureThreshold.CheckThreshold(air.Temperature, out var temperatureState))
@@ -267,11 +267,11 @@ public sealed class AtmosMonitorSystem : EntitySystem
if (temperatureState > state)
{
state = temperatureState;
alarmTypes.Add(AtmosMonitorThresholdType.Temperature);
alarmTypes |= AtmosMonitorThresholdTypeFlags.Temperature;
}
else if (temperatureState == AtmosAlarmType.Normal)
{
alarmTypes.Remove(AtmosMonitorThresholdType.Temperature);
alarmTypes &= ~AtmosMonitorThresholdTypeFlags.Temperature;
}
}
@@ -282,11 +282,11 @@ public sealed class AtmosMonitorSystem : EntitySystem
if (pressureState > state)
{
state = pressureState;
alarmTypes.Add(AtmosMonitorThresholdType.Pressure);
alarmTypes |= AtmosMonitorThresholdTypeFlags.Pressure;
}
else if (pressureState == AtmosAlarmType.Normal)
{
alarmTypes.Remove(AtmosMonitorThresholdType.Pressure);
alarmTypes &= ~AtmosMonitorThresholdTypeFlags.Pressure;
}
}
@@ -306,17 +306,17 @@ public sealed class AtmosMonitorSystem : EntitySystem
if (tripped)
{
alarmTypes.Add(AtmosMonitorThresholdType.Gas);
alarmTypes |= AtmosMonitorThresholdTypeFlags.Gas;
}
else
{
alarmTypes.Remove(AtmosMonitorThresholdType.Gas);
alarmTypes &= ~AtmosMonitorThresholdTypeFlags.Gas;
}
}
// if the state of the current air doesn't match the last alarm state,
// we update the state
if (state != monitor.LastAlarmState || !alarmTypes.SetEquals(monitor.TrippedThresholds))
if (state != monitor.LastAlarmState || alarmTypes != monitor.TrippedThresholds)
{
Alert(uid, state, alarmTypes, monitor);
}
@@ -327,7 +327,7 @@ public sealed class AtmosMonitorSystem : EntitySystem
/// </summary>
/// <param name="state">The alarm state to set this monitor to.</param>
/// <param name="alarms">The alarms that caused this alarm state.</param>
public void Alert(EntityUid uid, AtmosAlarmType state, HashSet<AtmosMonitorThresholdType>? alarms = null, AtmosMonitorComponent? monitor = null)
public void Alert(EntityUid uid, AtmosAlarmType state, AtmosMonitorThresholdTypeFlags? alarms = null, AtmosMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor))
return;

View File

@@ -1,24 +0,0 @@
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

@@ -1,48 +0,0 @@
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

@@ -12,110 +12,109 @@ namespace Content.Server.Chat.Managers;
/// </summary>
public sealed class ChatSanitizationManager : IChatSanitizationManager
{
private static readonly Dictionary<string, string> ShorthandToEmote = new()
{
private static readonly (Regex regex, string emoteKey)[] ShorthandToEmote =
[
// CP14-RU-Localization-Start
{ "лол", "chatsan-laughs" },
{ "хд", "chatsan-laughs" },
{ "о-о", "chatsan-wide-eyed" }, // cyrillic о
{ "о.о", "chatsan-wide-eyed" }, // cyrillic о
{ "0_о", "chatsan-wide-eyed" }, // cyrillic о
{ "о/", "chatsan-waves" }, // cyrillic о
{ "о7", "chatsan-salutes" }, // cyrillic о
{ "0_o", "chatsan-wide-eyed" },
{ "лмао", "chatsan-laughs" },
{ "рофл", "chatsan-laughs" },
{ "яхз", "chatsan-shrugs" },
{ ":0", "chatsan-surprised" },
{ ":р", "chatsan-stick-out-tongue" }, // cyrillic р
{ "кек", "chatsan-laughs" },
{ "T_T", "chatsan-cries" },
{ "Т_Т", "chatsan-cries" }, // cyrillic T
{ "=_(", "chatsan-cries" },
{ "))", "chatsan-smiles-widely" },
{ ")", "chatsan-smiles" },
{ "((", "chatsan-frowns-deeply" },
{ "(", "chatsan-frowns" },
Entry( "лол", "chatsan-laughs" ),
Entry( "хд", "chatsan-laughs" ),
Entry( "о-о", "chatsan-wide-eyed" ), // cyrillic о
Entry( "о.о", "chatsan-wide-eyed" ), // cyrillic о
Entry( "0_о", "chatsan-wide-eyed" ), // cyrillic о
Entry( "о/", "chatsan-waves" ), // cyrillic о
Entry( "о7", "chatsan-salutes" ), // cyrillic о
Entry( "0_o", "chatsan-wide-eyed" ),
Entry( "лмао", "chatsan-laughs" ),
Entry( "рофл", "chatsan-laughs" ),
Entry( "яхз", "chatsan-shrugs" ),
Entry( ":0", "chatsan-surprised" ),
Entry( ":р", "chatsan-stick-out-tongue" ), // cyrillic р
Entry( "кек", "chatsan-laughs" ),
Entry( "T_T", "chatsan-cries" ),
Entry( "Т_Т", "chatsan-cries" ), // cyrillic T
Entry( "=_(", "chatsan-cries" ),
Entry( "))", "chatsan-smiles-widely" ),
Entry( ")", "chatsan-smiles" ),
Entry( "((", "chatsan-frowns-deeply" ),
Entry( "(", "chatsan-frowns" ),
// CP14-RU-Localization-End
// I could've done this with regex, but felt it wasn't the right idea.
{ ":)", "chatsan-smiles" },
{ ":]", "chatsan-smiles" },
{ "=)", "chatsan-smiles" },
{ "=]", "chatsan-smiles" },
{ "(:", "chatsan-smiles" },
{ "[:", "chatsan-smiles" },
{ "(=", "chatsan-smiles" },
{ "[=", "chatsan-smiles" },
{ "^^", "chatsan-smiles" },
{ "^-^", "chatsan-smiles" },
{ ":(", "chatsan-frowns" },
{ ":[", "chatsan-frowns" },
{ "=(", "chatsan-frowns" },
{ "=[", "chatsan-frowns" },
{ "):", "chatsan-frowns" },
{ ")=", "chatsan-frowns" },
{ "]:", "chatsan-frowns" },
{ "]=", "chatsan-frowns" },
{ ":D", "chatsan-smiles-widely" },
{ "D:", "chatsan-frowns-deeply" },
{ ":O", "chatsan-surprised" },
{ ":3", "chatsan-smiles" },
{ ":S", "chatsan-uncertain" },
{ ":>", "chatsan-grins" },
{ ":<", "chatsan-pouts" },
{ "xD", "chatsan-laughs" },
{ ":'(", "chatsan-cries" },
{ ":'[", "chatsan-cries" },
{ "='(", "chatsan-cries" },
{ "='[", "chatsan-cries" },
{ ")':", "chatsan-cries" },
{ "]':", "chatsan-cries" },
{ ")'=", "chatsan-cries" },
{ "]'=", "chatsan-cries" },
{ ";-;", "chatsan-cries" },
{ ";_;", "chatsan-cries" },
{ "qwq", "chatsan-cries" },
{ ":u", "chatsan-smiles-smugly" },
{ ":v", "chatsan-smiles-smugly" },
{ ">:i", "chatsan-annoyed" },
{ ":i", "chatsan-sighs" },
{ ":|", "chatsan-sighs" },
{ ":p", "chatsan-stick-out-tongue" },
{ ";p", "chatsan-stick-out-tongue" },
{ ":b", "chatsan-stick-out-tongue" },
{ "0-0", "chatsan-wide-eyed" },
{ "o-o", "chatsan-wide-eyed" },
{ "o.o", "chatsan-wide-eyed" },
{ "._.", "chatsan-surprised" },
{ ".-.", "chatsan-confused" },
{ "-_-", "chatsan-unimpressed" },
{ "smh", "chatsan-unimpressed" },
{ "o/", "chatsan-waves" },
{ "^^/", "chatsan-waves" },
{ ":/", "chatsan-uncertain" },
{ ":\\", "chatsan-uncertain" },
{ "lmao", "chatsan-laughs" },
{ "lmfao", "chatsan-laughs" },
{ "lol", "chatsan-laughs" },
{ "lel", "chatsan-laughs" },
{ "kek", "chatsan-laughs" },
{ "rofl", "chatsan-laughs" },
{ "o7", "chatsan-salutes" },
{ ";_;7", "chatsan-tearfully-salutes" },
{ "idk", "chatsan-shrugs" },
{ ";)", "chatsan-winks" },
{ ";]", "chatsan-winks" },
{ "(;", "chatsan-winks" },
{ "[;", "chatsan-winks" },
{ ":')", "chatsan-tearfully-smiles" },
{ ":']", "chatsan-tearfully-smiles" },
{ "=')", "chatsan-tearfully-smiles" },
{ "=']", "chatsan-tearfully-smiles" },
{ "(':", "chatsan-tearfully-smiles" },
{ "[':", "chatsan-tearfully-smiles" },
{ "('=", "chatsan-tearfully-smiles" },
{ "['=", "chatsan-tearfully-smiles" }
};
Entry(":)", "chatsan-smiles"),
Entry(":]", "chatsan-smiles"),
Entry("=)", "chatsan-smiles"),
Entry("=]", "chatsan-smiles"),
Entry("(:", "chatsan-smiles"),
Entry("[:", "chatsan-smiles"),
Entry("(=", "chatsan-smiles"),
Entry("[=", "chatsan-smiles"),
Entry("^^", "chatsan-smiles"),
Entry("^-^", "chatsan-smiles"),
Entry(":(", "chatsan-frowns"),
Entry(":[", "chatsan-frowns"),
Entry("=(", "chatsan-frowns"),
Entry("=[", "chatsan-frowns"),
Entry("):", "chatsan-frowns"),
Entry(")=", "chatsan-frowns"),
Entry("]:", "chatsan-frowns"),
Entry("]=", "chatsan-frowns"),
Entry(":D", "chatsan-smiles-widely"),
Entry("D:", "chatsan-frowns-deeply"),
Entry(":O", "chatsan-surprised"),
Entry(":3", "chatsan-smiles"),
Entry(":S", "chatsan-uncertain"),
Entry(":>", "chatsan-grins"),
Entry(":<", "chatsan-pouts"),
Entry("xD", "chatsan-laughs"),
Entry(":'(", "chatsan-cries"),
Entry(":'[", "chatsan-cries"),
Entry("='(", "chatsan-cries"),
Entry("='[", "chatsan-cries"),
Entry(")':", "chatsan-cries"),
Entry("]':", "chatsan-cries"),
Entry(")'=", "chatsan-cries"),
Entry("]'=", "chatsan-cries"),
Entry(";-;", "chatsan-cries"),
Entry(";_;", "chatsan-cries"),
Entry("qwq", "chatsan-cries"),
Entry(":u", "chatsan-smiles-smugly"),
Entry(":v", "chatsan-smiles-smugly"),
Entry(">:i", "chatsan-annoyed"),
Entry(":i", "chatsan-sighs"),
Entry(":|", "chatsan-sighs"),
Entry(":p", "chatsan-stick-out-tongue"),
Entry(";p", "chatsan-stick-out-tongue"),
Entry(":b", "chatsan-stick-out-tongue"),
Entry("0-0", "chatsan-wide-eyed"),
Entry("o-o", "chatsan-wide-eyed"),
Entry("o.o", "chatsan-wide-eyed"),
Entry("._.", "chatsan-surprised"),
Entry(".-.", "chatsan-confused"),
Entry("-_-", "chatsan-unimpressed"),
Entry("smh", "chatsan-unimpressed"),
Entry("o/", "chatsan-waves"),
Entry("^^/", "chatsan-waves"),
Entry(":/", "chatsan-uncertain"),
Entry(":\\", "chatsan-uncertain"),
Entry("lmao", "chatsan-laughs"),
Entry("lmfao", "chatsan-laughs"),
Entry("lol", "chatsan-laughs"),
Entry("lel", "chatsan-laughs"),
Entry("kek", "chatsan-laughs"),
Entry("rofl", "chatsan-laughs"),
Entry("o7", "chatsan-salutes"),
Entry(";_;7", "chatsan-tearfully-salutes"),
Entry("idk", "chatsan-shrugs"),
Entry(";)", "chatsan-winks"),
Entry(";]", "chatsan-winks"),
Entry("(;", "chatsan-winks"),
Entry("[;", "chatsan-winks"),
Entry(":')", "chatsan-tearfully-smiles"),
Entry(":']", "chatsan-tearfully-smiles"),
Entry("=')", "chatsan-tearfully-smiles"),
Entry("=']", "chatsan-tearfully-smiles"),
Entry("(':", "chatsan-tearfully-smiles"),
Entry("[':", "chatsan-tearfully-smiles"),
Entry("('=", "chatsan-tearfully-smiles"),
Entry("['=", "chatsan-tearfully-smiles"),
];
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
@@ -149,21 +148,8 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
// -1 is just a canary for nothing found yet
var lastEmoteIndex = -1;
foreach (var (shorthand, emoteKey) in ShorthandToEmote)
foreach (var (r, emoteKey) in ShorthandToEmote)
{
// We have to escape it because shorthands like ":)" or "-_-" would break the regex otherwise.
var escaped = Regex.Escape(shorthand);
// So there are 2 cases:
// - If there is whitespace before it and after it is either punctuation, whitespace, or the end of the line
// Delete the word and the whitespace before
// - If it is at the start of the string and is followed by punctuation, whitespace, or the end of the line
// Delete the word and the punctuation if it exists.
var pattern =
$@"\s{escaped}(?=\p{{P}}|\s|$)|^{escaped}(?:\p{{P}}|(?=\s|$))";
var r = new Regex(pattern, RegexOptions.RightToLeft | RegexOptions.IgnoreCase);
// We're using sanitized as the original message until the end so that we can make sure the indices of
// the emotes are accurate.
var lastMatch = r.Match(sanitized);
@@ -183,4 +169,21 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
sanitized = message.Trim();
return emote is not null;
}
private static (Regex regex, string emoteKey) Entry(string shorthand, string emoteKey)
{
// We have to escape it because shorthands like ":)" or "-_-" would break the regex otherwise.
var escaped = Regex.Escape(shorthand);
// So there are 2 cases:
// - If there is whitespace before it and after it is either punctuation, whitespace, or the end of the line
// Delete the word and the whitespace before
// - If it is at the start of the string and is followed by punctuation, whitespace, or the end of the line
// Delete the word and the punctuation if it exists.
var pattern = new Regex(
$@"\s{escaped}(?=\p{{P}}|\s|$)|^{escaped}(?:\p{{P}}|(?=\s|$))",
RegexOptions.RightToLeft | RegexOptions.IgnoreCase | RegexOptions.Compiled);
return (pattern, emoteKey);
}
}

View File

@@ -101,7 +101,7 @@ public sealed class CluwneSystem : EntitySystem
else if (_robustRandom.Prob(component.KnockChance))
{
_audio.PlayPvs(component.KnockSound, uid);
_stunSystem.TryParalyze(uid, TimeSpan.FromSeconds(component.ParalyzeTime), true);
_stunSystem.TryUpdateParalyzeDuration(uid, TimeSpan.FromSeconds(component.ParalyzeTime));
_chat.TrySendInGameICMessage(uid, "spasms", InGameICChatType.Emote, ChatTransmitRange.Normal);
}
}

View File

@@ -42,7 +42,6 @@ namespace Content.Server.Communications
{
// All events that refresh the BUI
SubscribeLocalEvent<AlertLevelChangedEvent>(OnAlertLevelChanged);
SubscribeLocalEvent<CommunicationsConsoleComponent, ComponentInit>((uid, comp, _) => UpdateCommsConsoleInterface(uid, comp));
SubscribeLocalEvent<RoundEndSystemChangedEvent>(_ => OnGenericBroadcastEvent());
SubscribeLocalEvent<AlertLevelDelayFinishedEvent>(_ => OnGenericBroadcastEvent());
@@ -85,6 +84,7 @@ namespace Content.Server.Communications
public void OnCommunicationsConsoleMapInit(EntityUid uid, CommunicationsConsoleComponent comp, MapInitEvent args)
{
comp.AnnouncementCooldownRemaining = comp.InitialDelay;
UpdateCommsConsoleInterface(uid, comp);
}
/// <summary>

View File

@@ -1,4 +1,4 @@
using Content.Shared.Bed.Sleep;
using Content.Shared.Bed.Sleep;
using Content.Shared.Drowsiness;
using Content.Shared.StatusEffectNew;
using Content.Shared.StatusEffectNew.Components;

View File

@@ -397,7 +397,12 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
var shouldStun = siemensCoefficient > 0.5f;
if (shouldStun)
_stun.TryParalyze(uid, time * ParalyzeTimeMultiplier, refresh, statusEffects);
{
_ = refresh
? _stun.TryUpdateParalyzeDuration(uid, time * ParalyzeTimeMultiplier)
: _stun.TryAddParalyzeDuration(uid, time * ParalyzeTimeMultiplier);
}
// TODO: Sparks here.

View File

@@ -1,5 +0,0 @@
using System.Threading;
using Content.Shared.Ensnaring.Components;

View File

@@ -133,7 +133,7 @@ public sealed class EntityEffectSystem : EntitySystem
args.Result = false;
if (TryComp(args.Args.TargetEntity, out TemperatureComponent? temp))
{
if (temp.CurrentTemperature > args.Condition.Min && temp.CurrentTemperature < args.Condition.Max)
if (temp.CurrentTemperature >= args.Condition.Min && temp.CurrentTemperature <= args.Condition.Max)
args.Result = true;
}
}

View File

@@ -339,6 +339,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
// Ensure we actually have the component
EnsureComp<TileFrictionModifierComponent>(entity);
EnsureComp<SlipperyComponent>(entity, out var slipComp);
// This is the base amount of reagent needed before a puddle can be considered slippery. Is defined based on
// the sprite threshold for a puddle larger than 5 pixels.
var smallPuddleThreshold = FixedPoint2.New(entity.Comp.OverflowVolume.Float() * LowThreshold);
@@ -357,17 +359,21 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
var launchMult = FixedPoint2.Zero;
// A cumulative weighted amount of stun times from slippery reagents
var stunTimer = TimeSpan.Zero;
// A cumulative weighted amount of knockdown times from slippery reagents
var knockdownTimer = TimeSpan.Zero;
// Check if the puddle is big enough to slip in to avoid doing unnecessary logic
if (solution.Volume <= smallPuddleThreshold)
{
_stepTrigger.SetActive(entity, false, comp);
_tile.SetModifier(entity, 1f);
slipComp.SlipData.SlipFriction = 1f;
slipComp.AffectsSliding = false;
Dirty(entity, slipComp);
return;
}
if (!TryComp<SlipperyComponent>(entity, out var slipComp))
return;
slipComp.AffectsSliding = true;
foreach (var (reagent, quantity) in solution.Contents)
{
@@ -387,7 +393,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
// Aggregate launch speed based on quantity
launchMult += reagentProto.SlipData.LaunchForwardsMultiplier * quantity;
// Aggregate stun times based on quantity
stunTimer += reagentProto.SlipData.ParalyzeTime * (float)quantity;
stunTimer += reagentProto.SlipData.StunTime * (float)quantity;
knockdownTimer += reagentProto.SlipData.KnockdownTime * (float)quantity;
if (reagentProto.SlipData.SuperSlippery)
superSlipperyUnits += quantity;
@@ -405,8 +412,9 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
// A puddle with 10 units of lube vs a puddle with 10 of lube and 20 catchup should stun and launch forward the same amount.
if (slipperyUnits > 0)
{
slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult / slipperyUnits);
slipComp.SlipData.ParalyzeTime = stunTimer / (float)slipperyUnits;
slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult/slipperyUnits);
slipComp.SlipData.StunTime = (stunTimer/(float)slipperyUnits);
slipComp.SlipData.KnockdownTime = (knockdownTimer/(float)slipperyUnits);
}
// Only make it super slippery if there is enough super slippery units for its own puddle

View File

@@ -228,7 +228,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
continue;
_npcFaction.RemoveFaction(uid, RevolutionaryNpcFaction);
_stun.TryParalyze(uid, stunTime, true);
_stun.TryUpdateParalyzeDuration(uid, stunTime);
RemCompDeferred<RevolutionaryComponent>(uid);
_popup.PopupEntity(Loc.GetString("rev-break-control", ("name", Identity.Entity(uid, EntityManager))), uid);
_adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(uid)} was deconverted due to all Head Revolutionaries dying.");

View File

@@ -125,8 +125,8 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
if (traitorRole is not null)
{
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Add traitor briefing components");
AddComp<RoleBriefingComponent>(traitorRole.Value.Owner);
Comp<RoleBriefingComponent>(traitorRole.Value.Owner).Briefing = briefing;
EnsureComp<RoleBriefingComponent>(traitorRole.Value.Owner, out var briefingComp);
briefingComp.Briefing = briefing;
}
else
{

View File

@@ -5,7 +5,6 @@ using Content.Server.Ghost.Roles.Components;
using Content.Server.Ghost.Roles.Events;
using Content.Shared.Ghost.Roles.Raffles;
using Content.Server.Ghost.Roles.UI;
using Content.Server.Mind.Commands;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Database;
@@ -698,7 +697,7 @@ public sealed class GhostRoleSystem : EntitySystem
RaiseLocalEvent(mob, spawnedEvent);
if (ghostRole.MakeSentient)
MakeSentientCommand.MakeSentient(mob, EntityManager, ghostRole.AllowMovement, ghostRole.AllowSpeech);
_mindSystem.MakeSentient(mob, ghostRole.AllowMovement, ghostRole.AllowSpeech);
EnsureComp<MindContainerComponent>(mob);
@@ -745,7 +744,7 @@ public sealed class GhostRoleSystem : EntitySystem
}
if (ghostRole.MakeSentient)
MakeSentientCommand.MakeSentient(uid, EntityManager, ghostRole.AllowMovement, ghostRole.AllowSpeech);
_mindSystem.MakeSentient(uid, ghostRole.AllowMovement, ghostRole.AllowSpeech);
GhostRoleInternalCreateMindAndTransfer(args.Player, uid, uid, ghostRole);
UnregisterGhostRole((uid, ghostRole));

View File

@@ -1,131 +0,0 @@
using Content.Server.Administration.Logs;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Database;
using Content.Shared.Glue;
using Content.Shared.Hands;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Components;
using Content.Shared.Item;
using Content.Shared.NameModifier.EntitySystems;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Timing;
namespace Content.Server.Glue;
public sealed class GlueSystem : SharedGlueSystem
{
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly OpenableSystem _openable = default!;
[Dependency] private readonly NameModifierSystem _nameMod = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GlueComponent, AfterInteractEvent>(OnInteract, after: new[] { typeof(OpenableSystem) });
SubscribeLocalEvent<GluedComponent, ComponentInit>(OnGluedInit);
SubscribeLocalEvent<GlueComponent, GetVerbsEvent<UtilityVerb>>(OnUtilityVerb);
SubscribeLocalEvent<GluedComponent, GotEquippedHandEvent>(OnHandPickUp);
SubscribeLocalEvent<GluedComponent, RefreshNameModifiersEvent>(OnRefreshNameModifiers);
}
// When glue bottle is used on item it will apply the glued and unremoveable components.
private void OnInteract(Entity<GlueComponent> entity, ref AfterInteractEvent args)
{
if (args.Handled)
return;
if (!args.CanReach || args.Target is not { Valid: true } target)
return;
if (TryGlue(entity, target, args.User))
args.Handled = true;
}
private void OnUtilityVerb(Entity<GlueComponent> entity, ref GetVerbsEvent<UtilityVerb> args)
{
if (!args.CanInteract || !args.CanAccess || args.Target is not { Valid: true } target ||
_openable.IsClosed(entity))
return;
var user = args.User;
var verb = new UtilityVerb()
{
Act = () => TryGlue(entity, target, user),
IconEntity = GetNetEntity(entity),
Text = Loc.GetString("glue-verb-text"),
Message = Loc.GetString("glue-verb-message")
};
args.Verbs.Add(verb);
}
private bool TryGlue(Entity<GlueComponent> entity, EntityUid target, EntityUid actor)
{
// if item is glued then don't apply glue again so it can be removed for reasonable time
// If glue is applied to an unremoveable item, the component will disappear after the duration.
// This effecitvely means any unremoveable item could be removed with a bottle of glue.
if (HasComp<GluedComponent>(target) || !HasComp<ItemComponent>(target) || HasComp<UnremoveableComponent>(target))
{
_popup.PopupEntity(Loc.GetString("glue-failure", ("target", target)), actor, actor, PopupType.Medium);
return false;
}
if (HasComp<ItemComponent>(target) && _solutionContainer.TryGetSolution(entity.Owner, entity.Comp.Solution, out _, out var solution))
{
var quantity = solution.RemoveReagent(entity.Comp.Reagent, entity.Comp.ConsumptionUnit);
if (quantity > 0)
{
EnsureComp<GluedComponent>(target).Duration = quantity.Double() * entity.Comp.DurationPerUnit;
_adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(actor):actor} glued {ToPrettyString(target):subject} with {ToPrettyString(entity.Owner):tool}");
_audio.PlayPvs(entity.Comp.Squeeze, entity.Owner);
_popup.PopupEntity(Loc.GetString("glue-success", ("target", target)), actor, actor, PopupType.Medium);
return true;
}
}
_popup.PopupEntity(Loc.GetString("glue-failure", ("target", target)), actor, actor, PopupType.Medium);
return false;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<GluedComponent, UnremoveableComponent>();
while (query.MoveNext(out var uid, out var glue, out var _))
{
if (_timing.CurTime < glue.Until)
continue;
RemComp<UnremoveableComponent>(uid);
RemComp<GluedComponent>(uid);
_nameMod.RefreshNameModifiers(uid);
}
}
private void OnGluedInit(Entity<GluedComponent> entity, ref ComponentInit args)
{
_nameMod.RefreshNameModifiers(entity.Owner);
}
private void OnHandPickUp(Entity<GluedComponent> entity, ref GotEquippedHandEvent args)
{
var comp = EnsureComp<UnremoveableComponent>(entity);
comp.DeleteOnDrop = false;
entity.Comp.Until = _timing.CurTime + entity.Comp.Duration;
}
private void OnRefreshNameModifiers(Entity<GluedComponent> entity, ref RefreshNameModifiersEvent args)
{
args.AddModifier("glued-name-prefix");
}
}

View File

@@ -461,7 +461,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
{
if (instrument.InstrumentPlayer is {Valid: true} mob)
{
_stuns.TryParalyze(mob, TimeSpan.FromSeconds(1), true);
_stuns.TryUpdateParalyzeDuration(mob, TimeSpan.FromSeconds(1));
_popup.PopupEntity(Loc.GetString("instrument-component-finger-cramps-max-message"),
uid, mob, PopupType.LargeCaution);

View File

@@ -2,16 +2,15 @@ using Content.Server.Body.Systems;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Forensics;
using Content.Server.Popups;
using Content.Server.Stunnable;
using Content.Shared.Body.Components;
using Content.Shared.Body.Systems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.IdentityManagement;
using Content.Shared.Movement.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.StatusEffect;
using Robust.Server.Audio;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
@@ -27,7 +26,7 @@ namespace Content.Server.Medical
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly PuddleSystem _puddle = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly StunSystem _stun = default!;
[Dependency] private readonly MovementModStatusSystem _movementMod = default!;
[Dependency] private readonly ThirstSystem _thirst = default!;
[Dependency] private readonly ForensicsSystem _forensics = default!;
[Dependency] private readonly BloodstreamSystem _bloodstream = default!;
@@ -57,8 +56,7 @@ namespace Content.Server.Medical
// It fully empties the stomach, this amount from the chem stream is relatively small
var solutionSize = (MathF.Abs(thirstAdded) + MathF.Abs(hungerAdded)) / 6;
// Apply a bit of slowdown
if (TryComp<StatusEffectsComponent>(uid, out var status))
_stun.TrySlowdown(uid, TimeSpan.FromSeconds(solutionSize), true, 0.5f, 0.5f, status);
_movementMod.TryUpdateMovementSpeedModDuration(uid, MovementModStatusSystem.VomitingSlowdown, TimeSpan.FromSeconds(solutionSize), 0.5f);
// TODO: Need decals
var solution = new Solution();

View File

@@ -1,63 +1,30 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Emoting;
using Content.Shared.Examine;
using Content.Shared.Mind.Components;
using Content.Shared.Movement.Components;
using Content.Shared.Speech;
using Robust.Shared.Console;
namespace Content.Server.Mind.Commands
namespace Content.Server.Mind.Commands;
[AdminCommand(AdminFlags.Admin)]
public sealed class MakeSentientCommand : LocalizedEntityCommands
{
[AdminCommand(AdminFlags.Admin)]
public sealed class MakeSentientCommand : IConsoleCommand
[Dependency] private readonly MindSystem _mindSystem = default!;
public override string Command => "makesentient";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
[Dependency] private readonly IEntityManager _entManager = default!;
public string Command => "makesentient";
public string Description => "Makes an entity sentient (able to be controlled by a player)";
public string Help => "makesentient <entity id>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
if (args.Length != 1)
{
if (args.Length != 1)
{
shell.WriteLine("Wrong number of arguments.");
return;
}
if (!NetEntity.TryParse(args[0], out var entNet) || !_entManager.TryGetEntity(entNet, out var entId))
{
shell.WriteLine("Invalid argument.");
return;
}
if (!_entManager.EntityExists(entId))
{
shell.WriteLine("Invalid entity specified!");
return;
}
MakeSentient(entId.Value, _entManager, true, true);
shell.WriteLine(Loc.GetString("shell-need-exactly-one-argument"));
return;
}
public static void MakeSentient(EntityUid uid, IEntityManager entityManager, bool allowMovement = true, bool allowSpeech = true)
if (!NetEntity.TryParse(args[0], out var entNet) || !EntityManager.TryGetEntity(entNet, out var entId) || !EntityManager.EntityExists(entId))
{
entityManager.EnsureComponent<MindContainerComponent>(uid);
if (allowMovement)
{
entityManager.EnsureComponent<InputMoverComponent>(uid);
entityManager.EnsureComponent<MobMoverComponent>(uid);
entityManager.EnsureComponent<MovementSpeedModifierComponent>(uid);
}
if (allowSpeech)
{
entityManager.EnsureComponent<SpeechComponent>(uid);
entityManager.EnsureComponent<EmotingComponent>(uid);
}
entityManager.EnsureComponent<ExaminerComponent>(uid);
shell.WriteLine(Loc.GetString("shell-could-not-find-entity-with-uid", ("uid", args[0])));
return;
}
_mindSystem.MakeSentient(entId.Value);
}
}

View File

@@ -1,7 +1,6 @@
using Content.Server.Administration.Logs;
using Content.Server.GameTicking;
using Content.Server.Ghost;
using Content.Server.Mind.Commands;
using Content.Shared.Database;
using Content.Shared.Ghost;
using Content.Shared.Mind;
@@ -349,7 +348,7 @@ public sealed class MindSystem : SharedMindSystem
return;
}
MakeSentientCommand.MakeSentient(target, EntityManager);
MakeSentient(target);
TransferTo(mindId, target, ghostCheckOverride: true, mind: mind);
}
}

View File

@@ -65,6 +65,10 @@ public sealed class CritMobActionsSystem : EntitySystem
_quickDialog.OpenDialog(actor.PlayerSession, Loc.GetString("action-name-crit-last-words"), "",
(string lastWords) =>
{
// if a person is gibbed/deleted, they can't say last words
if (Deleted(uid))
return;
// Intentionally does not check for muteness
if (actor.PlayerSession.AttachedEntity != uid
|| !_mobState.IsCritical(uid))

View File

@@ -205,7 +205,7 @@ public sealed partial class NPCCombatSystem
return;
}
_gun.AttemptShoot(uid, gunUid, gun, targetCordinates);
_gun.AttemptShoot(uid, gunUid, gun, targetCordinates, comp.Target);
}
}
}

View File

@@ -30,11 +30,16 @@ using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Content.Shared.Prying.Systems;
using Microsoft.Extensions.ObjectPool;
using Prometheus;
namespace Content.Server.NPC.Systems;
public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
{
private static readonly Gauge ActiveSteeringGauge = Metrics.CreateGauge(
"npc_steering_active_count",
"Amount of NPCs trying to actively do steering");
/*
* We use context steering to determine which way to move.
* This involves creating an array of possible directions and assigning a value for the desireability of each direction.
@@ -87,6 +92,8 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
private object _obstacles = new();
private int _activeSteeringCount;
public override void Initialize()
{
base.Initialize();
@@ -244,12 +251,15 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
};
var curTime = _timing.CurTime;
_activeSteeringCount = 0;
Parallel.For(0, index, options, i =>
{
var (uid, steering, mover, xform) = npcs[i];
Steer(uid, steering, mover, xform, frameTime, curTime);
});
ActiveSteeringGauge.Set(_activeSteeringCount);
if (_subscribedSessions.Count > 0)
{
@@ -324,6 +334,8 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
return;
}
Interlocked.Increment(ref _activeSteeringCount);
var agentRadius = steering.Radius;
var worldPos = _transform.GetWorldPosition(xform);
var (layer, mask) = _physics.GetHardCollision(uid);

View File

@@ -8,6 +8,7 @@ using Content.Shared.Mobs;
using Content.Shared.Mobs.Systems;
using Content.Shared.NPC;
using Content.Shared.NPC.Systems;
using Prometheus;
using Robust.Server.GameObjects;
using Robust.Shared.Configuration;
using Robust.Shared.Player;
@@ -19,6 +20,10 @@ namespace Content.Server.NPC.Systems
/// </summary>
public sealed partial class NPCSystem : EntitySystem
{
private static readonly Gauge ActiveGauge = Metrics.CreateGauge(
"npc_active_count",
"Amount of NPCs that are actively processing");
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly HTNSystem _htn = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
@@ -138,6 +143,8 @@ namespace Content.Server.NPC.Systems
// Add your system here.
_htn.UpdateNPC(ref _count, _maxUpdates, frameTime);
ActiveGauge.Set(Count<ActiveNPCComponent>());
}
public void OnMobStateChange(EntityUid uid, HTNComponent component, MobStateChangedEvent args)

View File

@@ -82,8 +82,8 @@ public sealed class NameIdentifierSystem : EntitySystem
randomVal = set[^1];
set.RemoveAt(set.Count - 1);
return proto.Prefix is not null
? $"{proto.Prefix}-{randomVal}"
return proto.Format is not null
? Loc.GetString(proto.Format, ("number", randomVal))
: $"{randomVal}";
}
@@ -104,8 +104,8 @@ public sealed class NameIdentifierSystem : EntitySystem
ids.Remove(ent.Comp.Identifier))
{
id = ent.Comp.Identifier;
uniqueName = group.Prefix is not null
? $"{group.Prefix}-{id}"
uniqueName = group.Format is not null
? Loc.GetString(group.Format, ("number", id))
: $"{id}";
}
else

View File

@@ -63,7 +63,7 @@ public sealed class StunProviderSystem : SharedStunProviderSystem
_audio.PlayPvs(comp.Sound, target);
_damageable.TryChangeDamage(target, comp.StunDamage, false, true, null, origin: uid);
_stun.TryParalyze(target, comp.StunTime, refresh: false);
_stun.TryAddParalyzeDuration(target, comp.StunTime);
// short cooldown to prevent instant stunlocking
_useDelay.SetLength((uid, useDelay), comp.Cooldown, id: comp.DelayId);

View File

@@ -7,6 +7,7 @@ using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.Shuttles.Components;
using Content.Shared.Shuttles.Systems;
using Prometheus;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
using DroneConsoleComponent = Content.Server.Shuttles.DroneConsoleComponent;
@@ -17,6 +18,10 @@ namespace Content.Server.Physics.Controllers;
public sealed class MoverController : SharedMoverController
{
private static readonly Gauge ActiveMoverGauge = Metrics.CreateGauge(
"physics_active_mover_count",
"Active amount of InputMovers being processed by MoverController");
[Dependency] private readonly ThrusterSystem _thruster = default!;
[Dependency] private readonly SharedTransformSystem _xformSystem = default!;
@@ -97,6 +102,8 @@ public sealed class MoverController : SharedMoverController
HandleMobMovement(mover, frameTime);
}
ActiveMoverGauge.Set(_movers.Count);
HandleShuttleMovement(frameTime);
}

View File

@@ -1,4 +1,3 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Storage.EntitySystems;
using Content.Server.Stunnable;
@@ -7,7 +6,6 @@ using Content.Shared.Atmos.Components;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Interaction;
using Content.Shared.PneumaticCannon;
using Content.Shared.StatusEffect;
using Content.Shared.Tools.Systems;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
@@ -80,10 +78,9 @@ public sealed class PneumaticCannonSystem : SharedPneumaticCannonSystem
if (gas == null && component.GasUsage > 0f)
return;
if (TryComp<StatusEffectsComponent>(args.User, out var status)
&& component.Power == PneumaticCannonPower.High)
if (component.Power == PneumaticCannonPower.High
&& _stun.TryUpdateParalyzeDuration(args.User, TimeSpan.FromSeconds(component.HighPowerStunTime)))
{
_stun.TryParalyze(args.User, TimeSpan.FromSeconds(component.HighPowerStunTime), true, status);
Popup.PopupEntity(Loc.GetString("pneumatic-cannon-component-power-stun",
("cannon", uid)), cannon, args.User);
}

View File

@@ -1,10 +1,7 @@
using Content.Server.Actions;
using Content.Server.Humanoid;
using Content.Server.Inventory;
using Content.Server.Mind.Commands;
using Content.Server.Polymorph.Components;
using Content.Shared.Actions;
using Content.Shared.Actions.Components;
using Content.Shared.Buckle;
using Content.Shared.Coordinates;
using Content.Shared.Damage;
@@ -210,7 +207,7 @@ public sealed partial class PolymorphSystem : EntitySystem
("child", Identity.Entity(child, EntityManager))),
child);
MakeSentientCommand.MakeSentient(child, EntityManager);
_mindSystem.MakeSentient(child);
var polymorphedComp = Factory.GetComponent<PolymorphedEntityComponent>();
polymorphedComp.Parent = uid;

View File

@@ -1,4 +1,6 @@
using Content.Server.Power.Components;
using Content.Server.Administration.Logs;
using Content.Server.Power.Components;
using Content.Shared.Database;
using Content.Shared.Power;
using Robust.Server.GameObjects;
@@ -19,6 +21,7 @@ namespace Content.Server.Power.EntitySystems;
/// </remarks>
public sealed class BatteryInterfaceSystem : EntitySystem
{
[Dependency] private readonly IAdminLogManager _adminLog = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = null!;
public override void Initialize()
@@ -43,12 +46,16 @@ public sealed class BatteryInterfaceSystem : EntitySystem
{
var netBattery = Comp<PowerNetworkBatteryComponent>(ent);
netBattery.CanCharge = args.On;
_adminLog.Add(LogType.Action,$"{ToPrettyString(args.Actor):actor} set input breaker to {args.On} on {ToPrettyString(ent):target}");
}
private void HandleSetOutputBreaker(Entity<BatteryInterfaceComponent> ent, ref BatterySetOutputBreakerMessage args)
{
var netBattery = Comp<PowerNetworkBatteryComponent>(ent);
netBattery.CanDischarge = args.On;
_adminLog.Add(LogType.Action,$"{ToPrettyString(args.Actor):actor} set output breaker to {args.On} on {ToPrettyString(ent):target}");
}
private void HandleSetChargeRate(Entity<BatteryInterfaceComponent> ent, ref BatterySetChargeRateMessage args)

View File

@@ -177,7 +177,7 @@ public sealed partial class RevenantSystem : EntitySystem
ChangeEssenceAmount(uid, -abilityCost, component, false);
_statusEffects.TryAddStatusEffect<CorporealComponent>(uid, "Corporeal", TimeSpan.FromSeconds(debuffs.Y), false);
_stun.TryStun(uid, TimeSpan.FromSeconds(debuffs.X), false);
_stun.TryAddStunDuration(uid, TimeSpan.FromSeconds(debuffs.X));
return true;
}

View File

@@ -1,6 +1,6 @@
using Content.Server.Administration.Managers;
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.EntitySystems;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Administration;
using Content.Shared.NodeContainer;
using Content.Shared.NodeContainer.NodeGroups;
@@ -78,7 +78,7 @@ namespace Content.Server.Sandbox.Commands
if (!EntityManager.TryGetComponent(x.Owner, out AtmosPipeColorComponent? atmosPipeColorComponent))
continue;
_pipeColorSystem.SetColor(x.Owner, atmosPipeColorComponent, color);
_pipeColorSystem.SetColor((x.Owner, atmosPipeColorComponent), color);
}
}
}

View File

@@ -635,10 +635,7 @@ public sealed partial class ShuttleSystem
{
foreach (var child in toKnock)
{
if (!_statusQuery.TryGetComponent(child, out var status))
continue;
_stuns.TryParalyze(child, _hyperspaceKnockdownTime, true, status);
_stuns.TryUpdateParalyzeDuration(child, _hyperspaceKnockdownTime);
// If the guy we knocked down is on a spaced tile, throw them too
if (grid != null)

View File

@@ -249,7 +249,7 @@ public sealed partial class ShuttleSystem
if (direction.LengthSquared() > minsq)
{
_stuns.TryKnockdown(uid, knockdownTime, true);
_stuns.TryUpdateKnockdownDuration(uid, knockdownTime);
_throwing.TryThrow(uid, direction, physics, Transform(uid), _projQuery, direction.Length(), playSound: false);
}
else

View File

@@ -19,9 +19,21 @@ namespace Content.Server.Speech.EntitySystems
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
private readonly Dictionary<ProtoId<ReplacementAccentPrototype>, (Regex regex, string replacement)[]>
_cachedReplacements = new();
public override void Initialize()
{
SubscribeLocalEvent<ReplacementAccentComponent, AccentGetEvent>(OnAccent);
_proto.PrototypesReloaded += OnPrototypesReloaded;
}
public override void Shutdown()
{
base.Shutdown();
_proto.PrototypesReloaded -= OnPrototypesReloaded;
}
private void OnAccent(EntityUid uid, ReplacementAccentComponent component, AccentGetEvent args)
@@ -48,27 +60,22 @@ namespace Content.Server.Speech.EntitySystems
return prototype.FullReplacements.Length != 0 ? Loc.GetString(_random.Pick(prototype.FullReplacements)) : "";
}
if (prototype.WordReplacements == null)
return message;
// Prohibition of repeated word replacements.
// All replaced words placed in the final message are placed here as dashes (___) with the same length.
// The regex search goes through this buffer message, from which the already replaced words are crossed out,
// ensuring that the replaced words cannot be replaced again.
var maskMessage = message;
foreach (var (first, replace) in prototype.WordReplacements)
foreach (var (regex, replace) in GetCachedReplacements(prototype))
{
var f = _loc.GetString(first);
var r = _loc.GetString(replace);
// this is kind of slow but its not that bad
// essentially: go over all matches, try to match capitalization where possible, then replace
// rather than using regex.replace
for (int i = Regex.Count(maskMessage, $@"(?<!\w){f}(?!\w)", RegexOptions.IgnoreCase); i > 0; i--)
for (int i = regex.Count(maskMessage); i > 0; i--)
{
// fetch the match again as the character indices may have changed
Match match = Regex.Match(maskMessage, $@"(?<!\w){f}(?!\w)", RegexOptions.IgnoreCase);
var replacement = r;
Match match = regex.Match(maskMessage);
var replacement = replace;
// Intelligently replace capitalization
// two cases where we will do so:
@@ -98,5 +105,40 @@ namespace Content.Server.Speech.EntitySystems
return message;
}
private (Regex regex, string replacement)[] GetCachedReplacements(ReplacementAccentPrototype prototype)
{
if (!_cachedReplacements.TryGetValue(prototype.ID, out var replacements))
{
replacements = GenerateCachedReplacements(prototype);
_cachedReplacements.Add(prototype.ID, replacements);
}
return replacements;
}
private (Regex regex, string replacement)[] GenerateCachedReplacements(ReplacementAccentPrototype prototype)
{
if (prototype.WordReplacements is not { } replacements)
return [];
return replacements.Select(kv =>
{
var (first, replace) = kv;
var firstLoc = _loc.GetString(first);
var replaceLoc = _loc.GetString(replace);
var regex = new Regex($@"(?<!\w){firstLoc}(?!\w)", RegexOptions.IgnoreCase);
return (regex, replaceLoc);
})
.ToArray();
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs obj)
{
_cachedReplacements.Clear();
}
}
}

View File

@@ -1,4 +1,4 @@
using Content.Server.Abilities.Mime;
using Content.Shared.Abilities.Mime;
using Content.Server.Chat.Systems;
using Content.Server.Popups;
using Content.Server.Speech.Components;

View File

@@ -1,5 +1,5 @@
using Content.Server.Atmos.Piping.Components;
using Content.Server.Atmos.Piping.EntitySystems;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Content.Server.Charges;
using Content.Server.Decals;
using Content.Server.Destructible;
@@ -147,7 +147,7 @@ public sealed class SprayPainterSystem : SharedSprayPainterSystem
return;
Audio.PlayPvs(ent.Comp.SpraySound, ent);
_pipeColor.SetColor(target, color, args.Color);
_pipeColor.SetColor((target, color), args.Color);
args.Handled = true;
}

View File

@@ -1,7 +1,7 @@
using Content.Server.Access.Systems;
using Content.Server.Humanoid;
using Content.Server.IdentityManagement;
using Content.Server.Mind.Commands;
using Content.Server.Mind;
using Content.Server.PDA;
using Content.Server.Station.Components;
using Content.Shared.Access.Components;
@@ -41,6 +41,7 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly PdaSystem _pdaSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
/// <summary>
/// Attempts to spawn a player character onto the given station.
@@ -110,7 +111,7 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
{
DebugTools.Assert(entity is null);
var jobEntity = Spawn(prototype.JobEntity, coordinates);
MakeSentientCommand.MakeSentient(jobEntity, EntityManager);
_mindSystem.MakeSentient(jobEntity);
// Make sure custom names get handled, what is gameticker control flow whoopy.
if (loadout != null)

View File

@@ -8,21 +8,47 @@ namespace Content.Server.Stunnable.Components
{
// TODO: Can probably predict this.
// See stunsystem for what these do
[DataField("stunAmount")]
public int StunAmount;
/// <summary>
/// How long we are stunned for
/// </summary>
[DataField]
public TimeSpan StunAmount;
[DataField("knockdownAmount")]
public int KnockdownAmount;
/// <summary>
/// How long we are knocked down for
/// </summary>
[DataField]
public TimeSpan KnockdownAmount;
[DataField("slowdownAmount")]
public int SlowdownAmount;
/// <summary>
/// How long we are slowed down for
/// </summary>
[DataField]
public TimeSpan SlowdownAmount;
[DataField("walkSpeedMultiplier")]
public float WalkSpeedMultiplier = 1f;
/// <summary>
/// Multiplier for a mob's walking speed
/// </summary>
[DataField]
public float WalkSpeedModifier = 1f;
[DataField("runSpeedMultiplier")]
public float RunSpeedMultiplier = 1f;
/// <summary>
/// Multiplier for a mob's sprinting speed
/// </summary>
[DataField]
public float SprintSpeedModifier = 1f;
/// <summary>
/// Refresh Stun or Slowdown on hit
/// </summary>
[DataField]
public bool Refresh = true;
/// <summary>
/// Should the entity try and stand automatically after being knocked down?
/// </summary>
[DataField]
public bool AutoStand = true;
/// <summary>
/// Fixture we track for the collision.

View File

@@ -0,0 +1,6 @@
using Content.Shared.Stunnable;
namespace Content.Server.Stunnable;
public sealed class StunSystem : SharedStunSystem;

View File

@@ -1,8 +1,6 @@
using Content.Server.Stunnable.Components;
using Content.Shared.Standing;
using Content.Shared.StatusEffect;
using Content.Shared.Movement.Systems;
using JetBrains.Annotations;
using Robust.Shared.Physics.Dynamics;
using Content.Shared.Throwing;
using Robust.Shared.Physics.Events;
@@ -12,6 +10,7 @@ namespace Content.Server.Stunnable
internal sealed class StunOnCollideSystem : EntitySystem
{
[Dependency] private readonly StunSystem _stunSystem = default!;
[Dependency] private readonly MovementModStatusSystem _movementMod = default!;
public override void Initialize()
{
@@ -22,18 +21,19 @@ namespace Content.Server.Stunnable
private void TryDoCollideStun(EntityUid uid, StunOnCollideComponent component, EntityUid target)
{
_stunSystem.TryUpdateStunDuration(target, component.StunAmount);
if (TryComp<StatusEffectsComponent>(target, out var status))
{
_stunSystem.TryStun(target, TimeSpan.FromSeconds(component.StunAmount), true, status);
_stunSystem.TryKnockdown(target, component.KnockdownAmount, component.Refresh, component.AutoStand, force: true);
_stunSystem.TryKnockdown(target, TimeSpan.FromSeconds(component.KnockdownAmount), true,
status);
_stunSystem.TrySlowdown(target, TimeSpan.FromSeconds(component.SlowdownAmount), true,
component.WalkSpeedMultiplier, component.RunSpeedMultiplier, status);
}
_movementMod.TryUpdateMovementSpeedModDuration(
target,
MovementModStatusSystem.TaserSlowdown,
component.SlowdownAmount,
component.WalkSpeedModifier,
component.SprintSpeedModifier
);
}
private void HandleCollide(EntityUid uid, StunOnCollideComponent component, ref StartCollideEvent args)
{
if (args.OurFixtureId != component.FixtureID)

View File

@@ -1,7 +0,0 @@
using Content.Shared.Stunnable;
namespace Content.Server.Stunnable
{
public sealed class StunSystem : SharedStunSystem
{}
}

View File

@@ -1,6 +1,8 @@
using Content.Server.Administration.Logs;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Emp;
using Content.Shared.ActionBlocker;
using Content.Shared.Database;
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Events;
using Content.Shared.Power;
@@ -21,6 +23,8 @@ public sealed class SurveillanceCameraSystem : EntitySystem
[Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
[Dependency] private readonly UserInterfaceSystem _userInterface = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
// Pings a surveillance camera subnet. All cameras will always respond
// with a data message if they are on the same subnet.
@@ -170,6 +174,7 @@ public sealed class SurveillanceCameraSystem : EntitySystem
component.CameraId = args.Name;
component.NameSet = true;
UpdateSetupInterface(uid, component);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"{ToPrettyString(args.Actor)} set the name of {ToPrettyString(uid)} to \"{args.Name}.\"");
}
private void OnSetNetwork(EntityUid uid, SurveillanceCameraComponent component,

View File

@@ -68,9 +68,13 @@ public sealed class TipsSystem : EntitySystem
{
return args.Length switch
{
1 => CompletionResult.FromHintOptions(CompletionHelper.SessionNames(), Loc.GetString("cmd-tippy-auto-1")),
1 => CompletionResult.FromHintOptions(
CompletionHelper.SessionNames(players: _playerManager),
Loc.GetString("cmd-tippy-auto-1")),
2 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-2")),
3 => CompletionResult.FromHintOptions(CompletionHelper.PrototypeIDs<EntityPrototype>(), Loc.GetString("cmd-tippy-auto-3")),
3 => CompletionResult.FromHintOptions(
CompletionHelper.PrototypeIdsLimited<EntityPrototype>(args[2], _prototype),
Loc.GetString("cmd-tippy-auto-3")),
4 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-4")),
5 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-5")),
6 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-6")),

View File

@@ -1,47 +1,5 @@
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.FixedPoint;
using Content.Shared.Tools.Components;
using SharedToolSystem = Content.Shared.Tools.Systems.SharedToolSystem;
using Content.Shared.Tools.Systems;
namespace Content.Server.Tools;
public sealed class ToolSystem : SharedToolSystem
{
public override void Update(float frameTime)
{
base.Update(frameTime);
UpdateWelders(frameTime);
}
//todo move to shared once you can remove reagents from shared without it freaking out.
private void UpdateWelders(float frameTime)
{
var query = EntityQueryEnumerator<WelderComponent, SolutionContainerManagerComponent>();
while (query.MoveNext(out var uid, out var welder, out var solutionContainer))
{
if (!welder.Enabled)
continue;
welder.WelderTimer += frameTime;
if (welder.WelderTimer < welder.WelderUpdateTimer)
continue;
if (!SolutionContainerSystem.TryGetSolution((uid, solutionContainer), welder.FuelSolutionName, out var solutionComp, out var solution))
continue;
SolutionContainerSystem.RemoveReagent(solutionComp.Value, welder.FuelReagent, welder.FuelConsumption * welder.WelderTimer);
if (solution.GetTotalPrototypeQuantity(welder.FuelReagent) <= FixedPoint2.Zero)
{
ItemToggle.Toggle(uid, predicted: false);
}
Dirty(uid, welder);
welder.WelderTimer -= welder.WelderUpdateTimer;
}
}
}
public sealed class ToolSystem : SharedToolSystem;

View File

@@ -114,12 +114,12 @@ namespace Content.Server.Voting
{
var ties = string.Join(", ", eventArgs.Winners.Select(c => args[(int) c]));
_adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Custom vote {options.Title} finished as tie: {ties}");
_chatManager.DispatchServerAnnouncement(Loc.GetString("cmd-customvote-on-finished-tie", ("ties", ties)));
_chatManager.DispatchServerAnnouncement(Loc.GetString("cmd-customvote-on-finished-tie", ("title", options.Title), ("ties", ties)));
}
else
{
_adminLogger.Add(LogType.Vote, LogImpact.Medium, $"Custom vote {options.Title} finished: {args[(int) eventArgs.Winner]}");
_chatManager.DispatchServerAnnouncement(Loc.GetString("cmd-customvote-on-finished-win", ("winner", args[(int) eventArgs.Winner])));
_chatManager.DispatchServerAnnouncement(Loc.GetString("cmd-customvote-on-finished-win", ("title", options.Title), ("winner", args[(int) eventArgs.Winner])));
}
_voteWebhooks.UpdateWebhookIfConfigured(webhookState, eventArgs);

View File

@@ -7,7 +7,6 @@ using Content.Server.Humanoid;
using Content.Server.IdentityManagement;
using Content.Server.Inventory;
using Content.Server.Mind;
using Content.Server.Mind.Commands;
using Content.Server.NPC;
using Content.Server.NPC.HTN;
using Content.Server.NPC.Systems;
@@ -215,7 +214,7 @@ public sealed partial class ZombieSystem
_popup.PopupEntity(Loc.GetString("zombie-transform", ("target", target)), target, PopupType.LargeCaution);
//Make it sentient if it's an animal or something
MakeSentientCommand.MakeSentient(target, EntityManager);
_mind.MakeSentient(target);
//Make the zombie not die in the cold. Good for space zombies
if (TryComp<TemperatureComponent>(target, out var tempComp))

View File

@@ -101,7 +101,7 @@ public sealed class CP14BloodMoonCurseRule : GameRuleSystem<CP14BloodMoonCurseRu
if (!Resolve(ent.Owner, ref ent.Comp, false))
return;
_stun.TryParalyze(ent, ent.Comp.EndStunDuration, true);
_stun.TryUpdateParalyzeDuration(ent, ent.Comp.EndStunDuration);
_popup.PopupEntity(Loc.GetString("cp14-bloodmoon-curse-removed"), ent, PopupType.SmallCaution);
if (TryComp<CP14BloodMoonCurseComponent>(ent, out var curseComp))
{