Optimizations from server profile (#38290)

* Properly cache regexes in chat sanitization/accents

Wow I wonder if `new Regex()` has a cost to it *looks at server profile*.

* Avoid lag caused by Tippy command completions

CompletionHelper.PrototypeIDs explicitly says *not* to use it with EntityPrototype. Unsurprisingly, reporting a completion result for every entity prototype in the game is a *bad idea*.

* Add active count metrics to some high-load systems

Mover & NPCs

I suspect the thing that caused the Leviathan round to shit itself on performance is NPC spam in space or something. So let's verify that.

* Enable parallel processing on pow3r again

Originally disabled due to a theory of it causing bugs, it was re-enabled on Vulture, and I'm not aware of it having caused any issues there.

* Replace hashset with bitflags for AtmosMonitor alert types.

Allocating these hashsets was like 20% of the CPU of atmos, somehow.

* Cache HashSet used for space movement collider checks

Turns out this was a ton of server allocations. Huh.
This commit is contained in:
Pieter-Jan Briers
2025-07-26 11:44:34 +02:00
committed by GitHub
parent d0c104e4b0
commit 444180c20d
14 changed files with 217 additions and 128 deletions

View File

@@ -48,7 +48,7 @@ public sealed partial class AtmosAlarmableComponent : Component
public HashSet<string> SyncWithTags { get; private set; } = new(); public HashSet<string> SyncWithTags { get; private set; } = new();
[DataField("monitorAlertTypes")] [DataField("monitorAlertTypes")]
public HashSet<AtmosMonitorThresholdType>? MonitorAlertTypes { get; private set; } public AtmosMonitorThresholdTypeFlags MonitorAlertTypes { get; private set; }
/// <summary> /// <summary>
/// If this device should receive only. If it can only /// 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; public AtmosAlarmType LastAlarmState = AtmosAlarmType.Normal;
[DataField("trippedThresholds")] [DataField("trippedThresholds")]
public HashSet<AtmosMonitorThresholdType> TrippedThresholds = new(); public AtmosMonitorThresholdTypeFlags TrippedThresholds;
/// <summary> /// <summary>
/// Registered devices in this atmos monitor. Alerts will be sent directly /// Registered devices in this atmos monitor. Alerts will be sent directly

View File

@@ -108,9 +108,9 @@ public sealed class AtmosAlarmableSystem : EntitySystem
break; 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)) if (!component.NetworkAlarmStates.ContainsKey(args.SenderAddress))

View File

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

View File

@@ -12,86 +12,86 @@ namespace Content.Server.Chat.Managers;
/// </summary> /// </summary>
public sealed class ChatSanitizationManager : IChatSanitizationManager public sealed class ChatSanitizationManager : IChatSanitizationManager
{ {
private static readonly Dictionary<string, string> ShorthandToEmote = new() private static readonly (Regex regex, string emoteKey)[] ShorthandToEmote =
{ [
{ ":)", "chatsan-smiles" }, Entry(":)", "chatsan-smiles"),
{ ":]", "chatsan-smiles" }, Entry(":]", "chatsan-smiles"),
{ "=)", "chatsan-smiles" }, Entry("=)", "chatsan-smiles"),
{ "=]", "chatsan-smiles" }, Entry("=]", "chatsan-smiles"),
{ "(:", "chatsan-smiles" }, Entry("(:", "chatsan-smiles"),
{ "[:", "chatsan-smiles" }, Entry("[:", "chatsan-smiles"),
{ "(=", "chatsan-smiles" }, Entry("(=", "chatsan-smiles"),
{ "[=", "chatsan-smiles" }, Entry("[=", "chatsan-smiles"),
{ "^^", "chatsan-smiles" }, Entry("^^", "chatsan-smiles"),
{ "^-^", "chatsan-smiles" }, Entry("^-^", "chatsan-smiles"),
{ ":(", "chatsan-frowns" }, Entry(":(", "chatsan-frowns"),
{ ":[", "chatsan-frowns" }, Entry(":[", "chatsan-frowns"),
{ "=(", "chatsan-frowns" }, Entry("=(", "chatsan-frowns"),
{ "=[", "chatsan-frowns" }, Entry("=[", "chatsan-frowns"),
{ "):", "chatsan-frowns" }, Entry("):", "chatsan-frowns"),
{ ")=", "chatsan-frowns" }, Entry(")=", "chatsan-frowns"),
{ "]:", "chatsan-frowns" }, Entry("]:", "chatsan-frowns"),
{ "]=", "chatsan-frowns" }, Entry("]=", "chatsan-frowns"),
{ ":D", "chatsan-smiles-widely" }, Entry(":D", "chatsan-smiles-widely"),
{ "D:", "chatsan-frowns-deeply" }, Entry("D:", "chatsan-frowns-deeply"),
{ ":O", "chatsan-surprised" }, Entry(":O", "chatsan-surprised"),
{ ":3", "chatsan-smiles" }, Entry(":3", "chatsan-smiles"),
{ ":S", "chatsan-uncertain" }, Entry(":S", "chatsan-uncertain"),
{ ":>", "chatsan-grins" }, Entry(":>", "chatsan-grins"),
{ ":<", "chatsan-pouts" }, Entry(":<", "chatsan-pouts"),
{ "xD", "chatsan-laughs" }, Entry("xD", "chatsan-laughs"),
{ ":'(", "chatsan-cries" }, Entry(":'(", "chatsan-cries"),
{ ":'[", "chatsan-cries" }, Entry(":'[", "chatsan-cries"),
{ "='(", "chatsan-cries" }, Entry("='(", "chatsan-cries"),
{ "='[", "chatsan-cries" }, Entry("='[", "chatsan-cries"),
{ ")':", "chatsan-cries" }, Entry(")':", "chatsan-cries"),
{ "]':", "chatsan-cries" }, Entry("]':", "chatsan-cries"),
{ ")'=", "chatsan-cries" }, Entry(")'=", "chatsan-cries"),
{ "]'=", "chatsan-cries" }, Entry("]'=", "chatsan-cries"),
{ ";-;", "chatsan-cries" }, Entry(";-;", "chatsan-cries"),
{ ";_;", "chatsan-cries" }, Entry(";_;", "chatsan-cries"),
{ "qwq", "chatsan-cries" }, Entry("qwq", "chatsan-cries"),
{ ":u", "chatsan-smiles-smugly" }, Entry(":u", "chatsan-smiles-smugly"),
{ ":v", "chatsan-smiles-smugly" }, Entry(":v", "chatsan-smiles-smugly"),
{ ">:i", "chatsan-annoyed" }, Entry(">:i", "chatsan-annoyed"),
{ ":i", "chatsan-sighs" }, Entry(":i", "chatsan-sighs"),
{ ":|", "chatsan-sighs" }, Entry(":|", "chatsan-sighs"),
{ ":p", "chatsan-stick-out-tongue" }, Entry(":p", "chatsan-stick-out-tongue"),
{ ";p", "chatsan-stick-out-tongue" }, Entry(";p", "chatsan-stick-out-tongue"),
{ ":b", "chatsan-stick-out-tongue" }, Entry(":b", "chatsan-stick-out-tongue"),
{ "0-0", "chatsan-wide-eyed" }, Entry("0-0", "chatsan-wide-eyed"),
{ "o-o", "chatsan-wide-eyed" }, Entry("o-o", "chatsan-wide-eyed"),
{ "o.o", "chatsan-wide-eyed" }, Entry("o.o", "chatsan-wide-eyed"),
{ "._.", "chatsan-surprised" }, Entry("._.", "chatsan-surprised"),
{ ".-.", "chatsan-confused" }, Entry(".-.", "chatsan-confused"),
{ "-_-", "chatsan-unimpressed" }, Entry("-_-", "chatsan-unimpressed"),
{ "smh", "chatsan-unimpressed" }, Entry("smh", "chatsan-unimpressed"),
{ "o/", "chatsan-waves" }, Entry("o/", "chatsan-waves"),
{ "^^/", "chatsan-waves" }, Entry("^^/", "chatsan-waves"),
{ ":/", "chatsan-uncertain" }, Entry(":/", "chatsan-uncertain"),
{ ":\\", "chatsan-uncertain" }, Entry(":\\", "chatsan-uncertain"),
{ "lmao", "chatsan-laughs" }, Entry("lmao", "chatsan-laughs"),
{ "lmfao", "chatsan-laughs" }, Entry("lmfao", "chatsan-laughs"),
{ "lol", "chatsan-laughs" }, Entry("lol", "chatsan-laughs"),
{ "lel", "chatsan-laughs" }, Entry("lel", "chatsan-laughs"),
{ "kek", "chatsan-laughs" }, Entry("kek", "chatsan-laughs"),
{ "rofl", "chatsan-laughs" }, Entry("rofl", "chatsan-laughs"),
{ "o7", "chatsan-salutes" }, Entry("o7", "chatsan-salutes"),
{ ";_;7", "chatsan-tearfully-salutes" }, Entry(";_;7", "chatsan-tearfully-salutes"),
{ "idk", "chatsan-shrugs" }, Entry("idk", "chatsan-shrugs"),
{ ";)", "chatsan-winks" }, Entry(";)", "chatsan-winks"),
{ ";]", "chatsan-winks" }, Entry(";]", "chatsan-winks"),
{ "(;", "chatsan-winks" }, Entry("(;", "chatsan-winks"),
{ "[;", "chatsan-winks" }, Entry("[;", "chatsan-winks"),
{ ":')", "chatsan-tearfully-smiles" }, Entry(":')", "chatsan-tearfully-smiles"),
{ ":']", "chatsan-tearfully-smiles" }, Entry(":']", "chatsan-tearfully-smiles"),
{ "=')", "chatsan-tearfully-smiles" }, Entry("=')", "chatsan-tearfully-smiles"),
{ "=']", "chatsan-tearfully-smiles" }, Entry("=']", "chatsan-tearfully-smiles"),
{ "(':", "chatsan-tearfully-smiles" }, Entry("(':", "chatsan-tearfully-smiles"),
{ "[':", "chatsan-tearfully-smiles" }, Entry("[':", "chatsan-tearfully-smiles"),
{ "('=", "chatsan-tearfully-smiles" }, Entry("('=", "chatsan-tearfully-smiles"),
{ "['=", "chatsan-tearfully-smiles" } Entry("['=", "chatsan-tearfully-smiles"),
}; ];
[Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly ILocalizationManager _loc = default!; [Dependency] private readonly ILocalizationManager _loc = default!;
@@ -125,21 +125,8 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
// -1 is just a canary for nothing found yet // -1 is just a canary for nothing found yet
var lastEmoteIndex = -1; 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 // We're using sanitized as the original message until the end so that we can make sure the indices of
// the emotes are accurate. // the emotes are accurate.
var lastMatch = r.Match(sanitized); var lastMatch = r.Match(sanitized);
@@ -159,4 +146,21 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
sanitized = message.Trim(); sanitized = message.Trim();
return emote is not null; 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

@@ -30,11 +30,16 @@ using Robust.Shared.Timing;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using Content.Shared.Prying.Systems; using Content.Shared.Prying.Systems;
using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.ObjectPool;
using Prometheus;
namespace Content.Server.NPC.Systems; namespace Content.Server.NPC.Systems;
public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem 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. * 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. * 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 object _obstacles = new();
private int _activeSteeringCount;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
@@ -244,12 +251,15 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
}; };
var curTime = _timing.CurTime; var curTime = _timing.CurTime;
_activeSteeringCount = 0;
Parallel.For(0, index, options, i => Parallel.For(0, index, options, i =>
{ {
var (uid, steering, mover, xform) = npcs[i]; var (uid, steering, mover, xform) = npcs[i];
Steer(uid, steering, mover, xform, frameTime, curTime); Steer(uid, steering, mover, xform, frameTime, curTime);
}); });
ActiveSteeringGauge.Set(_activeSteeringCount);
if (_subscribedSessions.Count > 0) if (_subscribedSessions.Count > 0)
{ {
@@ -324,6 +334,8 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
return; return;
} }
Interlocked.Increment(ref _activeSteeringCount);
var agentRadius = steering.Radius; var agentRadius = steering.Radius;
var worldPos = _transform.GetWorldPosition(xform); var worldPos = _transform.GetWorldPosition(xform);
var (layer, mask) = _physics.GetHardCollision(uid); var (layer, mask) = _physics.GetHardCollision(uid);

View File

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

View File

@@ -7,6 +7,7 @@ using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems; using Content.Shared.Movement.Systems;
using Content.Shared.Shuttles.Components; using Content.Shared.Shuttles.Components;
using Content.Shared.Shuttles.Systems; using Content.Shared.Shuttles.Systems;
using Prometheus;
using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Components;
using Robust.Shared.Player; using Robust.Shared.Player;
using DroneConsoleComponent = Content.Server.Shuttles.DroneConsoleComponent; using DroneConsoleComponent = Content.Server.Shuttles.DroneConsoleComponent;
@@ -17,6 +18,10 @@ namespace Content.Server.Physics.Controllers;
public sealed class MoverController : SharedMoverController 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 ThrusterSystem _thruster = default!;
[Dependency] private readonly SharedTransformSystem _xformSystem = default!; [Dependency] private readonly SharedTransformSystem _xformSystem = default!;
@@ -97,6 +102,8 @@ public sealed class MoverController : SharedMoverController
HandleMobMovement(mover, frameTime); HandleMobMovement(mover, frameTime);
} }
ActiveMoverGauge.Set(_movers.Count);
HandleShuttleMovement(frameTime); HandleShuttleMovement(frameTime);
} }

View File

@@ -19,9 +19,21 @@ namespace Content.Server.Speech.EntitySystems
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ILocalizationManager _loc = default!; [Dependency] private readonly ILocalizationManager _loc = default!;
private readonly Dictionary<ProtoId<ReplacementAccentPrototype>, (Regex regex, string replacement)[]>
_cachedReplacements = new();
public override void Initialize() public override void Initialize()
{ {
SubscribeLocalEvent<ReplacementAccentComponent, AccentGetEvent>(OnAccent); 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) 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)) : ""; return prototype.FullReplacements.Length != 0 ? Loc.GetString(_random.Pick(prototype.FullReplacements)) : "";
} }
if (prototype.WordReplacements == null)
return message;
// Prohibition of repeated word replacements. // Prohibition of repeated word replacements.
// All replaced words placed in the final message are placed here as dashes (___) with the same length. // 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, // 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. // ensuring that the replaced words cannot be replaced again.
var maskMessage = message; 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 // this is kind of slow but its not that bad
// essentially: go over all matches, try to match capitalization where possible, then replace // essentially: go over all matches, try to match capitalization where possible, then replace
// rather than using regex.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 // fetch the match again as the character indices may have changed
Match match = Regex.Match(maskMessage, $@"(?<!\w){f}(?!\w)", RegexOptions.IgnoreCase); Match match = regex.Match(maskMessage);
var replacement = r; var replacement = replace;
// Intelligently replace capitalization // Intelligently replace capitalization
// two cases where we will do so: // two cases where we will do so:
@@ -98,5 +105,40 @@ namespace Content.Server.Speech.EntitySystems
return message; 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

@@ -68,9 +68,13 @@ public sealed class TipsSystem : EntitySystem
{ {
return args.Length switch 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")), 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")), 4 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-4")),
5 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-5")), 5 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-5")),
6 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-6")), 6 => CompletionResult.FromHint(Loc.GetString("cmd-tippy-auto-6")),

View File

@@ -388,9 +388,21 @@ public enum AtmosMonitorLimitType //<todo.eoin Very similar to the above...
// fields you can find this prototype in // fields you can find this prototype in
public enum AtmosMonitorThresholdType public enum AtmosMonitorThresholdType
{ {
Temperature, Temperature = 0,
Pressure, Pressure = 1,
Gas Gas = 2
}
/// <summary>
/// Bitflags version of <see cref="AtmosMonitorThresholdType"/>
/// </summary>
[Flags]
public enum AtmosMonitorThresholdTypeFlags
{
None = 0,
Temperature = 1 << 0,
Pressure = 1 << 1,
Gas = 1 << 2,
} }
[Serializable, NetSerializable] [Serializable, NetSerializable]

View File

@@ -36,5 +36,5 @@ public sealed partial class CCVars : CVars
/// Set to true to disable parallel processing in the pow3r solver. /// Set to true to disable parallel processing in the pow3r solver.
/// </summary> /// </summary>
public static readonly CVarDef<bool> DebugPow3rDisableParallel = public static readonly CVarDef<bool> DebugPow3rDisableParallel =
CVarDef.Create("debug.pow3r_disable_parallel", true, CVar.SERVERONLY); CVarDef.Create("debug.pow3r_disable_parallel", false, CVar.SERVERONLY);
} }

View File

@@ -72,6 +72,8 @@ public abstract partial class SharedMoverController : VirtualController
/// </summary> /// </summary>
public Dictionary<EntityUid, bool> UsedMobMovement = new(); public Dictionary<EntityUid, bool> UsedMobMovement = new();
private readonly HashSet<EntityUid> _aroundColliderSet = [];
public override void Initialize() public override void Initialize()
{ {
UpdatesBefore.Add(typeof(TileFrictionController)); UpdatesBefore.Add(typeof(TileFrictionController));
@@ -454,7 +456,9 @@ public abstract partial class SharedMoverController : VirtualController
var (uid, collider, mover, transform) = entity; var (uid, collider, mover, transform) = entity;
var enlargedAABB = _lookup.GetWorldAABB(entity.Owner, transform).Enlarged(mover.GrabRange); var enlargedAABB = _lookup.GetWorldAABB(entity.Owner, transform).Enlarged(mover.GrabRange);
foreach (var otherEntity in lookupSystem.GetEntitiesIntersecting(transform.MapID, enlargedAABB)) _aroundColliderSet.Clear();
lookupSystem.GetEntitiesIntersecting(transform.MapID, enlargedAABB, _aroundColliderSet);
foreach (var otherEntity in _aroundColliderSet)
{ {
if (otherEntity == uid) if (otherEntity == uid)
continue; // Don't try to push off of yourself! continue; // Don't try to push off of yourself!

View File

@@ -14,6 +14,3 @@ force_client_hud_version_watermark = true
[chat] [chat]
motd = "\n########################################################\n\n[font size=17]This is a test server. You can play with the newest changes to the game, but these [color=red]changes may not be final or stable[/color], and may be reverted. Please report bugs via our GitHub, forum, or community Discord.[/font]\n\n########################################################\n" motd = "\n########################################################\n\n[font size=17]This is a test server. You can play with the newest changes to the game, but these [color=red]changes may not be final or stable[/color], and may be reverted. Please report bugs via our GitHub, forum, or community Discord.[/font]\n\n########################################################\n"
[debug]
pow3r_disable_parallel = false