]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Optimizations from server profile (#38290)
authorPieter-Jan Briers <pieterjan.briers+git@gmail.com>
Sat, 26 Jul 2025 09:44:34 +0000 (11:44 +0200)
committerGitHub <noreply@github.com>
Sat, 26 Jul 2025 09:44:34 +0000 (11:44 +0200)
* 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.

14 files changed:
Content.Server/Atmos/Monitor/Components/AtmosAlarmableComponent.cs
Content.Server/Atmos/Monitor/Components/AtmosMonitorComponent.cs
Content.Server/Atmos/Monitor/Systems/AtmosAlarmableSystem.cs
Content.Server/Atmos/Monitor/Systems/AtmosMonitoringSystem.cs
Content.Server/Chat/Managers/ChatSanitizationManager.cs
Content.Server/NPC/Systems/NPCSteeringSystem.cs
Content.Server/NPC/Systems/NPCSystem.cs
Content.Server/Physics/Controllers/MoverController.cs
Content.Server/Speech/EntitySystems/ReplacementAccentSystem.cs
Content.Server/Tips/TipsSystem.cs
Content.Shared/Atmos/Monitor/AtmosAlarmThreshold.cs
Content.Shared/CCVar/CCVars.cs
Content.Shared/Movement/Systems/SharedMoverController.cs
Resources/ConfigPresets/WizardsDen/vulture.toml

index e291334ad0f9b47d6d6750c0e2da5aded17ab386..cc53df2ecdee17cdd5e018c149824b34e77c4272 100644 (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
index 830479561dea0aa8061e82f738572c896fdc298e..ffb1fe0d2777f9dedd2f12eeed6fc06647cb5eff 100644 (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
index b6bc4bd303389aaa8cf63975e141bc829d8c9ed0..2dcba3f464f20b5e21e19e09ac9e0615efd6c403 100644 (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))
index 520afe0c58622cf04ddc78f630d7044e5e1107c8..452b30033145a8481401ecf9ce8f181988a11466 100644 (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;
index 0c78e45f86eda146235ffbd714b534f9ec1b512d..106e5313e6ed65c2c7f9e410d66448ad034a7786 100644 (file)
@@ -12,86 +12,86 @@ namespace Content.Server.Chat.Managers;
 /// </summary>
 public sealed class ChatSanitizationManager : IChatSanitizationManager
 {
-    private static readonly Dictionary<string, string> ShorthandToEmote = new()
-    {
-        { ":)", "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" }
-    };
+    private static readonly (Regex regex, string emoteKey)[] ShorthandToEmote =
+    [
+        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!;
@@ -125,21 +125,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);
@@ -159,4 +146,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);
+    }
 }
index 6a736f3bc997ede60a349384fa632dd88067fe30..3585711860b638e173e1e005c1378994425bba1d 100644 (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);
index c7690cb295b134c4e142fddc4a1d3f04dfef8170..27b2a1691d62b682550b1796659a150fe9468b99 100644 (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)
index f0a723f3c0896de1fd5fbecac3fa82e023bcff88..5c87de186386de721d1ef0b4fecb8ca7f57146a6 100644 (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);
     }
 
index 5b215e9bea8ab9d2b08de536fdb54307ea68b0fd..c285063d2d91fcc165b4ac47a19d8f04f66347d5 100644 (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();
+        }
     }
 }
index effbb7ba87e9290663da6bc9c6115d0d7b7996f7..cd7f7b52f548f25c5813b58843c964e9e6740dc0 100644 (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")),
index 89d0bf239292786b3c8a7e63a1540e0e237fe71c..becc5378f26673f86b6f529272d06561bc34b8c0 100644 (file)
@@ -388,9 +388,21 @@ public enum AtmosMonitorLimitType //<todo.eoin Very similar to the above...
 // fields you can find this prototype in
 public enum AtmosMonitorThresholdType
 {
-    Temperature,
-    Pressure,
-    Gas
+    Temperature = 0,
+    Pressure = 1,
+    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]
index d68ab168743666bac9b7fd2a83db2d73e4a007e9..87b2da129aaf4b70ae7bab104fd77e4cfbf81d20 100644 (file)
@@ -36,5 +36,5 @@ public sealed partial class CCVars : CVars
     /// Set to true to disable parallel processing in the pow3r solver.
     /// </summary>
     public static readonly CVarDef<bool> DebugPow3rDisableParallel =
-        CVarDef.Create("debug.pow3r_disable_parallel", true, CVar.SERVERONLY);
+        CVarDef.Create("debug.pow3r_disable_parallel", false, CVar.SERVERONLY);
 }
index e43800dc9f399269b49718406647ffc07655a028..f8495fcd18aba89c2987ca1129430c4b2ed1d404 100644 (file)
@@ -72,6 +72,8 @@ public abstract partial class SharedMoverController : VirtualController
     /// </summary>
     public Dictionary<EntityUid, bool> UsedMobMovement = new();
 
+    private readonly HashSet<EntityUid> _aroundColliderSet = [];
+
     public override void Initialize()
     {
         UpdatesBefore.Add(typeof(TileFrictionController));
@@ -454,7 +456,9 @@ public abstract partial class SharedMoverController : VirtualController
         var (uid, collider, mover, transform) = entity;
         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)
                 continue; // Don't try to push off of yourself!
index 0e89f63741132f2297d0ffe521e81e8dcb103912..8eccfa48e19f4cc12c0d81de4f743a110d9b1a45 100644 (file)
@@ -14,6 +14,3 @@ force_client_hud_version_watermark = true
 
 [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"
-
-[debug]
-pow3r_disable_parallel = false