From 30b1f8eb1dd20b4cdae17f5037789a92f3548865 Mon Sep 17 00:00:00 2001 From: MaxSMokeSkaarj Date: Fri, 10 Oct 2025 22:25:26 +1000 Subject: [PATCH] Reapply "Merge branch 'master' of ssh://github/space-wizards/space-station-14" This reverts commit bf4ee1f7035efd49efc041b63054c503cee9662d. --- .../Tests/Helpers/TestListenerComponent.cs | 13 ++ .../Tests/Helpers/TestListenerSystem.cs | 45 ++++++ .../Interaction/InteractionTest.Helpers.cs | 138 ++++++++++++++++++ .../Tests/Movement/SlippingTest.cs | 32 ++-- Content.Shared/Armor/SharedArmorSystem.cs | 12 +- Content.Shared/Bed/Sleep/SleepingSystem.cs | 38 ++--- .../Blinding/Systems/EyeProtectionSystem.cs | 4 + Content.Shared/Flash/SharedFlashSystem.cs | 4 + .../Systems/ParcelWrappingSystem.cs | 3 +- .../Entities/Objects/Misc/parcel_wrap.yml | 11 ++ .../Entities/Structures/Machines/lathe.yml | 7 + Resources/Prototypes/Reagents/narcotics.yml | 11 +- .../Recipes/Lathes/Packs/security.yml | 3 - .../Recipes/Lathes/Packs/syndicate.yml | 9 ++ .../ServerRules/SpaceLaw/SpaceLaw.xml | 6 +- 15 files changed, 283 insertions(+), 53 deletions(-) create mode 100644 Content.IntegrationTests/Tests/Helpers/TestListenerComponent.cs create mode 100644 Content.IntegrationTests/Tests/Helpers/TestListenerSystem.cs create mode 100644 Resources/Prototypes/Recipes/Lathes/Packs/syndicate.yml diff --git a/Content.IntegrationTests/Tests/Helpers/TestListenerComponent.cs b/Content.IntegrationTests/Tests/Helpers/TestListenerComponent.cs new file mode 100644 index 0000000000..817558b426 --- /dev/null +++ b/Content.IntegrationTests/Tests/Helpers/TestListenerComponent.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Robust.Shared.GameObjects; + +namespace Content.IntegrationTests.Tests.Helpers; + +/// +/// Component that is used by to store any information about received events. +/// +[RegisterComponent] +public sealed partial class TestListenerComponent : Component +{ + public Dictionary> Events = new(); +} diff --git a/Content.IntegrationTests/Tests/Helpers/TestListenerSystem.cs b/Content.IntegrationTests/Tests/Helpers/TestListenerSystem.cs new file mode 100644 index 0000000000..2481cef03f --- /dev/null +++ b/Content.IntegrationTests/Tests/Helpers/TestListenerSystem.cs @@ -0,0 +1,45 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using Robust.Shared.GameObjects; +using Robust.Shared.Utility; + +namespace Content.IntegrationTests.Tests.Helpers; + +/// +/// Generic system that listens for and records any received events of a given type. +/// +public abstract class TestListenerSystem : EntitySystem where TEvent : notnull +{ + public override void Initialize() + { + // TODO + // supporting broadcast events requires cleanup on test finish, which will probably require changes to the + // test pair/pool manager and would conflict with #36797 + SubscribeLocalEvent(OnDirectedEvent); + } + + protected virtual void OnDirectedEvent(Entity ent, ref TEvent args) + { + ent.Comp.Events.GetOrNew(args.GetType()).Add(args); + } + + public int Count(EntityUid uid, Func? predicate = null) + { + return GetEvents(uid, predicate).Count(); + } + + public void Clear(EntityUid uid) + { + CompOrNull(uid)?.Events.Remove(typeof(TEvent)); + } + + public IEnumerable GetEvents(EntityUid uid, Func? predicate = null) + { + var events = CompOrNull(uid)?.Events.GetValueOrDefault(typeof(TEvent)); + if (events == null) + return []; + + return events.Cast().Where(e => predicate?.Invoke(e) ?? true); + } +} diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs index fa16730dd5..d04ed4cb3c 100644 --- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs +++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Numerics; using System.Reflection; using Content.Client.Construction; +using Content.IntegrationTests.Tests.Helpers; using Content.Server.Atmos.EntitySystems; using Content.Server.Construction.Components; using Content.Server.Gravity; @@ -22,6 +23,8 @@ using Robust.Shared.Input; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Maths; +using Robust.Shared.Reflection; +using Robust.UnitTesting; using ItemToggleComponent = Content.Shared.Item.ItemToggle.Components.ItemToggleComponent; namespace Content.IntegrationTests.Tests.Interaction; @@ -29,6 +32,8 @@ namespace Content.IntegrationTests.Tests.Interaction; // This partial class defines various methods that are useful for performing & validating interactions public abstract partial class InteractionTest { + private Dictionary _listenerCache = new(); + /// /// Begin constructing an entity. /// @@ -758,6 +763,139 @@ public abstract partial class InteractionTest #endregion + #region EventListener + + /// + /// Asserts that running the given action causes an event to be fired directed at the specified entity (defaults to ). + /// + /// + /// This currently only checks server-side events. + /// + /// The entity at which the events are supposed to be directed + /// How many new events are expected + /// Whether to clear all previously recorded events before invoking the delegate + protected async Task AssertFiresEvent(Func act, EntityUid? uid = null, int count = 1, bool clear = true) + where TEvent : notnull + { + var sys = GetListenerSystem(); + + uid ??= STarget; + if (uid == null) + { + Assert.Fail("No target specified"); + return; + } + + if (clear) + sys.Clear(uid.Value); + else + count += sys.Count(uid.Value); + + await Server.WaitPost(() => SEntMan.EnsureComponent(uid.Value)); + await act(); + AssertEvent(uid, count: count); + } + + /// + /// This is a variant of that passes the delegate to . + /// + /// + /// This currently only checks for server-side events. + /// + /// The entity at which the events are supposed to be directed + /// How many new events are expected + /// Whether to clear all previously recorded events before invoking the delegate + protected async Task AssertPostFiresEvent(Action act, EntityUid? uid = null, int count = 1, bool clear = true) + where TEvent : notnull + { + await AssertFiresEvent(async () => await Server.WaitPost(act), uid, count, clear); + } + + /// + /// This is a variant of that passes the delegate to . + /// + /// + /// This currently only checks for server-side events. + /// + /// The entity at which the events are supposed to be directed + /// How many new events are expected + /// Whether to clear all previously recorded events before invoking the delegate + protected async Task AssertAssertionFiresEvent(Action act, + EntityUid? uid = null, + int count = 1, + bool clear = true) + where TEvent : notnull + { + await AssertFiresEvent(async () => await Server.WaitAssertion(act), uid, count, clear); + } + + /// + /// Asserts that the specified event has been fired some number of times at the given entity (defaults to ). + /// For this to work, this requires that the entity has been given a + /// + /// + /// This currently only checks server-side events. + /// + /// The entity at which the events were directed + /// How many new events are expected + /// A predicate that can be used to filter the recorded events + protected void AssertEvent(EntityUid? uid = null, int count = 1, Func? predicate = null) + where TEvent : notnull + { + Assert.That(GetEvents(uid, predicate).Count, Is.EqualTo(count)); + } + + /// + /// Gets all the events of the specified type that have been fired at the given entity (defaults to ). + /// For this to work, this requires that the entity has been given a + /// + /// + /// This currently only gets for server-side events. + /// + /// The entity at which the events were directed + /// A predicate that can be used to filter the returned events + protected IEnumerable GetEvents(EntityUid? uid = null, Func? predicate = null) + where TEvent : notnull + { + uid ??= STarget; + if (uid == null) + { + Assert.Fail("No target specified"); + return []; + } + + Assert.That(SEntMan.HasComponent(uid), $"Entity must have {nameof(TestListenerComponent)}"); + return GetListenerSystem().GetEvents(uid.Value, predicate); + } + + protected TestListenerSystem GetListenerSystem() + where TEvent : notnull + { + if (_listenerCache.TryGetValue(typeof(TEvent), out var listener)) + return (TestListenerSystem) listener; + + var type = Server.Resolve().GetAllChildren>().Single(); + if (!SEntMan.EntitySysManager.TryGetEntitySystem(type, out var systemObj)) + { + // There has to be a listener system that is manually defined. Event subscriptions are locked once + // finalized, so we can't really easily create new subscriptions on the fly. + // TODO find a better solution + throw new InvalidOperationException($"Event {typeof(TEvent).Name} has no associated listener system!"); + } + + var system = (TestListenerSystem)systemObj; + _listenerCache[typeof(TEvent)] = system; + return system; + } + + /// + /// Clears all recorded events of the given type. + /// + protected void ClearEvents(EntityUid uid) where TEvent : notnull + => GetListenerSystem().Clear(uid); + + #endregion + #region Entity lookups /// diff --git a/Content.IntegrationTests/Tests/Movement/SlippingTest.cs b/Content.IntegrationTests/Tests/Movement/SlippingTest.cs index 7ee895d7c2..92e4d2471e 100644 --- a/Content.IntegrationTests/Tests/Movement/SlippingTest.cs +++ b/Content.IntegrationTests/Tests/Movement/SlippingTest.cs @@ -1,10 +1,8 @@ #nullable enable -using System.Collections.Generic; -using Content.IntegrationTests.Tests.Interaction; +using Content.IntegrationTests.Tests.Helpers; using Content.Shared.Movement.Components; using Content.Shared.Slippery; using Content.Shared.Stunnable; -using Robust.Shared.GameObjects; using Robust.Shared.Input; using Robust.Shared.Maths; @@ -12,44 +10,32 @@ namespace Content.IntegrationTests.Tests.Movement; public sealed class SlippingTest : MovementTest { - public sealed class SlipTestSystem : EntitySystem - { - public HashSet Slipped = new(); - public override void Initialize() - { - SubscribeLocalEvent(OnSlip); - } - - private void OnSlip(EntityUid uid, SlipperyComponent component, ref SlipEvent args) - { - Slipped.Add(args.Slipped); - } - } + public sealed class SlipTestSystem : TestListenerSystem; [Test] public async Task BananaSlipTest() { - var sys = SEntMan.System(); await SpawnTarget("TrashBananaPeel"); var modifier = Comp(Player).SprintSpeedModifier; Assert.That(modifier, Is.EqualTo(1), "Player is not moving at full speed."); - // Player is to the left of the banana peel and has not slipped. + // Player is to the left of the banana peel. Assert.That(Delta(), Is.GreaterThan(0.5f)); - Assert.That(sys.Slipped, Does.Not.Contain(SEntMan.GetEntity(Player))); // Walking over the banana slowly does not trigger a slip. await SetKey(EngineKeyFunctions.Walk, BoundKeyState.Down); - await Move(DirectionFlag.East, 1f); + await AssertFiresEvent(async () => await Move(DirectionFlag.East, 1f), count: 0); + Assert.That(Delta(), Is.LessThan(0.5f)); - Assert.That(sys.Slipped, Does.Not.Contain(SEntMan.GetEntity(Player))); AssertComp(false, Player); // Moving at normal speeds does trigger a slip. await SetKey(EngineKeyFunctions.Walk, BoundKeyState.Up); - await Move(DirectionFlag.West, 1f); - Assert.That(sys.Slipped, Does.Contain(SEntMan.GetEntity(Player))); + await AssertFiresEvent(async () => await Move(DirectionFlag.West, 1f)); + + // And the person that slipped was the player + AssertEvent(predicate: @event => @event.Slipped == SPlayer); AssertComp(true, Player); } } diff --git a/Content.Shared/Armor/SharedArmorSystem.cs b/Content.Shared/Armor/SharedArmorSystem.cs index 1ff1bbc073..972289460f 100644 --- a/Content.Shared/Armor/SharedArmorSystem.cs +++ b/Content.Shared/Armor/SharedArmorSystem.cs @@ -1,4 +1,5 @@ -using Content.Shared.Damage; +using Content.Shared.Clothing.Components; +using Content.Shared.Damage; using Content.Shared.Examine; using Content.Shared.Inventory; using Content.Shared.Silicons.Borgs; @@ -32,6 +33,9 @@ public abstract class SharedArmorSystem : EntitySystem /// The event, contains the running count of armor percentage as a coefficient private void OnCoefficientQuery(Entity ent, ref InventoryRelayedEvent args) { + if (TryComp(ent, out var mask) && mask.IsToggled) + return; + foreach (var armorCoefficient in ent.Comp.Modifiers.Coefficients) { args.Args.DamageModifiers.Coefficients[armorCoefficient.Key] = args.Args.DamageModifiers.Coefficients.TryGetValue(armorCoefficient.Key, out var coefficient) ? coefficient * armorCoefficient.Value : armorCoefficient.Value; @@ -40,12 +44,18 @@ public abstract class SharedArmorSystem : EntitySystem private void OnDamageModify(EntityUid uid, ArmorComponent component, InventoryRelayedEvent args) { + if (TryComp(uid, out var mask) && mask.IsToggled) + return; + args.Args.Damage = DamageSpecifier.ApplyModifierSet(args.Args.Damage, component.Modifiers); } private void OnBorgDamageModify(EntityUid uid, ArmorComponent component, ref BorgModuleRelayedEvent args) { + if (TryComp(uid, out var mask) && mask.IsToggled) + return; + args.Args.Damage = DamageSpecifier.ApplyModifierSet(args.Args.Damage, component.Modifiers); } diff --git a/Content.Shared/Bed/Sleep/SleepingSystem.cs b/Content.Shared/Bed/Sleep/SleepingSystem.cs index eca6a8befa..27e11bc878 100644 --- a/Content.Shared/Bed/Sleep/SleepingSystem.cs +++ b/Content.Shared/Bed/Sleep/SleepingSystem.cs @@ -59,6 +59,8 @@ public sealed partial class SleepingSystem : EntitySystem SubscribeLocalEvent(OnZombified); SubscribeLocalEvent(OnMobStateChanged); SubscribeLocalEvent(OnCompInit); + SubscribeLocalEvent(OnComponentRemoved); + SubscribeLocalEvent(OnRejuvenate); SubscribeLocalEvent(OnSpeakAttempt); SubscribeLocalEvent(OnSeeAttempt); SubscribeLocalEvent(OnPointAttempt); @@ -69,7 +71,6 @@ public sealed partial class SleepingSystem : EntitySystem SubscribeLocalEvent(OnInteractHand); SubscribeLocalEvent(OnStunEndAttempt); SubscribeLocalEvent(OnStandUpAttempt); - SubscribeLocalEvent(OnRejuvenate); SubscribeLocalEvent(OnStatusEffectApplied); SubscribeLocalEvent(OnUnbuckleAttempt); @@ -102,6 +103,12 @@ public sealed partial class SleepingSystem : EntitySystem TrySleeping((ent, ent.Comp)); } + private void OnRejuvenate(Entity ent, ref RejuvenateEvent args) + { + // WAKE UP!!! + RemComp(ent); + } + /// /// when sleeping component is added or removed, we do some stuff with other components. /// @@ -143,6 +150,16 @@ public sealed partial class SleepingSystem : EntitySystem _actionsSystem.AddAction(ent, ref ent.Comp.WakeAction, WakeActionId, ent); } + private void OnComponentRemoved(Entity ent, ref ComponentRemove args) + { + _actionsSystem.RemoveAction(ent.Owner, ent.Comp.WakeAction); + + var ev = new SleepStateChangedEvent(false); + RaiseLocalEvent(ent, ref ev); + + _blindableSystem.UpdateIsBlind(ent.Owner); + } + private void OnSpeakAttempt(Entity ent, ref SpeakAttemptEvent args) { // TODO reduce duplication of this behavior with MobStateSystem somehow @@ -187,11 +204,6 @@ public sealed partial class SleepingSystem : EntitySystem args.Cancelled = true; } - private void OnRejuvenate(Entity ent, ref RejuvenateEvent args) - { - TryWaking((ent.Owner, ent.Comp), true); - } - private void OnExamined(Entity ent, ref ExaminedEvent args) { if (args.IsInDetailsRange) @@ -275,17 +287,6 @@ public sealed partial class SleepingSystem : EntitySystem TrySleeping(args.Target); } - private void Wake(Entity ent) - { - RemComp(ent); - _actionsSystem.RemoveAction(ent.Owner, ent.Comp.WakeAction); - - var ev = new SleepStateChangedEvent(false); - RaiseLocalEvent(ent, ref ev); - - _blindableSystem.UpdateIsBlind(ent.Owner); - } - /// /// Try sleeping. Only mobs can sleep. /// @@ -345,8 +346,7 @@ public sealed partial class SleepingSystem : EntitySystem _popupSystem.PopupClient(Loc.GetString("wake-other-success", ("target", Identity.Entity(ent, EntityManager))), ent, user); } - Wake((ent, ent.Comp)); - return true; + return RemComp(ent); } /// diff --git a/Content.Shared/Eye/Blinding/Systems/EyeProtectionSystem.cs b/Content.Shared/Eye/Blinding/Systems/EyeProtectionSystem.cs index 0fc01f1b4e..0b4353eeda 100644 --- a/Content.Shared/Eye/Blinding/Systems/EyeProtectionSystem.cs +++ b/Content.Shared/Eye/Blinding/Systems/EyeProtectionSystem.cs @@ -3,6 +3,7 @@ using Content.Shared.Inventory; using Content.Shared.Eye.Blinding.Components; using Content.Shared.Tools.Components; using Content.Shared.Item.ItemToggle.Components; +using Content.Shared.Clothing.Components; namespace Content.Shared.Eye.Blinding.Systems { @@ -29,6 +30,9 @@ namespace Content.Shared.Eye.Blinding.Systems private void OnGetProtection(EntityUid uid, EyeProtectionComponent component, GetEyeProtectionEvent args) { + if (TryComp(uid, out var mask) && mask.IsToggled) + return; + args.Protection += component.ProtectionTime; } diff --git a/Content.Shared/Flash/SharedFlashSystem.cs b/Content.Shared/Flash/SharedFlashSystem.cs index 7f69e86042..02513aa91b 100644 --- a/Content.Shared/Flash/SharedFlashSystem.cs +++ b/Content.Shared/Flash/SharedFlashSystem.cs @@ -22,6 +22,7 @@ using Robust.Shared.Timing; using System.Linq; using Content.Shared.Movement.Systems; using Content.Shared.Random.Helpers; +using Content.Shared.Clothing.Components; namespace Content.Shared.Flash; @@ -258,6 +259,9 @@ public abstract class SharedFlashSystem : EntitySystem private void OnFlashImmunityFlashAttempt(Entity ent, ref FlashAttemptEvent args) { + if (TryComp(ent, out var mask) && mask.IsToggled) + return; + if (ent.Comp.Enabled) args.Cancelled = true; } diff --git a/Content.Shared/ParcelWrap/Systems/ParcelWrappingSystem.cs b/Content.Shared/ParcelWrap/Systems/ParcelWrappingSystem.cs index b19f4b845c..7ea6daeed8 100644 --- a/Content.Shared/ParcelWrap/Systems/ParcelWrappingSystem.cs +++ b/Content.Shared/ParcelWrap/Systems/ParcelWrappingSystem.cs @@ -51,7 +51,6 @@ public sealed partial class ParcelWrappingSystem : EntitySystem wrapper.Owner != target && // Wrapper should never be empty, but may as well make sure. !_charges.IsEmpty(wrapper.Owner) && - _whitelist.IsWhitelistPass(wrapper.Comp.Whitelist, target) && - _whitelist.IsBlacklistFail(wrapper.Comp.Blacklist, target); + _whitelist.CheckBoth(target, wrapper.Comp.Blacklist, wrapper.Comp.Whitelist); } } diff --git a/Resources/Prototypes/Entities/Objects/Misc/parcel_wrap.yml b/Resources/Prototypes/Entities/Objects/Misc/parcel_wrap.yml index 61d8452b93..8d7baa4339 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/parcel_wrap.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/parcel_wrap.yml @@ -28,6 +28,17 @@ - type: LimitedCharges maxCharges: 30 +- type: entity + parent: ParcelWrap + id: ParcelWrapAdmeme + name: bluespace wrap + suffix: Admeme + description: Paper used contain items for transport. This one seems to be able to store an unusual amount of space within it. + components: + - type: ParcelWrap + whitelist: null + blacklist: null + - type: entity parent: BaseItem id: WrappedParcel diff --git a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml index 0a933ef1f4..d7fec6aa8c 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/lathe.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/lathe.yml @@ -136,6 +136,7 @@ - type: EmagLatheRecipes emagStaticPacks: - SecurityAmmoStatic + - SyndicateAmmoStatic - type: BlueprintReceiver whitelist: tags: @@ -406,6 +407,9 @@ - SecurityAmmo - SecurityWeapons - SecurityDisablers + - type: EmagLatheRecipes + emagStaticPacks: + - SyndicateAmmoStatic - type: MaterialStorage whitelist: tags: @@ -442,6 +446,9 @@ runningState: icon staticPacks: - SecurityAmmoStatic + - type: EmagLatheRecipes + emagStaticPacks: + - SyndicateAmmoStatic - type: MaterialStorage whitelist: tags: diff --git a/Resources/Prototypes/Reagents/narcotics.yml b/Resources/Prototypes/Reagents/narcotics.yml index a1b1033a4b..92236d746c 100644 --- a/Resources/Prototypes/Reagents/narcotics.yml +++ b/Resources/Prototypes/Reagents/narcotics.yml @@ -289,16 +289,19 @@ meltingPoint: 128.0 metabolisms: Narcotic: - effects: + effects: # It would be nice to have speech slurred or mumbly, but accents are a bit iffy atm. Same for distortion effects. + - !type:MovespeedModifier + walkSpeedModifier: 0.65 + sprintSpeedModifier: 0.65 - !type:ModifyStatusEffect conditions: - !type:ReagentThreshold reagent: Nocturine min: 8 effectProto: StatusEffectForcedSleeping - time: 3 - delay: 6 - type: Add + time: 6 + delay: 5 + type: Update - type: reagent id: MuteToxin diff --git a/Resources/Prototypes/Recipes/Lathes/Packs/security.yml b/Resources/Prototypes/Recipes/Lathes/Packs/security.yml index 0dc5fe3d90..51058205c4 100644 --- a/Resources/Prototypes/Recipes/Lathes/Packs/security.yml +++ b/Resources/Prototypes/Recipes/Lathes/Packs/security.yml @@ -49,9 +49,6 @@ - MagazinePistolSubMachineGunTopMountedEmpty - MagazineRifle - MagazineRifleEmpty - - MagazineShotgun - - MagazineShotgunEmpty - - MagazineShotgunSlug - SpeedLoaderMagnum - SpeedLoaderMagnumEmpty diff --git a/Resources/Prototypes/Recipes/Lathes/Packs/syndicate.yml b/Resources/Prototypes/Recipes/Lathes/Packs/syndicate.yml new file mode 100644 index 0000000000..8c87f80780 --- /dev/null +++ b/Resources/Prototypes/Recipes/Lathes/Packs/syndicate.yml @@ -0,0 +1,9 @@ +## Static recipes + +# Added to emagged autolathe +- type: latheRecipePack + id: SyndicateAmmoStatic + recipes: + - MagazineShotgun + - MagazineShotgunEmpty + - MagazineShotgunSlug diff --git a/Resources/ServerInfo/Guidebook/ServerRules/SpaceLaw/SpaceLaw.xml b/Resources/ServerInfo/Guidebook/ServerRules/SpaceLaw/SpaceLaw.xml index 25ccac38f6..917483b10a 100644 --- a/Resources/ServerInfo/Guidebook/ServerRules/SpaceLaw/SpaceLaw.xml +++ b/Resources/ServerInfo/Guidebook/ServerRules/SpaceLaw/SpaceLaw.xml @@ -25,7 +25,11 @@ [color=#a4885c]Tracking Implants:[/color] Trackers can be applied to any suspect that has been convicted of a violent crime (the red linked crimes). - [color=#a4885c]Mind Shields:[/color] Shields can be administered to any inmate who has been clearly mind controlled, lost control of themselves, or a suspect charged with unlawful control. Unlike standard implantation you may hold a prisoner until you finish issuing Mind Shields, so long as it's done in a timely fashion. If a suspect refuses to cooperate or the implant fails to function they can be charged with Refusal of Mental Shielding. + [color=#a4885c]Mind Shields:[/color] Mind Shields can be administered to any suspect charged with a crime if there is reasonable and articulable suspicion to believe they committed the crime while under mind control. If there is undeniable proof of ongoing mind control on the station, such as a successful deconversion, all the following statements apply: + + - Crew can be required to be shielded and may be forcibly implanted if they refuse. + - If a suspect refuses to cooperate or the implant fails to function they can be charged with Refusal of Mental Shielding. + - Unlike standard implantation you may indefinitely extend the duration of a prisoner's sentence until you finish issuing Mind Shields, so long as active attempts are made to procure implants and complete the implantation procedure. Usage of contraband implanters is a detainable offense, but it's not reasonable to detain someone solely for having an unidentified implant inside them. -- 2.52.0