From f95774b3ce4300012b8dbaa60829499ba6a68203 Mon Sep 17 00:00:00 2001
From: sleepyyapril <123355664+sleepyyapril@users.noreply.github.com>
Date: Sun, 17 Nov 2024 15:47:11 -0400
Subject: [PATCH] Mass Engine Update (#1220)

<!--
This is a semi-strict format, you can add/remove sections as needed but
the order/format should be kept the same
Remove these comments before submitting
-->

# Description

<!--
Explain this PR in as much detail as applicable

Some example prompts to consider:
How might this affect the game? The codebase?
What might be some alternatives to this?
How/Who does this benefit/hurt [the game/codebase]?
-->

An attempt to do all engine updates in one go. Not focusing on content
associated, only what's needed for it to run and any bug fixes.

---

# TODO

- [x] Fix bug where unbuckling resets you to lying down.
- [x] Fix bug where you can no longer get up after lying down.
- [x] See what else I broke.
---

---------

Signed-off-by: sleepyyapril <123355664+sleepyyapril@users.noreply.github.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Co-authored-by: Tayrtahn <tayrtahn@gmail.com>
Co-authored-by: VMSolidus <evilexecutive@gmail.com>
Co-authored-by: Nemanja <98561806+EmoGarbage404@users.noreply.github.com>
Co-authored-by: ShadowCommander <10494922+ShadowCommander@users.noreply.github.com>
Co-authored-by: Plykiya <58439124+Plykiya@users.noreply.github.com>
Co-authored-by: plykiya <plykiya@protonmail.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
Co-authored-by: Chief-Engineer <119664036+Chief-Engineer@users.noreply.github.com>
Co-authored-by: deltanedas <39013340+deltanedas@users.noreply.github.com>
---
 Content.Client/Alerts/ClientAlertsSystem.cs   |    2 +-
 Content.Client/Audio/AmbientSoundSystem.cs    |    6 +-
 .../Audio/ClientGlobalSoundSystem.cs          |    4 +-
 .../Audio/ContentAudioSystem.AmbientMusic.cs  |    2 +-
 .../Audio/ContentAudioSystem.LobbyMusic.cs    |    6 +-
 Content.Client/Buckle/BuckleSystem.cs         |   66 +-
 .../Clickable/ClickableComponent.cs           |  152 +-
 Content.Client/Clickable/ClickableSystem.cs   |  168 ++
 Content.Client/Entry/EntryPoint.cs            |    3 -
 Content.Client/Explosion/ExplosionOverlay.cs  |    3 +-
 Content.Client/Gameplay/GameplayStateBase.cs  |   44 +-
 .../Guidebook/Controls/GuidebookWindow.xaml   |    2 +-
 .../Guidebook/DocumentParsingManager.cs       |   19 +
 Content.Client/Info/InfoSystem.cs             |   25 -
 Content.Client/Info/RulesAndInfoWindow.cs     |   14 +-
 Content.Client/Info/RulesControl.xaml         |   24 +-
 Content.Client/Info/RulesControl.xaml.cs      |   45 +-
 Content.Client/Info/RulesManager.cs           |  105 -
 Content.Client/Info/RulesPopup.xaml           |   10 +-
 Content.Client/Info/RulesPopup.xaml.cs        |    2 -
 Content.Client/IoC/ClientContentIoC.cs        |   40 +-
 .../Movement/Systems/WaddleAnimationSystem.cs |    4 +-
 .../Visualizers/DeepFriedVisualizer.cs        |    3 +-
 .../Visualizers/DeepFryerVisualizer.cs        |    4 +-
 Content.Client/Nyanotrasen/Mail/MailSystem.cs |    3 +-
 .../Outline/InteractionOutlineSystem.cs       |    6 +-
 Content.Client/Revenant/RevenantSystem.cs     |    3 +-
 Content.Client/Sandbox/SandboxSystem.cs       |    2 +-
 Content.Client/Shuttles/FtlArrivalOverlay.cs  |   82 +
 .../Systems/ShuttleSystem.EmergencyConsole.cs |    3 +-
 .../Shuttles/Systems/ShuttleSystem.cs         |   21 +
 Content.Client/Traits/ParacusiaSystem.cs      |    6 +-
 .../Systems/Alerts/AlertsUIController.cs      |    3 +-
 .../Systems/Alerts/Widgets/AlertsUI.xaml.cs   |   13 +-
 .../Systems/Info/InfoUIController.cs          |  101 +-
 .../Systems/Storage/Controls/ItemGridPiece.cs |    4 +-
 Content.Client/Weather/WeatherSystem.cs       |   14 +-
 .../Tests/Actions/ActionPvsDetachTest.cs      |   22 +-
 .../Tests/Actions/ActionsAddedTest.cs         |    2 +-
 .../Tests/Body/GibTest.cs                     |    2 +-
 .../Tests/Body/LegTest.cs                     |    8 +-
 .../Tests/Body/LungTest.cs                    |    8 +-
 .../Tests/Body/SaveLoadReparentTest.cs        |    5 +-
 .../Tests/Buckle/BuckleDragTest.cs            |   60 +
 .../Tests/Buckle/BuckleTest.Interact.cs       |  108 +
 .../Tests/Buckle/BuckleTest.cs                |   22 +-
 Content.IntegrationTests/Tests/CargoTest.cs   |   19 +-
 .../Tests/Chemistry/DispenserTest.cs          |    2 +-
 .../Chemistry/FixedPoint2SerializationTest.cs |    8 +-
 .../Tests/Chemistry/SolutionRoundingTest.cs   |    4 +-
 .../Tests/Chemistry/SolutionSystemTests.cs    |   12 +-
 .../Tests/Chemistry/TryAllReactionsTest.cs    |    4 +-
 .../Tests/ClickableTest.cs                    |    4 +-
 .../Tests/Climbing/ClimbingTest.cs            |    1 +
 .../Tests/Commands/PardonCommand.cs           |   10 +-
 .../Tests/Commands/RejuvenateTest.cs          |    6 +-
 .../Tests/Commands/RestartRoundTest.cs        |    2 +-
 .../Interaction/ComputerContruction.cs        |   10 +-
 .../Construction/Interaction/CraftingTests.cs |   56 +-
 .../Interaction/GrilleWindowConstruction.cs   |   13 +-
 .../Interaction/MachineConstruction.cs        |    7 +-
 .../Construction/Interaction/PanelScrewing.cs |   12 +-
 .../Interaction/PlaceableDeconstruction.cs    |    4 +-
 .../Interaction/WallConstruction.cs           |    9 +-
 .../Interaction/WindowConstruction.cs         |    8 +-
 .../Construction/Interaction/WindowRepair.cs  |    2 +-
 .../Tests/ContainerOcclusionTest.cs           |   18 +-
 .../Tests/Damageable/DamageSpecifierTest.cs   |   44 +-
 .../Tests/Damageable/DamageableTest.cs        |    5 +-
 .../DestructibleDestructionTest.cs            |    1 +
 .../Tests/DoAfter/DoAfterCancellationTests.cs |   39 +-
 .../Tests/Doors/AirlockTest.cs                |   20 +-
 .../Tests/DummyIconTest.cs                    |    2 +-
 .../EncryptionKeys/RemoveEncryptionKeys.cs    |   16 +-
 Content.IntegrationTests/Tests/EntityTest.cs  |   12 +-
 .../Tests/Fluids/FluidSpillTest.cs            |   27 +-
 .../Tests/Fluids/PuddleTest.cs                |   14 +-
 .../Tests/FollowerSystemTest.cs               |    5 +-
 .../Components/ActionBlocking/HandCuffTest.cs |    7 +-
 .../Components/Mobs/AlertsComponentTests.cs   |   15 +-
 .../Tests/GameRules/AntagPreferenceTest.cs    |    2 +-
 .../Tests/GameRules/FailAndStartPresetTest.cs |   24 +-
 .../Tests/GameRules/RuleMaxTimeRestartTest.cs |   10 +-
 .../Tests/Gravity/WeightlessStatusTests.cs    |    8 +-
 .../Tests/GravityGridTest.cs                  |   30 +-
 .../Click/InteractionSystemTests.cs           |   50 +-
 .../Tests/Interaction/InRangeUnobstructed.cs  |    5 +-
 .../InteractionTest.EntitySpecifier.cs        |    8 +-
 .../Interaction/InteractionTest.Helpers.cs    |  403 +++-
 .../Tests/Interaction/InteractionTest.cs      |   48 +-
 .../Tests/Linter/StaticFieldValidationTest.cs |  118 +-
 .../Tests/MachineBoardTest.cs                 |   11 +-
 .../Tests/Mapping/MappingTests.cs             |    2 +-
 .../Tests/MaterialArbitrageTest.cs            |   17 +-
 .../Tests/Minds/GhostTests.cs                 |   44 +-
 .../Tests/Minds/MindTests.Helpers.cs          |    2 +-
 .../Tests/Movement/BuckleMovementTest.cs      |   63 +
 .../{Interaction => Movement}/MovementTest.cs |    5 +-
 .../Tests/Movement/PullingTest.cs             |   73 +
 .../{Slipping => Movement}/SlippingTest.cs    |    6 +-
 .../Tests/Networking/PvsCommandTest.cs        |    4 +-
 .../Networking/SimplePredictReconcileTest.cs  |    9 +-
 .../Tests/Payload/ModularGrenadeTests.cs      |   14 +-
 .../Tests/PostMapInitTest.cs                  |   33 +-
 .../Tests/Power/PowerTest.cs                  |  272 ++-
 .../Tests/PrototypeSaveTest.cs                |    5 +-
 .../Tests/Puller/PullerTest.cs                |    2 +-
 .../Tests/ResearchTest.cs                     |    4 +-
 Content.IntegrationTests/Tests/SalvageTest.cs |    4 +-
 .../Tests/SaveLoadMapTest.cs                  |   21 +-
 .../Tests/SaveLoadSaveTest.cs                 |   20 +-
 .../Tests/Serialization/SerializationTest.cs  |   16 +-
 .../Tests/Shuttle/DockTest.cs                 |    5 +-
 Content.IntegrationTests/Tests/ShuttleTest.cs |   19 +-
 .../Tests/Sprite/ItemSpriteTest.cs            |   10 +-
 .../Tests/{Minds => Station}/JobTests.cs      |    0
 .../Tests/Storage/StorageInteractionTest.cs   |   75 +
 Content.IntegrationTests/Tests/Tag/TagTest.cs |   26 +-
 .../Tests/Tiles/TileConstructionTests.cs      |   16 +-
 .../Tests/Toolshed/ToolshedTest.cs            |    8 +-
 .../Tests/VendingMachineRestockTest.cs        |    3 +-
 .../Tests/Weldable/WeldableTests.cs           |    2 +-
 ...0606065731_RemoveLastReadRules.Designer.cs | 1909 +++++++++++++++++
 .../20240606065731_RemoveLastReadRules.cs     |   29 +
 .../PostgresServerDbContextModelSnapshot.cs   |    4 -
 ...0606065717_RemoveLastReadRules.Designer.cs | 1834 ++++++++++++++++
 .../20240606065717_RemoveLastReadRules.cs     |   29 +
 .../SqliteServerDbContextModelSnapshot.cs     |    4 -
 Content.Server.Database/Model.cs              |    2 -
 .../Abilities/Mime/MimePowersComponent.cs     |    8 +
 .../Abilities/Mime/MimePowersSystem.cs        |   11 +-
 .../Administration/Managers/AdminManager.cs   |    2 +
 Content.Server/Administration/ServerApi.cs    |    2 +-
 .../Systems/AdminVerbSystem.Smites.cs         |    6 +-
 Content.Server/Alert/Commands/ClearAlert.cs   |    7 +-
 Content.Server/Alert/Commands/ShowAlert.cs    |    7 +-
 Content.Server/Antag/AntagSelectionSystem.cs  |    2 +-
 .../Components/AntagSelectionComponent.cs     |    2 +-
 .../Antag/MobReplacementRuleSystem.cs         |   11 +-
 .../Atmos/Components/BarotraumaComponent.cs   |   10 +
 .../Atmos/Components/FlammableComponent.cs    |    4 +
 .../Atmos/EntitySystems/BarotraumaSystem.cs   |   11 +-
 .../Atmos/EntitySystems/FlammableSystem.cs    |    4 +-
 .../Atmos/EntitySystems/GasAnalyzerSystem.cs  |    6 +-
 .../EntitySystems/GasTileOverlaySystem.cs     |   12 +-
 .../Unary/EntitySystems/GasPortableSystem.cs  |    8 +-
 .../BarSign/Systems/BarSignSystem.cs          |    2 +-
 Content.Server/Bed/BedSystem.cs               |   49 +-
 .../Bed/Components/HealOnBuckleComponent.cs   |   21 +-
 .../Bed/Components/HealOnBuckleHealing.cs     |    1 +
 .../Bed/Components/StasisBedComponent.cs      |    3 +-
 .../Body/Components/BloodstreamComponent.cs   |    4 +
 .../Body/Components/InternalsComponent.cs     |    6 +
 .../Body/Components/LungComponent.cs          |    4 +-
 .../Body/Systems/BloodstreamSystem.cs         |    7 +-
 .../Body/Systems/InternalsSystem.cs           |   14 +-
 .../Body/Systems/MetabolizerSystem.cs         |    6 +
 .../Body/Systems/RespiratorSystem.cs          |    3 +
 .../Cargo/Systems/CargoSystem.Shuttle.cs      |   42 +-
 Content.Server/Cargo/Systems/CargoSystem.cs   |    3 +
 Content.Server/Carrying/CarryingSystem.cs     |    4 +-
 Content.Server/Chat/Systems/ChatSystem.cs     |    6 +-
 Content.Server/Chat/TelepathicChatSystem.cs   |   10 +-
 .../Chemistry/ReagentEffects/AdjustAlert.cs   |    8 +-
 Content.Server/Clothing/MagbootsSystem.cs     |    4 +-
 .../Systems/ChameleonClothingSystem.cs        |    4 +-
 .../Construction/PartExchangerSystem.cs       |    7 +-
 Content.Server/Database/ServerDbBase.cs       |   24 -
 Content.Server/Database/ServerDbManager.cs    |   19 -
 Content.Server/Decals/DecalSystem.cs          |    5 +-
 .../Behaviors/SpawnEntitiesBehavior.cs        |    7 +-
 .../Destructible/Thresholds/MinMax.cs         |   26 -
 .../Systems/NetworkConfiguratorSystem.cs      |    2 +-
 .../Disposal/Tube/DisposalTubeSystem.cs       |   16 +-
 .../Unit/EntitySystems/DisposalUnitSystem.cs  |   11 +-
 Content.Server/Dragon/DragonRiftSystem.cs     |    2 +-
 .../Ensnaring/EnsnareableSystem.Ensnaring.cs  |   10 +-
 Content.Server/Ensnaring/EnsnareableSystem.cs |    2 +-
 Content.Server/Entry/EntryPoint.cs            |    2 -
 .../GameTicking/GameTicker.GameRule.cs        |    3 +-
 .../GameTicking/GameTicker.Spawning.cs        |   15 +-
 .../GameTicking/Rules/DeathMatchRuleSystem.cs |    2 +-
 .../GameTicking/Rules/GameRulePrototype.cs    |   15 -
 .../Rules/GameRuleSystem.Utility.cs           |    2 +-
 .../GameTicking/Rules/GameRuleSystem.cs       |    2 +-
 .../Rules/InactivityTimeRestartRuleSystem.cs  |    2 +-
 .../Rules/KillCalloutRuleSystem.cs            |    2 +-
 .../GameTicking/Rules/LoadMapRuleSystem.cs    |    4 +-
 .../Rules/MaxTimeRestartRuleSystem.cs         |    2 +-
 .../GameTicking/Rules/NukeopsRuleSystem.cs    |    6 +-
 .../GameTicking/Rules/RespawnRuleSystem.cs    |    3 +-
 .../Rules/RevolutionaryRuleSystem.cs          |    2 +-
 .../RoundstartStationVariationRuleSystem.cs   |    2 +-
 .../GameTicking/Rules/SandboxRuleSystem.cs    |    2 +-
 .../GameTicking/Rules/SecretRuleSystem.cs     |    2 +-
 .../GameTicking/Rules/SubGamemodesSystem.cs   |    2 +-
 .../GameTicking/Rules/TraitorRuleSystem.cs    |    6 +-
 .../GameTicking/Rules/ZombieRuleSystem.cs     |    4 +-
 Content.Server/Geras/GerasComponent.cs        |    2 +-
 Content.Server/Ghost/GhostSystem.cs           |   28 +-
 Content.Server/Gravity/GravitySystem.cs       |    4 +-
 .../Holiday/Christmas/RandomGiftSystem.cs     |    2 +-
 Content.Server/HotPotato/HotPotatoSystem.cs   |    4 +-
 .../HumanoidAppearanceSystem.Modifier.cs      |    6 +-
 .../Systems/HumanoidAppearanceSystem.cs       |   13 +-
 Content.Server/Info/InfoSystem.cs             |   35 -
 Content.Server/Info/RulesManager.cs           |   48 -
 Content.Server/Info/ShowRulesCommand.cs       |   14 +-
 .../Instruments/SwappableInstrumentSystem.cs  |    2 +-
 .../Actions/ChangeStandingStateAction.cs      |    2 +-
 Content.Server/IoC/ServerContentIoC.cs        |    2 -
 .../Kitchen/EntitySystems/MicrowaveSystem.cs  |    8 +-
 .../EntitySystems/ReagentGrinderSystem.cs     |   10 +-
 .../EntitySystems/RotatingLightSystem.cs      |    2 +-
 Content.Server/Lightning/LightningSystem.cs   |   22 +-
 .../Materials/MaterialReclaimerSystem.cs      |    2 +-
 .../EntitySystems/MechGrabberSystem.cs        |   10 +-
 Content.Server/Mech/Systems/MechSystem.cs     |   10 +-
 Content.Server/Mood/MoodComponent.cs          |   30 +-
 Content.Server/Mood/MoodSystem.cs             |    4 +-
 .../Operators/Combat/UnbuckleOperator.cs      |    7 +-
 Content.Server/NPC/Systems/NPCJukeSystem.cs   |   10 +-
 .../Systems/NPCSteeringSystem.Obstacles.cs    |    2 +-
 .../NPC/Systems/NPCSteeringSystem.cs          |    1 +
 .../NameIdentifier/NameIdentifierSystem.cs    |    2 +-
 .../Ninja/Systems/SpaceNinjaSystem.cs         |   11 +-
 .../Commands/TileWindowsCommand.cs            |   20 +-
 .../PlayTimeTrackingManager.Whitelist.cs      |    2 +-
 .../StationEvents/Events/MidRoundAntagRule.cs |    2 +-
 Content.Server/Objectives/ObjectivesSystem.cs |    2 +-
 Content.Server/OfferItem/OfferItemSystem.cs   |    4 +-
 .../Physics/Controllers/ConveyorController.cs |    4 +-
 Content.Server/Pinpointer/NavMapSystem.cs     |   14 +-
 .../PowerMonitoringConsoleSystem.cs           |    2 +-
 .../PowerCell/PowerCellSystem.Draw.cs         |    4 +-
 Content.Server/Psionics/Dreams/DreamSystem.cs |    2 +-
 .../Research/Systems/ResearchSystem.Server.cs |    8 +-
 .../Systems/ResearchSystem.Technology.cs      |    8 +-
 .../Revenant/EntitySystems/RevenantSystem.cs  |    2 +-
 .../Salvage/SalvageSystem.Runner.cs           |    8 +-
 Content.Server/Shadowkin/ShadowkinSystem.cs   |   12 +-
 .../Shuttles/Systems/ShuttleConsoleSystem.cs  |    6 +-
 .../Systems/ShuttleSystem.FasterThanLight.cs  |   32 +-
 .../Shuttles/Systems/ShuttleSystem.cs         |    2 +
 .../Charge/Systems/SiliconChargeSystem.cs     |   12 +-
 Content.Server/Silicons/Borgs/BorgSystem.cs   |   22 +-
 .../Components/EntityTableSpawnerComponent.cs |   30 +
 .../EntitySystems/ConditionalSpawnerSystem.cs |   31 +-
 Content.Server/Sprite/RandomSpriteSystem.cs   |    2 +-
 .../BasicStationEventSchedulerSystem.cs       |    4 +-
 .../Events/AlertLevelInterceptionRule.cs      |    4 +-
 .../StationEvents/Events/AnomalySpawnRule.cs  |    5 +-
 .../Events/BluespaceArtifactRule.cs           |    3 +-
 .../Events/BluespaceLockerRule.cs             |    2 +-
 .../StationEvents/Events/BreakerFlipRule.cs   |    3 +-
 .../Events/BureaucraticErrorRule.cs           |    2 +-
 .../StationEvents/Events/CargoGiftsRule.cs    |    3 +-
 .../StationEvents/Events/ClericalErrorRule.cs |    2 +-
 .../StationEvents/Events/FalseAlarmRule.cs    |    3 +-
 .../StationEvents/Events/FreeProberRule.cs    |   15 +-
 .../StationEvents/Events/GasLeakRule.cs       |    2 +-
 .../Events/GlimmerEventSystem.cs              |    2 +-
 .../Events/GlimmerMobSpawnRule.cs             |    2 +-
 .../Events/GlimmerRandomSentienceRule.cs      |    2 +-
 .../Events/GlimmerRevenantSpawnRule.cs        |    2 +-
 .../StationEvents/Events/ImmovableRodRule.cs  |    4 +-
 .../StationEvents/Events/IonStormRule.cs      |    2 +-
 .../StationEvents/Events/KudzuGrowthRule.cs   |    2 +-
 .../Events/MassHallucinationsRule.cs          |    2 +-
 .../StationEvents/Events/MassMindSwapRule.cs  |    2 +-
 .../StationEvents/Events/MeteorSwarmRule.cs   |    2 +-
 .../StationEvents/Events/NinjaSpawnRule.cs    |    2 +-
 .../StationEvents/Events/NoosphericFryRule.cs |   13 +-
 .../Events/NoosphericStormRule.cs             |    2 +-
 .../StationEvents/Events/NoosphericZapRule.cs |    2 +-
 .../Events/PirateRadioSpawnRule.cs            |    2 +-
 .../Events/PowerGridCheckRule.cs              |    2 +-
 .../Events/PsionicCatGotYourTongueRule.cs     |    2 +-
 .../Events/RandomEntityStorageSpawnRule.cs    |    2 +-
 .../Events/RandomSentienceRule.cs             |    7 +-
 .../StationEvents/Events/RandomSpawnRule.cs   |    2 +-
 .../StationEvents/Events/SolarFlareRule.cs    |    2 +-
 .../Events/StationEventSystem.cs              |    2 +-
 .../StationEvents/Events/VentClogRule.cs      |    2 +-
 .../StationEvents/Events/VentCrittersRule.cs  |    2 +-
 .../OscillatingStationEventScheduler.cs       |    2 +-
 .../RampingStationEventSchedulerSystem.cs     |    2 +-
 .../Components/TemperatureComponent.cs        |    8 +
 .../Temperature/Systems/TemperatureSystem.cs  |   14 +-
 .../Assorted/ForeignerTraitComponent.cs       |    2 +-
 .../Traits/Assorted/ParacusiaSystem.cs        |    6 +-
 .../Ranged/Systems/GunSystem.Battery.cs       |    2 +-
 .../Ranged/Systems/GunSystem.Revolver.cs      |    2 +-
 .../Systems/RandomInstrumentArtifactSystem.cs |    2 +-
 .../Zombies/ZombieSystem.Transform.cs         |    2 +-
 .../Actions/Events/FabricateActionEvent.cs    |    2 +-
 Content.Shared/Alert/AlertCategory.cs         |   21 -
 .../Alert/AlertCategoryPrototype.cs           |   14 +
 Content.Shared/Alert/AlertKey.cs              |   12 +-
 Content.Shared/Alert/AlertOrderPrototype.cs   |   35 +-
 Content.Shared/Alert/AlertPrototype.cs        |  206 +-
 Content.Shared/Alert/AlertState.cs            |    3 +-
 Content.Shared/Alert/AlertType.cs             |   78 -
 Content.Shared/Alert/AlertsSystem.cs          |   26 +-
 Content.Shared/Alert/ClickAlertEvent.cs       |    7 +-
 .../Buckle/Components/BuckleComponent.cs      |  113 +-
 .../Buckle/Components/StrapComponent.cs       |   53 +-
 .../Buckle/SharedBuckleSystem.Buckle.cs       |  571 +++--
 .../Buckle/SharedBuckleSystem.Interaction.cs  |  200 ++
 .../Buckle/SharedBuckleSystem.Strap.cs        |  257 +--
 Content.Shared/Buckle/SharedBuckleSystem.cs   |   49 +-
 Content.Shared/CCVar/CCVars.cs                |   18 +-
 .../Chapel/SharedSacrificialAltarSystem.cs    |    4 +-
 .../Climbing/Systems/ClimbSystem.cs           |    6 +-
 .../SharedChameleonClothingSystem.cs          |    2 +-
 .../Loadouts/Systems/LoadoutSystem.cs         |    3 +-
 Content.Shared/Clothing/MagbootsComponent.cs  |    4 +
 .../Pacification/PacificationSystem.cs        |    5 +-
 .../Pacification/PacifiedComponent.cs         |    4 +
 .../Containers/ContainerFillSystem.cs         |   37 +
 .../EntityTableContainerFillComponent.cs      |   13 +
 .../Containers/ItemSlot/ItemSlotsSystem.cs    |    6 +-
 .../Cuffs/Components/CuffableComponent.cs     |    5 +
 Content.Shared/Cuffs/SharedCuffableSystem.cs  |   34 +-
 .../Damage/Components/StaminaComponent.cs     |    7 +-
 .../Damage/Systems/StaminaSystem.cs           |   17 +-
 Content.Shared/Decals/SharedDecalSystem.cs    |    2 +-
 .../Destructible/Thresholds/MinMax.cs         |   24 +
 Content.Shared/Dice/SharedDiceSystem.cs       |    2 +-
 .../SharedElectrocutionSystem.cs              |    2 +-
 Content.Shared/Emoting/EmoteSystem.cs         |    2 +-
 .../Components/EnsnareableComponent.cs        |    5 +
 .../EntitySelectors/AllSelector.cs            |   25 +
 .../EntitySelectors/EntSelector.cs            |   27 +
 .../EntitySelectors/EntityTableSelector.cs    |   49 +
 .../EntitySelectors/GroupSelector.cs          |   28 +
 .../EntitySelectors/NestedSelector.cs         |   20 +
 .../EntitySelectors/NoneSelector.cs           |   16 +
 .../EntityTable/EntityTablePrototype.cs       |   18 +
 .../EntityTable/EntityTableSystem.cs          |   20 +
 .../ValueSelector/ConstantNumberSelector.cs   |   22 +
 .../ValueSelector/NumberSelector.cs           |   16 +
 .../ValueSelector/RangeNumberSelector.cs      |   19 +
 Content.Shared/Examine/ExamineSystemShared.cs |   30 +-
 .../OnTrigger/SmokeOnTriggerComponent.cs      |    2 +-
 Content.Shared/Foldable/FoldableSystem.cs     |    6 +-
 .../Friction/TileFrictionController.cs        |    2 +-
 .../Components/ActiveGameRuleComponent.cs     |    6 +-
 .../Components/DelayedStartRuleComponent.cs   |    2 +-
 .../Components/EndedGameRuleComponent.cs      |    6 +-
 .../Components/GameRuleComponent.cs           |    5 +-
 .../Gravity/SharedFloatingVisualizerSystem.cs |    2 +-
 .../Gravity/SharedGravitySystem.Shake.cs      |    4 +-
 Content.Shared/Gravity/SharedGravitySystem.cs |   15 +-
 .../EntitySystems/SharedHandsSystem.Drop.cs   |    4 +-
 .../SharedHumanoidAppearanceSystem.cs         |   24 +-
 .../Implants/SharedImplanterSystem.cs         |    4 +-
 Content.Shared/Info/RulesMessages.cs          |   25 +
 Content.Shared/Info/SharedInfo.cs             |   28 -
 Content.Shared/Info/SharedRulesManager.cs     |   60 -
 .../Instruments/SharedInstrumentSystem.cs     |    4 +-
 .../Interaction/RotateToFaceSystem.cs         |   32 +-
 .../Interaction/SharedInteractionSystem.cs    |    2 +-
 .../Item/ItemToggle/SharedItemToggleSystem.cs |   19 +-
 .../Light/SharedHandheldLightSystem.cs        |    2 +-
 .../Light/SharedRgbLightControllerSystem.cs   |    6 +-
 .../Mech/EntitySystems/SharedMechSystem.cs    |    6 +-
 .../Medical/CPR/Systems/CPRSystem.cs          |    8 +-
 .../Mobs/Components/MobThresholdsComponent.cs |   28 +-
 .../Systems/MobStateSystem.Subscribers.cs     |   18 +-
 .../Mobs/Systems/MobThresholdSystem.cs        |    2 +-
 .../Pulling/Components/PullableComponent.cs   |    4 +
 .../Pulling/Components/PullerComponent.cs     |    6 +-
 .../Pulling/Events/PullStartedMessage.cs      |   11 +-
 .../Pulling/Events/PullStoppedMessage.cs      |   13 +-
 .../Movement/Pulling/Systems/PullingSystem.cs |   66 +-
 .../Movement/Systems/SharedMoverController.cs |    2 +-
 .../Systems/SpeedModifierContactsSystem.cs    |    2 +-
 .../Ninja/Components/SpaceNinjaComponent.cs   |    4 +
 .../Nutrition/Components/HungerComponent.cs   |   14 +-
 .../Nutrition/Components/ThirstComponent.cs   |   12 +-
 .../Nutrition/EntitySystems/HungerSystem.cs   |    8 +-
 .../Nutrition/EntitySystems/ThirstSystem.cs   |    2 +-
 .../OfferItem/OfferItemComponent.cs           |    5 +
 Content.Shared/PAI/PAIComponent.cs            |    2 +-
 .../Controllers/SharedConveyorController.cs   |    4 +-
 .../Preferences/HumanoidCharacterProfile.cs   |   36 +-
 Content.Shared/RCD/Systems/RCDAmmoSystem.cs   |    4 +-
 .../Random/Helpers/SharedRandomExtensions.cs  |   48 +-
 .../Revenant/Components/RevenantComponent.cs  |    4 +
 .../Shadowkin/ShadowkinComponent.cs           |    5 +
 Content.Shared/Showers/SharedShowerSystem.cs  |    7 +-
 .../Shuttles/Components/FTLComponent.cs       |   16 +-
 .../Components/FtlVisualizerComponent.cs      |   23 +
 .../Shuttles/Components/PilotComponent.cs     |    5 +
 .../Silicon/Systems/SharedSiliconSystem.cs    |    4 +-
 .../Borgs/Components/BorgChassisComponent.cs  |   10 +-
 .../EntitySystems/SharedEventHorizonSystem.cs |    8 +-
 Content.Shared/Stacks/SharedStackSystem.cs    |    6 +-
 .../Standing/SharedLayingDownSystem.cs        |    4 +-
 .../Standing/StandingStateSystem.cs           |    6 +-
 .../StationRecordKeyStorageSystem.cs          |    2 +-
 .../StatusEffect/StatusEffectPrototype.cs     |    2 +-
 .../StatusEffect/StatusEffectsSystem.cs       |   14 +-
 .../Storage/EntitySystems/BinSystem.cs        |    6 +-
 Content.Shared/Stunnable/SharedStunSystem.cs  |   24 +-
 .../SubFloor/SharedTrayScannerSystem.cs       |    2 +-
 Content.Shared/Tag/TagSystem.cs               |   54 +-
 .../Systems/LinkedEntitySystem.cs             |    6 +-
 .../Assorted/Systems/LegsParalyzedSystem.cs   |   27 +-
 .../Assorted/Systems/SharedSingerSystem.cs    |    2 +-
 .../Marker/SharedDamageMarkerSystem.cs        |    4 +-
 .../Weapons/Misc/SharedTetherGunSystem.cs     |   22 +-
 .../Systems/RechargeBasicEntityAmmoSystem.cs  |    8 +-
 .../Ranged/Systems/RechargeCycleAmmoSystem.cs |    2 +-
 .../Weapons/Ranged/Systems/SharedGunSystem.cs |    2 +-
 .../Weapons/Reflect/ReflectSystem.cs          |    7 +-
 Content.Shared/Weather/SharedWeatherSystem.cs |   20 +-
 .../Shared/Alert/AlertManagerTests.cs         |   17 +-
 .../Shared/Alert/AlertOrderPrototypeTests.cs  |   26 +-
 .../Shared/Alert/AlertPrototypeTests.cs       |    6 +-
 .../EinsteinEngines/default.toml              |    3 +-
 Resources/Locale/en-US/guidebook/guides.ftl   |   55 +
 Resources/Locale/en-US/info/rules.ftl         |    3 +
 Resources/Prototypes/Actions/borgs.yml        |    2 +-
 Resources/Prototypes/Actions/crit.yml         |    6 +-
 Resources/Prototypes/Actions/diona.yml        |    4 +-
 Resources/Prototypes/Actions/internals.yml    |    2 +-
 Resources/Prototypes/Actions/mech.yml         |    6 +-
 Resources/Prototypes/Actions/misc.yml         |    2 +-
 Resources/Prototypes/Actions/ninja.yml        |   12 +-
 Resources/Prototypes/Actions/polymorph.yml    |    8 +-
 Resources/Prototypes/Actions/psionics.yml     |   40 +-
 Resources/Prototypes/Actions/revenant.yml     |   10 +-
 Resources/Prototypes/Actions/speech.yml       |    2 +-
 Resources/Prototypes/Actions/spider.yml       |    4 +-
 Resources/Prototypes/Actions/types.yml        |   60 +-
 Resources/Prototypes/Alerts/categories.yml    |   38 +
 .../Prototypes/Body/Organs/Animal/animal.yml  |   12 +-
 .../Body/Organs/Animal/bloodsucker.yml        |    6 +-
 .../Body/Organs/Animal/ruminant.yml           |    2 +-
 .../Prototypes/Body/Organs/Friendstomach.yml  |    2 +-
 Resources/Prototypes/Body/Organs/arachnid.yml |    4 +-
 Resources/Prototypes/Body/Organs/diona.yml    |   12 +-
 Resources/Prototypes/Body/Organs/moth.yml     |    2 +-
 .../Prototypes/Body/Organs/reptilian.yml      |    2 +-
 Resources/Prototypes/Body/Parts/animal.yml    |    8 +-
 Resources/Prototypes/Body/Parts/rat.yml       |    2 +-
 Resources/Prototypes/Body/Parts/silicon.yml   |    2 +-
 .../Fills/Backpacks/StarterGear/backpack.yml  |   67 +-
 .../Fills/Backpacks/StarterGear/duffelbag.yml |   54 +-
 .../Fills/Backpacks/StarterGear/satchel.yml   |   56 +-
 .../Prototypes/Catalog/Fills/Crates/fun.yml   |   98 +
 .../Catalog/Fills/Crates/salvage.yml          |    2 +-
 .../Prototypes/Catalog/Fills/Lockers/misc.yml |  461 ++--
 .../DeltaV/Body/Organs/vulpkanin.yml          |    2 +-
 .../Fills/Backpacks/StarterGear/backpack.yml  |    6 +-
 .../Fills/Backpacks/StarterGear/duffelbag.yml |    6 +-
 .../Fills/Backpacks/StarterGear/satchel.yml   |    6 +-
 .../Clothing/Head/hardsuit-helmets.yml        |    8 +-
 .../Entities/Markers/Spawners/ghost_roles.yml |    2 +-
 .../DeltaV/Entities/Mobs/Player/human.yml     |    2 +-
 .../Entities/Mobs/Species/vulpkanin.yml       |    2 +-
 .../Objects/Misc/subdermal_implants.yml       |    2 +-
 .../Entities/Objects/Specific/Mail/mail.yml   |  120 +-
 .../Objects/Specific/Mail/mail_civilian.yml   |   24 +-
 .../Objects/Specific/Mail/mail_command.yml    |    6 +-
 .../Specific/Mail/mail_engineering.yml        |   14 +-
 .../Specific/Mail/mail_epistemology.yml       |    8 +-
 .../Objects/Specific/Mail/mail_medical.yml    |   14 +-
 .../Objects/Specific/Mail/mail_security.yml   |   14 +-
 .../Ammunition/Projectiles/replicated.yml     |    2 +-
 .../Guns/Ammunition/Projectiles/special.yml   |   14 +-
 .../Weapons/Guns/Projectiles/impacts.yml      |    2 +-
 .../Prototypes/DeltaV/GameRules/events.yml    |    8 +-
 .../Prototypes/DeltaV/GameRules/midround.yml  |    2 +-
 .../DeltaV/Objectives/paradox_anomaly.yml     |    6 +-
 .../Prototypes/DeltaV/Objectives/traitor.yml  |    6 +-
 .../DeltaV/Roles/Jobs/NPC/syndicateNPCs.yml   |    2 +-
 .../Prototypes/Entities/Clothing/Eyes/hud.yml |    2 +-
 .../Clothing/Head/base_clothinghead.yml       |    8 +-
 .../Clothing/Head/hardsuit-helmets.yml        |    2 +-
 .../Entities/Clothing/Head/hoods.yml          |   56 +-
 .../Clothing/Masks/base_clothingmask.yml      |    2 +-
 .../Entities/Clothing/Neck/misc.yml           |    2 +-
 .../Clothing/OuterClothing/wintercoats.yml    |    2 +-
 .../Entities/Clothing/Shoes/magboots.yml      |   10 +-
 .../Entities/Clothing/Shoes/misc.yml          |    2 +-
 .../Entities/Debugging/clicktest.yml          |    2 +-
 .../Entities/Debugging/debug_sweps.yml        |    2 +-
 .../Prototypes/Entities/Debugging/tippy.yml   |    2 +-
 .../Entities/Effects/ambient_sounds.yml       |    2 +-
 .../Entities/Effects/bluespace_flash.yml      |   14 +-
 .../Entities/Effects/chemistry_effects.yml    |   10 +-
 .../Entities/Effects/emp_effects.yml          |    4 +-
 .../Entities/Effects/exclamation.yml          |    4 +-
 .../Entities/Effects/explosion_light.yml      |    2 +-
 .../Prototypes/Entities/Effects/hearts.yml    |    2 +-
 .../Prototypes/Entities/Effects/lightning.yml |   12 +-
 .../Prototypes/Entities/Effects/mobspawn.yml  |    2 +-
 .../Prototypes/Entities/Effects/radiation.yml |    2 +-
 Resources/Prototypes/Entities/Effects/rcd.yml |   22 +-
 .../Prototypes/Entities/Effects/shuttle.yml   |   12 +
 .../Prototypes/Entities/Effects/sparks.yml    |    4 +-
 .../Entities/Effects/weapon_arc.yml           |   22 +-
 .../Markers/Spawners/Random/maintenance.yml   |  792 ++++---
 .../Entities/Markers/Spawners/corpses.yml     |    2 +-
 .../Entities/Markers/Spawners/ghost_roles.yml |    6 +-
 .../Entities/Markers/clientsideclone.yml      |    2 +-
 .../Entities/Markers/construction_ghost.yml   |    2 +-
 .../Entities/Markers/drag_shadow.yml          |    2 +-
 .../Entities/Markers/hover_entity.yml         |    2 +-
 .../Entities/Mobs/Corpses/corpses.yml         |    2 +-
 .../Prototypes/Entities/Mobs/NPCs/Rslimes.yml |    2 +-
 .../Prototypes/Entities/Mobs/NPCs/animals.yml |    2 +-
 .../Entities/Mobs/NPCs/regalrat.yml           |   14 +-
 .../Prototypes/Entities/Mobs/NPCs/slimes.yml  |    2 +-
 .../Prototypes/Entities/Mobs/NPCs/xenopet.yml |   14 +-
 .../Entities/Mobs/Player/admin_ghost.yml      |   14 +-
 .../Prototypes/Entities/Mobs/Player/diona.yml |    2 +-
 .../Entities/Mobs/Player/dragon.yml           |    8 +-
 .../Entities/Mobs/Player/guardian.yml         |    2 +-
 .../Prototypes/Entities/Mobs/Player/human.yml |    6 +-
 .../Prototypes/Entities/Mobs/Player/ipc.yml   |    2 +-
 .../Entities/Mobs/Player/observer.yml         |   12 +-
 .../Entities/Mobs/Player/replay_observer.yml  |    2 +-
 .../Entities/Mobs/Player/silicon.yml          |    2 +-
 .../Entities/Mobs/Species/arachne.yml         |    2 +-
 .../Entities/Mobs/Species/arachnid.yml        |    2 +-
 .../Entities/Mobs/Species/diona.yml           |    2 +-
 .../Entities/Mobs/Species/dwarf.yml           |    2 +-
 .../Entities/Mobs/Species/gingerbread.yml     |    2 +-
 .../Entities/Mobs/Species/harpy.yml           |    6 +-
 .../Entities/Mobs/Species/human.yml           |    2 +-
 .../Prototypes/Entities/Mobs/Species/moth.yml |    2 +-
 .../Entities/Mobs/Species/reptilian.yml       |    2 +-
 .../Entities/Mobs/Species/shadowkin.yml       |    2 +-
 .../Entities/Mobs/Species/skeleton.yml        |    2 +-
 .../Entities/Mobs/Species/slime.yml           |    2 +-
 .../Prototypes/Entities/Mobs/Species/vox.yml  |    2 +-
 .../Consumable/Food/Containers/box.yml        |    2 +-
 .../Objects/Consumable/Food/snacks.yml        |   60 +-
 .../Consumable/Smokeables/Vapes/vape.yml      |    2 +-
 .../Entities/Objects/Decoration/present.yml   |    2 +-
 .../Objects/Devices/chameleon_projector.yml   |    6 +-
 .../Objects/Devices/translator_implants.yml   |   22 +-
 .../Entities/Objects/Devices/translators.yml  |    8 +-
 .../Objects/Fun/Tabletop/backgammon.yml       |    2 +-
 .../Entities/Objects/Fun/Tabletop/base.yml    |    2 +-
 .../Objects/Fun/Tabletop/checkers.yml         |    2 +-
 .../Entities/Objects/Fun/Tabletop/chess.yml   |    2 +-
 .../Entities/Objects/Fun/Tabletop/dnd.yml     |   10 +-
 .../Entities/Objects/Fun/Tabletop/parchis.yml |    2 +-
 .../Prototypes/Entities/Objects/Fun/pai.yml   |    4 +-
 .../Entities/Objects/Fun/spray_paint.yml      |    2 +-
 .../Entities/Objects/Misc/buffering.yml       |    2 +-
 .../Objects/Misc/fire_extinguisher.yml        |    2 +-
 .../Entities/Objects/Misc/paper.yml           |    2 +-
 .../Objects/Misc/subdermal_implants.yml       |   30 +-
 .../Entities/Objects/Power/lights.yml         |    2 +-
 .../Objects/Specific/Chapel/bibles.yml        |    2 +-
 .../Objects/Specific/Forensics/forensics.yml  |    2 +-
 .../Objects/Specific/Janitorial/soap.yml      |    2 +-
 .../Objects/Specific/Janitorial/spray.yml     |    4 +-
 .../Objects/Specific/Mail/Items/misc.yml      |    6 +-
 .../Objects/Specific/Mail/base_mail_large.yml |    2 +-
 .../Specific/Robotics/borg_modules.yml        |    2 +-
 .../Objects/Specific/chemical-containers.yml  |   48 +-
 .../Entities/Objects/Tools/fulton.yml         |    2 +-
 .../Entities/Objects/Tools/glowstick.yml      |   12 +-
 .../Entities/Objects/Tools/jetpacks.yml       |    4 +-
 .../Entities/Objects/Weapons/Bombs/funny.yml  |    2 +-
 .../Ammunition/Projectiles/antimateriel.yml   |    2 +-
 .../Ammunition/Projectiles/caseless_rifle.yml |    6 +-
 .../Guns/Ammunition/Projectiles/grenade.yml   |    6 +-
 .../Ammunition/Projectiles/heavy_rifle.yml    |    4 +-
 .../Ammunition/Projectiles/light_rifle.yml    |   10 +-
 .../Guns/Ammunition/Projectiles/magnum.yml    |   12 +-
 .../Guns/Ammunition/Projectiles/pistol.yml    |   10 +-
 .../Guns/Ammunition/Projectiles/rifle.yml     |   10 +-
 .../Guns/Ammunition/Projectiles/shotgun.yml   |   22 +-
 .../Weapons/Guns/Projectiles/hitscan.yml      |    2 +-
 .../Weapons/Guns/Projectiles/impacts.yml      |    8 +-
 .../Weapons/Guns/Projectiles/magic.yml        |   24 +-
 .../Weapons/Guns/Projectiles/meteors.yml      |    2 +-
 .../Weapons/Guns/Projectiles/projectiles.yml  |   80 +-
 .../Objects/Weapons/Throwable/grenades.yml    |    4 +-
 .../Entities/Stations/nanotrasen.yml          |    8 +-
 .../Entities/Stations/syndicate.yml           |    2 +-
 .../Prototypes/Entities/Stations/test.yml     |    2 +-
 .../Entities/Structures/Furniture/chairs.yml  |    2 +
 .../Structures/Furniture/rollerbeds.yml       |    7 +
 .../Structures/Piping/Disposal/pipes.yml      |    2 +-
 .../Power/Generation/PA/particles.yml         |    4 +-
 .../Structures/Power/Generation/ame.yml       |    2 +-
 .../Power/Generation/generators.yml           |    2 +-
 .../Structures/Power/Generation/solar.yml     |    2 +-
 .../Structures/Power/Generation/teg.yml       |    2 +-
 .../Entities/Structures/Power/apc.yml         |    4 +-
 .../Entities/Structures/Power/substation.yml  |    4 +-
 .../Storage/Canisters/gas_canisters.yml       |   22 +-
 .../Storage/Crates/base_structurecrates.yml   |    4 +-
 .../Structures/Wallmounts/Signs/bar_sign.yml  |    2 +-
 .../Entities/Structures/Wallmounts/switch.yml |    4 +-
 .../Entities/Structures/Wallmounts/timer.yml  |    2 +-
 .../Prototypes/Entities/Virtual/beam.yml      |    2 +-
 .../Entities/Virtual/electrocution.yml        |    6 +-
 .../Entities/Virtual/stripping_hidden.yml     |    2 +-
 .../Prototypes/Entities/Virtual/tether.yml    |    2 +-
 .../Entities/Virtual/virtual_item.yml         |    2 +-
 .../Entities/World/Debris/asteroids.yml       |   16 +-
 .../Entities/World/Debris/wrecks.yml          |    6 +-
 Resources/Prototypes/Entities/World/chunk.yml |    2 +-
 .../Prototypes/GameRules/cargo_gifts.yml      |   20 +-
 Resources/Prototypes/GameRules/events.yml     |   60 +-
 Resources/Prototypes/GameRules/midround.yml   |    8 +-
 Resources/Prototypes/GameRules/roundstart.yml |   34 +-
 .../Prototypes/GameRules/unknown_shuttles.yml |   10 +-
 Resources/Prototypes/GameRules/variation.yml  |   16 +-
 Resources/Prototypes/Guidebook/rules.yml      |  362 ++++
 Resources/Prototypes/Guidebook/ss14.yml       |    1 +
 Resources/Prototypes/Magic/event_spells.yml   |    2 +-
 .../Prototypes/Magic/forcewall_spells.yml     |    2 +-
 Resources/Prototypes/Magic/knock_spell.yml    |    2 +-
 .../Prototypes/Magic/projectile_spells.yml    |    6 +-
 Resources/Prototypes/Magic/rune_spells.yml    |    8 +-
 Resources/Prototypes/Magic/smite_spells.yml   |    2 +-
 Resources/Prototypes/Magic/spawn_spells.yml   |    2 +-
 Resources/Prototypes/Magic/staves.yml         |    2 +-
 .../Prototypes/Magic/teleport_spells.yml      |    2 +-
 Resources/Prototypes/Magic/utility_spells.yml |    2 +-
 .../Prototypes/Nyanotrasen/Actions/types.yml  |    4 +-
 .../Fills/Backpacks/StarterGear/backpack.yml  |    2 +-
 .../Fills/Backpacks/StarterGear/duffelbag.yml |    2 +-
 .../Fills/Backpacks/StarterGear/satchel.yml   |    2 +-
 .../Nyanotrasen/Catalog/Fills/Books/lore.yml  |    2 +-
 .../Catalog/Fills/Paper/salvage_lore.yml      |    8 +-
 .../Clothing/Head/hardsuit-helmets.yml        |    6 +-
 .../Entities/Effects/lightning.yml            |    2 +-
 .../Entities/Markers/Spawners/ghost_roles.yml |    6 +-
 .../Entities/Mobs/Player/special.yml          |    2 +-
 .../Nyanotrasen/Entities/Mobs/Species/Oni.yml |    2 +-
 .../Entities/Mobs/Species/felinid.yml         |    2 +-
 .../Objects/Consumable/Food/ration.yml        |    2 +-
 .../Objects/Specific/Mail/base_mail.yml       |    2 +-
 .../Weapons/Guns/Projectiles/shotgun.yml      |    2 +-
 .../Nyanotrasen/GameRules/events.yml          |   30 +-
 .../Nyanotrasen/Objectives/traitor.yml        |    6 +-
 Resources/Prototypes/Objectives/dragon.yml    |    4 +-
 Resources/Prototypes/Objectives/ninja.yml     |   12 +-
 Resources/Prototypes/Objectives/thief.yml     |   82 +-
 Resources/Prototypes/Objectives/traitor.yml   |   42 +-
 .../Prototypes/Roles/Jobs/Civilian/mime.yml   |    2 +-
 .../Guidebook/ServerRules/BanDurations.xml    |   17 +
 .../Guidebook/ServerRules/BanTypes.xml        |   11 +
 .../ServerRules/CoreRules/RuleC0.xml          |   19 +
 .../ServerRules/CoreRules/RuleC10AHelp.xml    |   29 +
 .../CoreRules/RuleC11AhelpThreats.xml         |   20 +
 .../ServerRules/CoreRules/RuleC12MinAge.xml   |    6 +
 .../CoreRules/RuleC13CharacterNames.xml       |   66 +
 .../ServerRules/CoreRules/RuleC14ICinOOC.xml  |   13 +
 .../ServerRules/CoreRules/RuleC1Admins.xml    |    6 +
 .../ServerRules/CoreRules/RuleC2DBAD.xml      |    7 +
 .../ServerRules/CoreRules/RuleC3NoHate.xml    |   20 +
 .../ServerRules/CoreRules/RuleC4NoERP.xml     |   23 +
 .../ServerRules/CoreRules/RuleC5Metacomms.xml |   18 +
 .../CoreRules/RuleC6BanEvasion.xml            |   15 +
 .../CoreRules/RuleC7EnglishOnly.xml           |   10 +
 .../ServerRules/CoreRules/RuleC8Exploits.xml  |   12 +
 .../ServerRules/CoreRules/RuleC9Multikey.xml  |    7 +
 .../Guidebook/ServerRules/DefaultRules.xml    |    5 +
 .../Guidebook/ServerRules/README.txt          |    5 +
 .../Guidebook/ServerRules/RoleTypes.xml       |   21 +
 .../ServerRules/RoleplayRules/RuleR0.xml      |   26 +
 .../RoleplayRules/RuleR10Subordination.xml    |   26 +
 .../RuleR11-1AnimalEscalation.xml             |   36 +
 .../RoleplayRules/RuleR11-2ConflictTypes.xml  |   30 +
 .../RoleplayRules/RuleR11Escalation.xml       |   67 +
 .../RoleplayRules/RuleR12RoleAbandonment.xml  |   28 +
 .../RoleplayRules/RuleR13PerformRole.xml      |   26 +
 .../RoleplayRules/RuleR14SecComStandard.xml   |   37 +
 .../RoleplayRules/RuleR15SpaceLaw.xml         |   21 +
 .../RoleplayRules/RuleR1Silicons.xml          |    4 +
 .../RoleplayRules/RuleR2Familiars.xml         |    6 +
 .../RoleplayRules/RuleR3NormalRP.xml          |   20 +
 .../RoleplayRules/RuleR4Metashield.xml        |  103 +
 .../RoleplayRules/RuleR5Arrivals.xml          |   22 +
 .../RoleplayRules/RuleR6SelfAntag.xml         |   22 +
 .../RoleplayRules/RuleR7RoundStalling.xml     |   16 +
 .../RoleplayRules/RuleR8NoFriendlyAntag.xml   |   22 +
 .../RoleplayRules/RuleR9MassSabotage.xml      |   23 +
 .../ServerRules/SiliconRules/RuleS0.xml       |   15 +
 .../SiliconRules/RuleS10OrderConflicts.xml    |    9 +
 .../ServerRules/SiliconRules/RuleS1Laws.xml   |    6 +
 .../SiliconRules/RuleS2LawPriority.xml        |    9 +
 .../SiliconRules/RuleS3LawRedefinition.xml    |    8 +
 .../SiliconRules/RuleS4RequestChanges.xml     |    6 +
 .../SiliconRules/RuleS5FreeSilicon.xml        |    4 +
 .../SiliconRules/RuleS6UnreasonableOrders.xml |   22 +
 .../SiliconRules/RuleS7Consistency.xml        |    6 +
 .../RuleS8DefaultCrewDefinition.xml           |    4 +
 .../RuleS9DefaultHarmDefinition.xml           |   25 +
 .../SpaceLaw/SLControlledSubstances.xml       |   14 +
 .../ServerRules/SpaceLaw/SLRestrictedGear.xml |   21 +
 .../SpaceLaw/SLRestrictedWeapons.xml          |   11 +
 .../ServerRules/SpaceLaw/SpaceLaw.xml         |   67 +
 .../ServerRules/WizDenCoreOnlyRules.xml       |   26 +
 .../Guidebook/ServerRules/WizDenLRPRules.xml  |   65 +
 .../Guidebook/ServerRules/WizDenMRPRules.xml  |   65 +
 Resources/ServerInfo/Rules.txt                |   60 -
 .../Effects/medi_holo.rsi/medi_holo.png       |  Bin 0 -> 1286 bytes
 .../Textures/Effects/medi_holo.rsi/meta.json  |   26 +
 RobustToolbox                                 |    2 +-
 713 files changed, 11681 insertions(+), 4685 deletions(-)
 create mode 100644 Content.Client/Clickable/ClickableSystem.cs
 delete mode 100644 Content.Client/Info/InfoSystem.cs
 delete mode 100644 Content.Client/Info/RulesManager.cs
 create mode 100644 Content.Client/Shuttles/FtlArrivalOverlay.cs
 create mode 100644 Content.Client/Shuttles/Systems/ShuttleSystem.cs
 create mode 100644 Content.IntegrationTests/Tests/Buckle/BuckleDragTest.cs
 create mode 100644 Content.IntegrationTests/Tests/Buckle/BuckleTest.Interact.cs
 create mode 100644 Content.IntegrationTests/Tests/Movement/BuckleMovementTest.cs
 rename Content.IntegrationTests/Tests/{Interaction => Movement}/MovementTest.cs (93%)
 create mode 100644 Content.IntegrationTests/Tests/Movement/PullingTest.cs
 rename Content.IntegrationTests/Tests/{Slipping => Movement}/SlippingTest.cs (92%)
 rename Content.IntegrationTests/Tests/{Minds => Station}/JobTests.cs (100%)
 create mode 100644 Content.IntegrationTests/Tests/Storage/StorageInteractionTest.cs
 create mode 100644 Content.Server.Database/Migrations/Postgres/20240606065731_RemoveLastReadRules.Designer.cs
 create mode 100644 Content.Server.Database/Migrations/Postgres/20240606065731_RemoveLastReadRules.cs
 create mode 100644 Content.Server.Database/Migrations/Sqlite/20240606065717_RemoveLastReadRules.Designer.cs
 create mode 100644 Content.Server.Database/Migrations/Sqlite/20240606065717_RemoveLastReadRules.cs
 delete mode 100644 Content.Server/Destructible/Thresholds/MinMax.cs
 delete mode 100644 Content.Server/GameTicking/Rules/GameRulePrototype.cs
 delete mode 100644 Content.Server/Info/InfoSystem.cs
 delete mode 100644 Content.Server/Info/RulesManager.cs
 create mode 100644 Content.Server/Spawners/Components/EntityTableSpawnerComponent.cs
 delete mode 100644 Content.Shared/Alert/AlertCategory.cs
 create mode 100644 Content.Shared/Alert/AlertCategoryPrototype.cs
 delete mode 100644 Content.Shared/Alert/AlertType.cs
 create mode 100644 Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs
 create mode 100644 Content.Shared/Containers/EntityTableContainerFillComponent.cs
 create mode 100644 Content.Shared/Destructible/Thresholds/MinMax.cs
 create mode 100644 Content.Shared/EntityTable/EntitySelectors/AllSelector.cs
 create mode 100644 Content.Shared/EntityTable/EntitySelectors/EntSelector.cs
 create mode 100644 Content.Shared/EntityTable/EntitySelectors/EntityTableSelector.cs
 create mode 100644 Content.Shared/EntityTable/EntitySelectors/GroupSelector.cs
 create mode 100644 Content.Shared/EntityTable/EntitySelectors/NestedSelector.cs
 create mode 100644 Content.Shared/EntityTable/EntitySelectors/NoneSelector.cs
 create mode 100644 Content.Shared/EntityTable/EntityTablePrototype.cs
 create mode 100644 Content.Shared/EntityTable/EntityTableSystem.cs
 create mode 100644 Content.Shared/EntityTable/ValueSelector/ConstantNumberSelector.cs
 create mode 100644 Content.Shared/EntityTable/ValueSelector/NumberSelector.cs
 create mode 100644 Content.Shared/EntityTable/ValueSelector/RangeNumberSelector.cs
 rename {Content.Server => Content.Shared}/GameTicking/Components/ActiveGameRuleComponent.cs (67%)
 rename {Content.Server => Content.Shared}/GameTicking/Components/DelayedStartRuleComponent.cs (91%)
 rename {Content.Server => Content.Shared}/GameTicking/Components/EndedGameRuleComponent.cs (61%)
 rename {Content.Server => Content.Shared}/GameTicking/Components/GameRuleComponent.cs (92%)
 create mode 100644 Content.Shared/Info/RulesMessages.cs
 delete mode 100644 Content.Shared/Info/SharedInfo.cs
 delete mode 100644 Content.Shared/Info/SharedRulesManager.cs
 rename {Content.Server => Content.Shared}/Shuttles/Components/FTLComponent.cs (81%)
 create mode 100644 Content.Shared/Shuttles/Components/FtlVisualizerComponent.cs
 create mode 100644 Resources/Prototypes/Alerts/categories.yml
 create mode 100644 Resources/Prototypes/Entities/Effects/shuttle.yml
 create mode 100644 Resources/Prototypes/Guidebook/rules.yml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/BanDurations.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/BanTypes.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC0.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC10AHelp.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC11AhelpThreats.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC12MinAge.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC13CharacterNames.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC14ICinOOC.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC1Admins.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC2DBAD.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC3NoHate.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC4NoERP.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC5Metacomms.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC6BanEvasion.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC7EnglishOnly.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC8Exploits.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC9Multikey.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/DefaultRules.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/README.txt
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/RoleTypes.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR0.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR10Subordination.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR11-1AnimalEscalation.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR11-2ConflictTypes.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR11Escalation.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR12RoleAbandonment.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR13PerformRole.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR14SecComStandard.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR15SpaceLaw.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR1Silicons.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR2Familiars.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR3NormalRP.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR4Metashield.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR5Arrivals.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR6SelfAntag.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR7RoundStalling.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR8NoFriendlyAntag.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR9MassSabotage.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS0.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS10OrderConflicts.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS1Laws.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS2LawPriority.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS3LawRedefinition.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS4RequestChanges.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS5FreeSilicon.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS6UnreasonableOrders.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS7Consistency.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS8DefaultCrewDefinition.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS9DefaultHarmDefinition.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/SpaceLaw/SLControlledSubstances.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/SpaceLaw/SLRestrictedGear.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/SpaceLaw/SLRestrictedWeapons.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/SpaceLaw/SpaceLaw.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/WizDenCoreOnlyRules.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/WizDenLRPRules.xml
 create mode 100644 Resources/ServerInfo/Guidebook/ServerRules/WizDenMRPRules.xml
 delete mode 100644 Resources/ServerInfo/Rules.txt
 create mode 100644 Resources/Textures/Effects/medi_holo.rsi/medi_holo.png
 create mode 100644 Resources/Textures/Effects/medi_holo.rsi/meta.json

diff --git a/Content.Client/Alerts/ClientAlertsSystem.cs b/Content.Client/Alerts/ClientAlertsSystem.cs
index 223bf7876ac..359c8957f9d 100644
--- a/Content.Client/Alerts/ClientAlertsSystem.cs
+++ b/Content.Client/Alerts/ClientAlertsSystem.cs
@@ -91,7 +91,7 @@ private void OnPlayerDetached(EntityUid uid, AlertsComponent component, LocalPla
         ClearAlerts?.Invoke(this, EventArgs.Empty);
     }
 
-    public void AlertClicked(AlertType alertType)
+    public void AlertClicked(ProtoId<AlertPrototype> alertType)
     {
         RaiseNetworkEvent(new ClickAlertEvent(alertType));
     }
diff --git a/Content.Client/Audio/AmbientSoundSystem.cs b/Content.Client/Audio/AmbientSoundSystem.cs
index 0206017baef..f4e755a013c 100644
--- a/Content.Client/Audio/AmbientSoundSystem.cs
+++ b/Content.Client/Audio/AmbientSoundSystem.cs
@@ -303,7 +303,11 @@ private void ProcessNearbyAmbience(TransformComponent playerXform)
                     .WithMaxDistance(comp.Range);
 
                 var stream = _audio.PlayEntity(comp.Sound, Filter.Local(), uid, false, audioParams);
-                _playingSounds[comp] = (stream.Value.Entity, comp.Sound, key);
+
+                if (stream == null)
+                    continue;
+
+                _playingSounds[comp] = (stream!.Value.Entity, comp.Sound, key);
                 playingCount++;
 
                 if (_playingSounds.Count >= _maxAmbientCount)
diff --git a/Content.Client/Audio/ClientGlobalSoundSystem.cs b/Content.Client/Audio/ClientGlobalSoundSystem.cs
index 7c77865f741..6619285a96a 100644
--- a/Content.Client/Audio/ClientGlobalSoundSystem.cs
+++ b/Content.Client/Audio/ClientGlobalSoundSystem.cs
@@ -67,7 +67,7 @@ private void PlayAdminSound(AdminSoundEvent soundEvent)
         if(!_adminAudioEnabled) return;
 
         var stream = _audio.PlayGlobal(soundEvent.Filename, Filter.Local(), false, soundEvent.AudioParams);
-        _adminAudio.Add(stream.Value.Entity);
+        _adminAudio.Add(stream!.Value.Entity);
     }
 
     private void PlayStationEventMusic(StationEventMusicEvent soundEvent)
@@ -76,7 +76,7 @@ private void PlayStationEventMusic(StationEventMusicEvent soundEvent)
         if(!_eventAudioEnabled || _eventAudio.ContainsKey(soundEvent.Type)) return;
 
         var stream = _audio.PlayGlobal(soundEvent.Filename, Filter.Local(), false, soundEvent.AudioParams);
-        _eventAudio.Add(soundEvent.Type, stream.Value.Entity);
+        _eventAudio.Add(soundEvent.Type, stream!.Value.Entity);
     }
 
     private void PlayGameSound(GameGlobalSoundEvent soundEvent)
diff --git a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs
index 84b787a4ec9..58cd026da49 100644
--- a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs
+++ b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs
@@ -210,7 +210,7 @@ private void UpdateAmbientMusic()
             track.ToString(),
             Filter.Local(),
             false,
-            AudioParams.Default.WithVolume(_musicProto.Sound.Params.Volume + _volumeSlider));
+            AudioParams.Default.WithVolume(_musicProto.Sound.Params.Volume + _volumeSlider))!;
 
         _ambientMusicStream = strim.Value.Entity;
 
diff --git a/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs b/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs
index 92c5b7a4191..60b81b1c2df 100644
--- a/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs
+++ b/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs
@@ -185,7 +185,11 @@ private void PlaySoundtrack(string soundtrackFilename)
             false,
             _lobbySoundtrackParams.WithVolume(_lobbySoundtrackParams.Volume + SharedAudioSystem.GainToVolume(_configManager.GetCVar(CCVars.LobbyMusicVolume)))
         );
-        if (playResult.Value.Entity == default)
+
+        if (playResult == null)
+            return;
+
+        if (playResult!.Value.Entity == default)
         {
             _sawmill.Warning(
                 $"Tried to play lobby soundtrack '{{Filename}}' using {nameof(SharedAudioSystem)}.{nameof(SharedAudioSystem.PlayGlobal)} but it returned default value of EntityUid!",
diff --git a/Content.Client/Buckle/BuckleSystem.cs b/Content.Client/Buckle/BuckleSystem.cs
index d4614210d9f..4429996aca3 100644
--- a/Content.Client/Buckle/BuckleSystem.cs
+++ b/Content.Client/Buckle/BuckleSystem.cs
@@ -3,6 +3,7 @@
 using Content.Shared.Buckle.Components;
 using Content.Shared.Rotation;
 using Robust.Client.GameObjects;
+using Robust.Shared.GameStates;
 
 namespace Content.Client.Buckle;
 
@@ -14,40 +15,63 @@ public override void Initialize()
     {
         base.Initialize();
 
-        SubscribeLocalEvent<BuckleComponent, AfterAutoHandleStateEvent>(OnBuckleAfterAutoHandleState);
+        SubscribeLocalEvent<BuckleComponent, ComponentHandleState>(OnHandleState);
         SubscribeLocalEvent<BuckleComponent, AppearanceChangeEvent>(OnAppearanceChange);
+        SubscribeLocalEvent<StrapComponent, MoveEvent>(OnStrapMoveEvent);
     }
 
-    private void OnBuckleAfterAutoHandleState(EntityUid uid, BuckleComponent component, ref AfterAutoHandleStateEvent args)
+    private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveEvent args)
     {
-        ActionBlocker.UpdateCanMove(uid);
+        // I'm moving this to the client-side system, but for the sake of posterity let's keep this comment:
+        // > This is mega cursed. Please somebody save me from Mr Buckle's wild ride
 
-        if (!TryComp<SpriteComponent>(uid, out var ownerSprite))
+        // The nice thing is its still true, this is quite cursed, though maybe not omega cursed anymore.
+        // This code is garbage, it doesn't work with rotated viewports. I need to finally get around to reworking
+        // sprite rendering for entity layers & direction dependent sorting.
+
+        if (args.NewRotation == args.OldRotation)
             return;
 
-        // Adjust draw depth when the chair faces north so that the seat back is drawn over the player.
-        // Reset the draw depth when rotated in any other direction.
-        // TODO when ECSing, make this a visualizer
-        // This code was written before rotatable viewports were introduced, so hard-coding Direction.North
-        // and comparing it against LocalRotation now breaks this in other rotations. This is a FIXME, but
-        // better to get it working for most people before we look at a more permanent solution.
-        if (component is { Buckled: true, LastEntityBuckledTo: { } } &&
-            Transform(component.LastEntityBuckledTo.Value).LocalRotation.GetCardinalDir() == Direction.North &&
-            TryComp<SpriteComponent>(component.LastEntityBuckledTo, out var buckledSprite))
-        {
-            component.OriginalDrawDepth ??= ownerSprite.DrawDepth;
-            ownerSprite.DrawDepth = buckledSprite.DrawDepth - 1;
+        if (!TryComp<SpriteComponent>(uid, out var strapSprite))
             return;
-        }
 
-        // If here, we're not turning north and should restore the saved draw depth.
-        if (component.OriginalDrawDepth.HasValue)
+        var isNorth = Transform(uid).LocalRotation.GetCardinalDir() == Direction.North;
+        foreach (var buckledEntity in component.BuckledEntities)
         {
-            ownerSprite.DrawDepth = component.OriginalDrawDepth.Value;
-            component.OriginalDrawDepth = null;
+            if (!TryComp<BuckleComponent>(buckledEntity, out var buckle))
+                continue;
+
+            if (!TryComp<SpriteComponent>(buckledEntity, out var buckledSprite))
+                continue;
+
+            if (isNorth)
+            {
+                buckle.OriginalDrawDepth ??= buckledSprite.DrawDepth;
+                buckledSprite.DrawDepth = strapSprite.DrawDepth - 1;
+            }
+            else if (buckle.OriginalDrawDepth.HasValue)
+            {
+                buckledSprite.DrawDepth = buckle.OriginalDrawDepth.Value;
+                buckle.OriginalDrawDepth = null;
+            }
         }
     }
 
+    private void OnHandleState(Entity<BuckleComponent> ent, ref ComponentHandleState args)
+    {
+        if (args.Current is not BuckleState state)
+            return;
+
+        ent.Comp.DontCollide = state.DontCollide;
+        ent.Comp.BuckleTime = state.BuckleTime;
+        var strapUid = EnsureEntity<BuckleComponent>(state.BuckledTo, ent);
+
+        SetBuckledTo(ent, strapUid == null ? null : new (strapUid.Value, null));
+
+        var (uid, component) = ent;
+
+    }
+
     private void OnAppearanceChange(EntityUid uid, BuckleComponent component, ref AppearanceChangeEvent args)
     {
         if (!TryComp<RotationVisualsComponent>(uid, out var rotVisuals)
diff --git a/Content.Client/Clickable/ClickableComponent.cs b/Content.Client/Clickable/ClickableComponent.cs
index 6f75df46830..da81ed4c841 100644
--- a/Content.Client/Clickable/ClickableComponent.cs
+++ b/Content.Client/Clickable/ClickableComponent.cs
@@ -1,145 +1,17 @@
-using System.Numerics;
-using Robust.Client.GameObjects;
-using Robust.Client.Graphics;
-using Robust.Client.Utility;
-using Robust.Shared.Graphics;
-using static Robust.Client.GameObjects.SpriteComponent;
-using Direction = Robust.Shared.Maths.Direction;
+namespace Content.Client.Clickable;
 
-namespace Content.Client.Clickable
+[RegisterComponent]
+public sealed partial class ClickableComponent : Component
 {
-    [RegisterComponent]
-    public sealed partial class ClickableComponent : Component
-    {
-        [Dependency] private readonly IClickMapManager _clickMapManager = default!;
-
-        [DataField("bounds")] public DirBoundData? Bounds;
-
-        /// <summary>
-        /// Used to check whether a click worked. Will first check if the click falls inside of some explicit bounding
-        /// boxes (see <see cref="Bounds"/>). If that fails, attempts to use automatically generated click maps.
-        /// </summary>
-        /// <param name="worldPos">The world position that was clicked.</param>
-        /// <param name="drawDepth">
-        /// The draw depth for the sprite that captured the click.
-        /// </param>
-        /// <returns>True if the click worked, false otherwise.</returns>
-        public bool CheckClick(SpriteComponent sprite, TransformComponent transform, EntityQuery<TransformComponent> xformQuery, Vector2 worldPos, IEye eye, out int drawDepth, out uint renderOrder, out float bottom)
-        {
-            if (!sprite.Visible)
-            {
-                drawDepth = default;
-                renderOrder = default;
-                bottom = default;
-                return false;
-            }
-
-            drawDepth = sprite.DrawDepth;
-            renderOrder = sprite.RenderOrder;
-            var (spritePos, spriteRot) = transform.GetWorldPositionRotation(xformQuery);
-            var spriteBB = sprite.CalculateRotatedBoundingBox(spritePos, spriteRot, eye.Rotation);
-            bottom = Matrix3Helpers.CreateRotation(eye.Rotation).TransformBox(spriteBB).Bottom;
-
-            Matrix3x2.Invert(sprite.GetLocalMatrix(), out var invSpriteMatrix);
-
-            // This should have been the rotation of the sprite relative to the screen, but this is not the case with no-rot or directional sprites.
-            var relativeRotation = (spriteRot + eye.Rotation).Reduced().FlipPositive();
-
-            Angle cardinalSnapping = sprite.SnapCardinals ? relativeRotation.GetCardinalDir().ToAngle() : Angle.Zero;
-
-            // First we get `localPos`, the clicked location in the sprite-coordinate frame.
-            var entityXform = Matrix3Helpers.CreateInverseTransform(transform.WorldPosition, sprite.NoRotation ? -eye.Rotation : spriteRot - cardinalSnapping);
-            var localPos = Vector2.Transform(Vector2.Transform(worldPos, entityXform), invSpriteMatrix);
-
-            // Check explicitly defined click-able bounds
-            if (CheckDirBound(sprite, relativeRotation, localPos))
-                return true;
-
-            // Next check each individual sprite layer using automatically computed click maps.
-            foreach (var spriteLayer in sprite.AllLayers)
-            {
-                if (!spriteLayer.Visible || spriteLayer is not Layer layer)
-                    continue;
-
-                // Check the layer's texture, if it has one
-                if (layer.Texture != null)
-                {
-                    // Convert to image coordinates
-                    var imagePos = (Vector2i) (localPos * EyeManager.PixelsPerMeter * new Vector2(1, -1) + layer.Texture.Size / 2f);
-
-                    if (_clickMapManager.IsOccluding(layer.Texture, imagePos))
-                        return true;
-                }
-
-                // Either we weren't clicking on the texture, or there wasn't one. In which case: check the RSI next
-                if (layer.ActualRsi is not { } rsi || !rsi.TryGetState(layer.State, out var rsiState))
-                    continue;
-
-                var dir = Layer.GetDirection(rsiState.RsiDirections, relativeRotation);
+    [DataField] public DirBoundData? Bounds;
 
-                // convert to layer-local coordinates
-                layer.GetLayerDrawMatrix(dir, out var matrix);
-                Matrix3x2.Invert(matrix, out var inverseMatrix);
-                var layerLocal = Vector2.Transform(localPos, inverseMatrix);
-
-                // Convert to image coordinates
-                var layerImagePos = (Vector2i) (layerLocal * EyeManager.PixelsPerMeter * new Vector2(1, -1) + rsiState.Size / 2f);
-
-                // Next, to get the right click map we need the "direction" of this layer that is actually being used to draw the sprite on the screen.
-                // This **can** differ from the dir defined before, but can also just be the same.
-                if (sprite.EnableDirectionOverride)
-                    dir = sprite.DirectionOverride.Convert(rsiState.RsiDirections);
-                dir = dir.OffsetRsiDir(layer.DirOffset);
-
-                if (_clickMapManager.IsOccluding(layer.ActualRsi!, layer.State, dir, layer.AnimationFrame, layerImagePos))
-                    return true;
-            }
-
-            drawDepth = default;
-            renderOrder = default;
-            bottom = default;
-            return false;
-        }
-
-        public bool CheckDirBound(SpriteComponent sprite, Angle relativeRotation, Vector2 localPos)
-        {
-            if (Bounds == null)
-                return false;
-
-            // These explicit bounds only work for either 1 or 4 directional sprites.
-
-            // This would be the orientation of a 4-directional sprite.
-            var direction = relativeRotation.GetCardinalDir();
-
-            var modLocalPos = sprite.NoRotation
-                ? localPos
-                : direction.ToAngle().RotateVec(localPos);
-
-            // First, check the bounding box that is valid for all orientations
-            if (Bounds.All.Contains(modLocalPos))
-                return true;
-
-            // Next, get and check the appropriate bounding box for the current sprite orientation
-            var boundsForDir = (sprite.EnableDirectionOverride ? sprite.DirectionOverride : direction) switch
-            {
-                Direction.East => Bounds.East,
-                Direction.North => Bounds.North,
-                Direction.South => Bounds.South,
-                Direction.West => Bounds.West,
-                _ => throw new InvalidOperationException()
-            };
-
-            return boundsForDir.Contains(modLocalPos);
-        }
-
-        [DataDefinition]
-        public sealed partial class DirBoundData
-        {
-            [DataField("all")] public Box2 All;
-            [DataField("north")] public Box2 North;
-            [DataField("south")] public Box2 South;
-            [DataField("east")] public Box2 East;
-            [DataField("west")] public Box2 West;
-        }
+    [DataDefinition]
+    public sealed partial class DirBoundData
+    {
+        [DataField] public Box2 All;
+        [DataField] public Box2 North;
+        [DataField] public Box2 South;
+        [DataField] public Box2 East;
+        [DataField] public Box2 West;
     }
 }
diff --git a/Content.Client/Clickable/ClickableSystem.cs b/Content.Client/Clickable/ClickableSystem.cs
new file mode 100644
index 00000000000..15d13df625c
--- /dev/null
+++ b/Content.Client/Clickable/ClickableSystem.cs
@@ -0,0 +1,168 @@
+using System.Numerics;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.Utility;
+using Robust.Shared.Graphics;
+
+namespace Content.Client.Clickable;
+
+/// <summary>
+/// Handles click detection for sprites.
+/// </summary>
+public sealed class ClickableSystem : EntitySystem
+{
+    [Dependency] private readonly IClickMapManager _clickMapManager = default!;
+    [Dependency] private readonly SharedTransformSystem _transforms = default!;
+    [Dependency] private readonly SpriteSystem _sprites = default!;
+
+    private EntityQuery<ClickableComponent> _clickableQuery;
+    private EntityQuery<TransformComponent> _xformQuery;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        _clickableQuery = GetEntityQuery<ClickableComponent>();
+        _xformQuery = GetEntityQuery<TransformComponent>();
+    }
+
+    /// <summary>
+    /// Used to check whether a click worked. Will first check if the click falls inside of some explicit bounding
+    /// boxes (see <see cref="Bounds"/>). If that fails, attempts to use automatically generated click maps.
+    /// </summary>
+    /// <param name="worldPos">The world position that was clicked.</param>
+    /// <param name="drawDepth">
+    /// The draw depth for the sprite that captured the click.
+    /// </param>
+    /// <returns>True if the click worked, false otherwise.</returns>
+    public bool CheckClick(Entity<ClickableComponent?, SpriteComponent, TransformComponent?> entity, Vector2 worldPos, IEye eye, out int drawDepth, out uint renderOrder, out float bottom)
+    {
+        if (!_clickableQuery.Resolve(entity.Owner, ref entity.Comp1, false))
+        {
+            drawDepth = default;
+            renderOrder = default;
+            bottom = default;
+            return false;
+        }
+
+        if (!_xformQuery.Resolve(entity.Owner, ref entity.Comp3))
+        {
+            drawDepth = default;
+            renderOrder = default;
+            bottom = default;
+            return false;
+        }
+
+        var sprite = entity.Comp2;
+        var transform = entity.Comp3;
+
+        if (!sprite.Visible)
+        {
+            drawDepth = default;
+            renderOrder = default;
+            bottom = default;
+            return false;
+        }
+
+        drawDepth = sprite.DrawDepth;
+        renderOrder = sprite.RenderOrder;
+        var (spritePos, spriteRot) = _transforms.GetWorldPositionRotation(transform);
+        var spriteBB = sprite.CalculateRotatedBoundingBox(spritePos, spriteRot, eye.Rotation);
+        bottom = Matrix3Helpers.CreateRotation(eye.Rotation).TransformBox(spriteBB).Bottom;
+
+        Matrix3x2.Invert(sprite.GetLocalMatrix(), out var invSpriteMatrix);
+
+        // This should have been the rotation of the sprite relative to the screen, but this is not the case with no-rot or directional sprites.
+        var relativeRotation = (spriteRot + eye.Rotation).Reduced().FlipPositive();
+
+        var cardinalSnapping = sprite.SnapCardinals ? relativeRotation.GetCardinalDir().ToAngle() : Angle.Zero;
+
+        // First we get `localPos`, the clicked location in the sprite-coordinate frame.
+        var entityXform = Matrix3Helpers.CreateInverseTransform(spritePos, sprite.NoRotation ? -eye.Rotation : spriteRot - cardinalSnapping);
+        var localPos = Vector2.Transform(Vector2.Transform(worldPos, entityXform), invSpriteMatrix);
+
+        // Check explicitly defined click-able bounds
+        if (CheckDirBound((entity.Owner, entity.Comp1, entity.Comp2), relativeRotation, localPos))
+            return true;
+
+        // Next check each individual sprite layer using automatically computed click maps.
+        foreach (var spriteLayer in sprite.AllLayers)
+        {
+            if (spriteLayer is not SpriteComponent.Layer layer || !_sprites.IsVisible(layer))
+            {
+                continue;
+            }
+
+            // Check the layer's texture, if it has one
+            if (layer.Texture != null)
+            {
+                // Convert to image coordinates
+                var imagePos = (Vector2i) (localPos * EyeManager.PixelsPerMeter * new Vector2(1, -1) + layer.Texture.Size / 2f);
+
+                if (_clickMapManager.IsOccluding(layer.Texture, imagePos))
+                    return true;
+            }
+
+            // Either we weren't clicking on the texture, or there wasn't one. In which case: check the RSI next
+            if (layer.ActualRsi is not { } rsi || !rsi.TryGetState(layer.State, out var rsiState))
+                continue;
+
+            var dir = SpriteComponent.Layer.GetDirection(rsiState.RsiDirections, relativeRotation);
+
+            // convert to layer-local coordinates
+            layer.GetLayerDrawMatrix(dir, out var matrix);
+            Matrix3x2.Invert(matrix, out var inverseMatrix);
+            var layerLocal = Vector2.Transform(localPos, inverseMatrix);
+
+            // Convert to image coordinates
+            var layerImagePos = (Vector2i) (layerLocal * EyeManager.PixelsPerMeter * new Vector2(1, -1) + rsiState.Size / 2f);
+
+            // Next, to get the right click map we need the "direction" of this layer that is actually being used to draw the sprite on the screen.
+            // This **can** differ from the dir defined before, but can also just be the same.
+            if (sprite.EnableDirectionOverride)
+                dir = sprite.DirectionOverride.Convert(rsiState.RsiDirections);
+            dir = dir.OffsetRsiDir(layer.DirOffset);
+
+            if (_clickMapManager.IsOccluding(layer.ActualRsi!, layer.State, dir, layer.AnimationFrame, layerImagePos))
+                return true;
+        }
+
+        drawDepth = default;
+        renderOrder = default;
+        bottom = default;
+        return false;
+    }
+
+    public bool CheckDirBound(Entity<ClickableComponent, SpriteComponent> entity, Angle relativeRotation, Vector2 localPos)
+    {
+        var clickable = entity.Comp1;
+        var sprite = entity.Comp2;
+
+        if (clickable.Bounds == null)
+            return false;
+
+        // These explicit bounds only work for either 1 or 4 directional sprites.
+
+        // This would be the orientation of a 4-directional sprite.
+        var direction = relativeRotation.GetCardinalDir();
+
+        var modLocalPos = sprite.NoRotation
+            ? localPos
+            : direction.ToAngle().RotateVec(localPos);
+
+        // First, check the bounding box that is valid for all orientations
+        if (clickable.Bounds.All.Contains(modLocalPos))
+            return true;
+
+        // Next, get and check the appropriate bounding box for the current sprite orientation
+        var boundsForDir = (sprite.EnableDirectionOverride ? sprite.DirectionOverride : direction) switch
+        {
+            Direction.East => clickable.Bounds.East,
+            Direction.North => clickable.Bounds.North,
+            Direction.South => clickable.Bounds.South,
+            Direction.West => clickable.Bounds.West,
+            _ => throw new InvalidOperationException()
+        };
+
+        return boundsForDir.Contains(modLocalPos);
+    }
+}
diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs
index 1ab213ae0a3..bf5f021be3d 100644
--- a/Content.Client/Entry/EntryPoint.cs
+++ b/Content.Client/Entry/EntryPoint.cs
@@ -8,7 +8,6 @@
 using Content.Client.Fullscreen;
 using Content.Client.GhostKick;
 using Content.Client.Guidebook;
-using Content.Client.Info;
 using Content.Client.Input;
 using Content.Client.IoC;
 using Content.Client.Launcher;
@@ -54,7 +53,6 @@ public sealed class EntryPoint : GameClient
         [Dependency] private readonly IScreenshotHook _screenshotHook = default!;
         [Dependency] private readonly FullscreenHook _fullscreenHook = default!;
         [Dependency] private readonly ChangelogManager _changelogManager = default!;
-        [Dependency] private readonly RulesManager _rulesManager = default!;
         [Dependency] private readonly ViewportManager _viewportManager = default!;
         [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
         [Dependency] private readonly IInputManager _inputManager = default!;
@@ -131,7 +129,6 @@ public override void Init()
             _screenshotHook.Initialize();
             _fullscreenHook.Initialize();
             _changelogManager.Initialize();
-            _rulesManager.Initialize();
             _viewportManager.Initialize();
             _ghostKick.Initialize();
             _extendedDisconnectInformation.Initialize();
diff --git a/Content.Client/Explosion/ExplosionOverlay.cs b/Content.Client/Explosion/ExplosionOverlay.cs
index 8cf7447a5d8..f8f8ae53623 100644
--- a/Content.Client/Explosion/ExplosionOverlay.cs
+++ b/Content.Client/Explosion/ExplosionOverlay.cs
@@ -29,6 +29,7 @@ public ExplosionOverlay()
 
     protected override void Draw(in OverlayDrawArgs args)
     {
+        var appearanceSystem = _entMan.System<SharedAppearanceSystem>();
         var drawHandle = args.WorldHandle;
         drawHandle.UseShader(_shader);
 
@@ -41,7 +42,7 @@ protected override void Draw(in OverlayDrawArgs args)
             if (visuals.Epicenter.MapId != args.MapId)
                 continue;
 
-            if (!appearance.TryGetData(ExplosionAppearanceData.Progress, out int index))
+            if (!appearanceSystem.TryGetData(appearance.Owner, ExplosionAppearanceData.Progress, out int index))
                 continue;
 
             index = Math.Min(index, visuals.Intensity.Count - 1);
diff --git a/Content.Client/Gameplay/GameplayStateBase.cs b/Content.Client/Gameplay/GameplayStateBase.cs
index 6236cd8e958..315c053d935 100644
--- a/Content.Client/Gameplay/GameplayStateBase.cs
+++ b/Content.Client/Gameplay/GameplayStateBase.cs
@@ -2,6 +2,7 @@
 using System.Numerics;
 using Content.Client.Clickable;
 using Content.Client.UserInterface;
+using Content.Client.Viewport;
 using Content.Shared.Input;
 using Robust.Client.ComponentTrees;
 using Robust.Client.GameObjects;
@@ -13,11 +14,13 @@
 using Robust.Client.UserInterface.Controls;
 using Robust.Client.UserInterface.CustomControls;
 using Robust.Shared.Console;
+using Robust.Shared.Graphics;
 using Robust.Shared.Input;
 using Robust.Shared.Input.Binding;
 using Robust.Shared.Map;
 using Robust.Shared.Player;
 using Robust.Shared.Timing;
+using YamlDotNet.Serialization.TypeInspectors;
 
 namespace Content.Client.Gameplay
 {
@@ -98,7 +101,15 @@ private bool HandleInspect(ICommonSession? session, EntityCoordinates coords, En
 
         public EntityUid? GetClickedEntity(MapCoordinates coordinates)
         {
-            var first = GetClickableEntities(coordinates).FirstOrDefault();
+            return GetClickedEntity(coordinates, _eyeManager.CurrentEye);
+        }
+
+        public EntityUid? GetClickedEntity(MapCoordinates coordinates, IEye? eye)
+        {
+            if (eye == null)
+                return null;
+
+            var first = GetClickableEntities(coordinates, eye).FirstOrDefault();
             return first.IsValid() ? first : null;
         }
 
@@ -109,6 +120,20 @@ public IEnumerable<EntityUid> GetClickableEntities(EntityCoordinates coordinates
 
         public IEnumerable<EntityUid> GetClickableEntities(MapCoordinates coordinates)
         {
+            return GetClickableEntities(coordinates, _eyeManager.CurrentEye);
+        }
+
+        public IEnumerable<EntityUid> GetClickableEntities(MapCoordinates coordinates, IEye? eye)
+        {
+            /*
+             * TODO:
+             * 1. Stuff like MeleeWeaponSystem need an easy way to hook into viewport specific entities / entities under mouse
+             * 2. Cleanup the mess around InteractionOutlineSystem + below the keybind click detection.
+             */
+
+            if (eye == null)
+                return Array.Empty<EntityUid>();
+
             // Find all the entities intersecting our click
             var spriteTree = _entityManager.EntitySysManager.GetEntitySystem<SpriteTreeSystem>();
             var entities = spriteTree.QueryAabb(coordinates.MapId, Box2.CenteredAround(coordinates.Position, new Vector2(1, 1)));
@@ -116,15 +141,12 @@ public IEnumerable<EntityUid> GetClickableEntities(MapCoordinates coordinates)
             // Check the entities against whether or not we can click them
             var foundEntities = new List<(EntityUid, int, uint, float)>(entities.Count);
             var clickQuery = _entityManager.GetEntityQuery<ClickableComponent>();
-            var xformQuery = _entityManager.GetEntityQuery<TransformComponent>();
-
-            // TODO: Smelly
-            var eye = _eyeManager.CurrentEye;
+            var clickables = _entityManager.System<ClickableSystem>();
 
             foreach (var entity in entities)
             {
                 if (clickQuery.TryGetComponent(entity.Uid, out var component) &&
-                    component.CheckClick(entity.Component, entity.Transform, xformQuery, coordinates.Position, eye,  out var drawDepthClicked, out var renderOrder, out var bottom))
+                    clickables.CheckClick((entity.Uid, component, entity.Component, entity.Transform), coordinates.Position, eye,  out var drawDepthClicked, out var renderOrder, out var bottom))
                 {
                     foundEntities.Add((entity.Uid, drawDepthClicked, renderOrder, bottom));
                 }
@@ -187,7 +209,15 @@ protected virtual void OnKeyBindStateChanged(ViewportBoundKeyEventArgs args)
             if (args.Viewport is IViewportControl vp)
             {
                 var mousePosWorld = vp.PixelToMap(kArgs.PointerLocation.Position);
-                entityToClick = GetClickedEntity(mousePosWorld);
+
+                if (vp is ScalingViewport svp)
+                {
+                    entityToClick = GetClickedEntity(mousePosWorld, svp.Eye);
+                }
+                else
+                {
+                    entityToClick = GetClickedEntity(mousePosWorld);
+                }
 
                 coordinates = _mapManager.TryFindGridAt(mousePosWorld, out _, out var grid) ?
                     grid.MapToGrid(mousePosWorld) :
diff --git a/Content.Client/Guidebook/Controls/GuidebookWindow.xaml b/Content.Client/Guidebook/Controls/GuidebookWindow.xaml
index 8dbfde3c475..cc6cc6e82b0 100644
--- a/Content.Client/Guidebook/Controls/GuidebookWindow.xaml
+++ b/Content.Client/Guidebook/Controls/GuidebookWindow.xaml
@@ -18,7 +18,7 @@
                     Name="SearchBar"
                     PlaceHolder="{Loc 'guidebook-filter-placeholder-text'}"
                     HorizontalExpand="True"
-                    Margin="0 5 10 5">                
+                    Margin="0 5 10 5">
                 </LineEdit>
             </BoxContainer>
             <ScrollContainer Name="Scroll" HScrollEnabled="False" HorizontalExpand="True" VerticalExpand="True">
diff --git a/Content.Client/Guidebook/DocumentParsingManager.cs b/Content.Client/Guidebook/DocumentParsingManager.cs
index b81866a6260..9c9e569eb41 100644
--- a/Content.Client/Guidebook/DocumentParsingManager.cs
+++ b/Content.Client/Guidebook/DocumentParsingManager.cs
@@ -2,6 +2,8 @@
 using Content.Client.Guidebook.Richtext;
 using Pidgin;
 using Robust.Client.UserInterface;
+using Robust.Shared.ContentPack;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Reflection;
 using Robust.Shared.Sandboxing;
 using static Pidgin.Parser;
@@ -13,7 +15,9 @@ namespace Content.Client.Guidebook;
 /// </summary>
 public sealed partial class DocumentParsingManager
 {
+    [Dependency] private readonly IPrototypeManager _prototype = default!;
     [Dependency] private readonly IReflectionManager _reflectionManager = default!;
+    [Dependency] private readonly IResourceManager _resourceManager = default!;
     [Dependency] private readonly ISandboxHelper _sandboxHelper = default!;
 
     private readonly Dictionary<string, Parser<char, Control>> _tagControlParsers = new();
@@ -37,6 +41,21 @@ public void Initialize()
         ControlParser = SkipWhitespaces.Then(_controlParser.Many());
     }
 
+    public bool TryAddMarkup(Control control, ProtoId<GuideEntryPrototype> entryId, bool log = true)
+    {
+        if (!_prototype.TryIndex(entryId, out var entry))
+            return false;
+
+        using var file = _resourceManager.ContentFileReadText(entry.Text);
+        return TryAddMarkup(control, file.ReadToEnd(), log);
+    }
+
+    public bool TryAddMarkup(Control control, GuideEntry entry, bool log = true)
+    {
+        using var file = _resourceManager.ContentFileReadText(entry.Text);
+        return TryAddMarkup(control, file.ReadToEnd(), log);
+    }
+
     public bool TryAddMarkup(Control control, string text, bool log = true)
     {
         try
diff --git a/Content.Client/Info/InfoSystem.cs b/Content.Client/Info/InfoSystem.cs
deleted file mode 100644
index b6979949818..00000000000
--- a/Content.Client/Info/InfoSystem.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using Content.Shared.Info;
-using Robust.Shared.Log;
-
-namespace Content.Client.Info;
-
-public sealed class InfoSystem : EntitySystem
-{
-    public RulesMessage Rules = new RulesMessage("Server Rules", "The server did not send any rules.");
-    [Dependency] private readonly RulesManager _rules = default!;
-
-    public override void Initialize()
-    {
-        base.Initialize();
-        SubscribeNetworkEvent<RulesMessage>(OnRulesReceived);
-        Log.Debug("Requested server info.");
-        RaiseNetworkEvent(new RequestRulesMessage());
-    }
-
-    private void OnRulesReceived(RulesMessage message, EntitySessionEventArgs eventArgs)
-    {
-        Log.Debug("Received server rules.");
-        Rules = message;
-        _rules.UpdateRules();
-    }
-}
diff --git a/Content.Client/Info/RulesAndInfoWindow.cs b/Content.Client/Info/RulesAndInfoWindow.cs
index 7a763a1d6f4..b9131dcb3c0 100644
--- a/Content.Client/Info/RulesAndInfoWindow.cs
+++ b/Content.Client/Info/RulesAndInfoWindow.cs
@@ -1,10 +1,8 @@
 using System.Numerics;
 using Content.Client.UserInterface.Systems.EscapeMenu;
-using Robust.Client.ResourceManagement;
 using Robust.Client.UserInterface;
 using Robust.Client.UserInterface.Controls;
 using Robust.Client.UserInterface.CustomControls;
-using Robust.Shared.Configuration;
 using Robust.Shared.ContentPack;
 
 namespace Content.Client.Info
@@ -12,7 +10,6 @@ namespace Content.Client.Info
     public sealed class RulesAndInfoWindow : DefaultWindow
     {
         [Dependency] private readonly IResourceManager _resourceManager = default!;
-        [Dependency] private readonly RulesManager _rules = default!;
 
         public RulesAndInfoWindow()
         {
@@ -22,8 +19,14 @@ public RulesAndInfoWindow()
 
             var rootContainer = new TabContainer();
 
-            var rulesList = new Info();
-            var tutorialList = new Info();
+            var rulesList = new RulesControl
+            {
+                Margin = new Thickness(10)
+            };
+            var tutorialList = new Info
+            {
+                Margin = new Thickness(10)
+            };
 
             rootContainer.AddChild(rulesList);
             rootContainer.AddChild(tutorialList);
@@ -31,7 +34,6 @@ public RulesAndInfoWindow()
             TabContainer.SetTabTitle(rulesList, Loc.GetString("ui-info-tab-rules"));
             TabContainer.SetTabTitle(tutorialList, Loc.GetString("ui-info-tab-tutorial"));
 
-            AddSection(rulesList, _rules.RulesSection());
             PopulateTutorial(tutorialList);
 
             Contents.AddChild(rootContainer);
diff --git a/Content.Client/Info/RulesControl.xaml b/Content.Client/Info/RulesControl.xaml
index 3b247646884..04fa7191234 100644
--- a/Content.Client/Info/RulesControl.xaml
+++ b/Content.Client/Info/RulesControl.xaml
@@ -1,6 +1,18 @@
-<BoxContainer xmlns="https://spacestation14.io"
-              Name="InfoContainer"
-              Orientation="Vertical"
-              Margin="2 2 0 0"
-              SeparationOverride="10"
-              Access="Public"/>
+<BoxContainer xmlns="https://spacestation14.io" Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
+    <Control HorizontalExpand="True" VerticalExpand="True" HorizontalAlignment="Stretch">
+        <ScrollContainer Name="Scroll" HScrollEnabled="False" HorizontalExpand="True" VerticalExpand="True">
+            <BoxContainer Name="RulesContainer" VerticalExpand="True" Margin="0 0 5 0"/>
+        </ScrollContainer>
+        <BoxContainer Margin="0 0 15 0" HorizontalExpand="True" HorizontalAlignment="Right">
+            <Button Name="BackButton"
+                    Text="{Loc 'ui-rules-button-back'}"
+                    HorizontalAlignment="Right"
+                    VerticalAlignment="Top"/>
+            <Control MinWidth="5"/>
+            <Button Name="HomeButton"
+                    Text="{Loc 'ui-rules-button-home'}"
+                    HorizontalAlignment="Right"
+                    VerticalAlignment="Top"/>
+        </BoxContainer>
+    </Control>
+</BoxContainer>
diff --git a/Content.Client/Info/RulesControl.xaml.cs b/Content.Client/Info/RulesControl.xaml.cs
index 6086735e0ea..5e259074968 100644
--- a/Content.Client/Info/RulesControl.xaml.cs
+++ b/Content.Client/Info/RulesControl.xaml.cs
@@ -1,22 +1,53 @@
-using System.IO;
-using Content.Shared.CCVar;
+using Content.Client.Guidebook;
+using Content.Client.Guidebook.RichText;
+using Content.Client.UserInterface.Systems.Info;
 using Robust.Client.AutoGenerated;
-using Robust.Client.ResourceManagement;
 using Robust.Client.UserInterface.Controls;
 using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Configuration;
+using Robust.Shared.Prototypes;
 
 namespace Content.Client.Info;
 
 [GenerateTypedNameReferences]
-public sealed partial class RulesControl : BoxContainer
+public sealed partial class RulesControl : BoxContainer, ILinkClickHandler
 {
-    [Dependency] private readonly RulesManager _rules = default!;
+    [Dependency] private readonly DocumentParsingManager _parsingMan = default!;
+
+    private string? _currentEntry;
+    private readonly Stack<string> _priorEntries = new();
 
     public RulesControl()
     {
         RobustXamlLoader.Load(this);
         IoCManager.InjectDependencies(this);
-        AddChild(_rules.RulesSection());
+
+        SetGuide();
+
+        HomeButton.OnPressed += _ => SetGuide();
+
+        BackButton.OnPressed += _ => SetGuide(_priorEntries.Pop(), false);
+    }
+
+    public void HandleClick(string link)
+    {
+        SetGuide(link);
+    }
+
+    private void SetGuide(ProtoId<GuideEntryPrototype>? entry = null, bool addToPrior = true)
+    {
+        var coreEntry = UserInterfaceManager.GetUIController<InfoUIController>().GetCoreRuleEntry();
+        entry ??= coreEntry;
+
+        Scroll.SetScrollValue(default);
+        RulesContainer.Children.Clear();
+        if (!_parsingMan.TryAddMarkup(RulesContainer, entry.Value))
+            return;
+
+        if (addToPrior && _currentEntry != null)
+            _priorEntries.Push(_currentEntry);
+        _currentEntry = entry.Value;
+
+        HomeButton.Visible = entry.Value != coreEntry.Id;
+        BackButton.Visible = _priorEntries.Count != 0 && _priorEntries.Peek() != entry.Value;
     }
 }
diff --git a/Content.Client/Info/RulesManager.cs b/Content.Client/Info/RulesManager.cs
deleted file mode 100644
index 76e7c34e5f2..00000000000
--- a/Content.Client/Info/RulesManager.cs
+++ /dev/null
@@ -1,105 +0,0 @@
-using Content.Client.Lobby;
-using Content.Client.Gameplay;
-using Content.Shared.CCVar;
-using Content.Shared.Info;
-using Robust.Client.Console;
-using Robust.Client.State;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Shared.Configuration;
-using Robust.Shared.Network;
-
-namespace Content.Client.Info;
-
-public sealed class RulesManager : SharedRulesManager
-{
-    [Dependency] private readonly IConfigurationManager _configManager = default!;
-    [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
-    [Dependency] private readonly IStateManager _stateManager = default!;
-    [Dependency] private readonly IClientConsoleHost _consoleHost = default!;
-    [Dependency] private readonly INetManager _netManager = default!;
-    [Dependency] private readonly IEntitySystemManager _sysMan = default!;
-
-    private InfoSection rulesSection = new InfoSection("", "", false);
-    private bool _shouldShowRules = false;
-
-    private RulesPopup? _activePopup;
-
-    public void Initialize()
-    {
-        _netManager.RegisterNetMessage<ShouldShowRulesPopupMessage>(OnShouldShowRules);
-        _netManager.RegisterNetMessage<ShowRulesPopupMessage>(OnShowRulesPopupMessage);
-        _netManager.RegisterNetMessage<RulesAcceptedMessage>();
-        _stateManager.OnStateChanged += OnStateChanged;
-
-        _consoleHost.RegisterCommand("fuckrules", "", "", (_, _, _) =>
-        {
-            OnAcceptPressed();
-        });
-    }
-
-    private void OnShouldShowRules(ShouldShowRulesPopupMessage message)
-    {
-        _shouldShowRules = true;
-    }
-
-    private void OnShowRulesPopupMessage(ShowRulesPopupMessage message)
-    {
-        ShowRules(message.PopupTime);
-    }
-
-    private void OnStateChanged(StateChangedEventArgs args)
-    {
-        if (args.NewState is not (GameplayState or LobbyState))
-            return;
-
-        if (!_shouldShowRules)
-            return;
-
-        _shouldShowRules = false;
-
-        ShowRules(_configManager.GetCVar(CCVars.RulesWaitTime));
-    }
-
-    private void ShowRules(float time)
-    {
-        if (_activePopup != null)
-            return;
-
-        _activePopup = new RulesPopup
-        {
-            Timer = time
-        };
-
-        _activePopup.OnQuitPressed += OnQuitPressed;
-        _activePopup.OnAcceptPressed += OnAcceptPressed;
-        _userInterfaceManager.WindowRoot.AddChild(_activePopup);
-        LayoutContainer.SetAnchorPreset(_activePopup, LayoutContainer.LayoutPreset.Wide);
-    }
-
-    private void OnQuitPressed()
-    {
-        _consoleHost.ExecuteCommand("quit");
-    }
-
-    private void OnAcceptPressed()
-    {
-        _netManager.ClientSendMessage(new RulesAcceptedMessage());
-
-        _activePopup?.Orphan();
-        _activePopup = null;
-    }
-
-    public void UpdateRules()
-    {
-        var rules = _sysMan.GetEntitySystem<InfoSystem>().Rules;
-        rulesSection.SetText(rules.Title, rules.Text, true);
-    }
-
-    public Control RulesSection()
-    {
-        rulesSection = new InfoSection("", "", false);
-        UpdateRules();
-        return rulesSection;
-    }
-}
diff --git a/Content.Client/Info/RulesPopup.xaml b/Content.Client/Info/RulesPopup.xaml
index dc004af5a2b..ca1e35f08ed 100644
--- a/Content.Client/Info/RulesPopup.xaml
+++ b/Content.Client/Info/RulesPopup.xaml
@@ -5,20 +5,20 @@
          MouseFilter="Stop">
     <parallax:ParallaxControl />
     <Control VerticalExpand="True"
-             MaxWidth="650">
+             MaxWidth="800"
+             MaxHeight="900">
         <PanelContainer StyleClasses="windowPanel" />
-        <ScrollContainer HScrollEnabled="False">
-            <BoxContainer Orientation="Vertical" SeparationOverride="10">
-                <info:RulesControl />
+            <BoxContainer Orientation="Vertical" SeparationOverride="10" Margin="10 10 5 10">
+                <info:RulesControl/>
                 <Label Name="WaitLabel" />
                 <BoxContainer Orientation="Horizontal">
                     <Button Name="AcceptButton"
                             Text="{Loc 'ui-rules-accept'}"
                             Disabled="True" />
                     <Button Name="QuitButton"
+                            StyleClasses="Caution"
                             Text="{Loc 'ui-escape-quit'}" />
                 </BoxContainer>
             </BoxContainer>
-        </ScrollContainer>
     </Control>
 </Control>
diff --git a/Content.Client/Info/RulesPopup.xaml.cs b/Content.Client/Info/RulesPopup.xaml.cs
index 1e090049366..d770b83dc23 100644
--- a/Content.Client/Info/RulesPopup.xaml.cs
+++ b/Content.Client/Info/RulesPopup.xaml.cs
@@ -1,9 +1,7 @@
-using System;
 using Robust.Client.AutoGenerated;
 using Robust.Client.UserInterface;
 using Robust.Client.UserInterface.Controls;
 using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Localization;
 using Robust.Shared.Timing;
 
 namespace Content.Client.Info;
diff --git a/Content.Client/IoC/ClientContentIoC.cs b/Content.Client/IoC/ClientContentIoC.cs
index 8c2b676c2e2..80c04cda044 100644
--- a/Content.Client/IoC/ClientContentIoC.cs
+++ b/Content.Client/IoC/ClientContentIoC.cs
@@ -6,7 +6,6 @@
 using Content.Client.JoinQueue;
 using Content.Client.Eui;
 using Content.Client.GhostKick;
-using Content.Client.Info;
 using Content.Client.Launcher;
 using Content.Client.Parallax.Managers;
 using Content.Client.Players.PlayTimeTracking;
@@ -31,26 +30,25 @@ public static void Register()
         {
             var collection = IoCManager.Instance!;
 
-            IoCManager.Register<IParallaxManager, ParallaxManager>();
-            IoCManager.Register<IChatManager, ChatManager>();
-            IoCManager.Register<IClientPreferencesManager, ClientPreferencesManager>();
-            IoCManager.Register<IStylesheetManager, StylesheetManager>();
-            IoCManager.Register<IScreenshotHook, ScreenshotHook>();
-            IoCManager.Register<FullscreenHook, FullscreenHook>();
-            IoCManager.Register<IClickMapManager, ClickMapManager>();
-            IoCManager.Register<IClientAdminManager, ClientAdminManager>();
-            IoCManager.Register<ISharedAdminManager, ClientAdminManager>();
-            IoCManager.Register<EuiManager, EuiManager>();
-            IoCManager.Register<IVoteManager, VoteManager>();
-            IoCManager.Register<ChangelogManager, ChangelogManager>();
-            IoCManager.Register<RulesManager, RulesManager>();
-            IoCManager.Register<ViewportManager, ViewportManager>();
-            IoCManager.Register<ISharedAdminLogManager, SharedAdminLogManager>();
-            IoCManager.Register<GhostKickManager>();
-            IoCManager.Register<ExtendedDisconnectInformationManager>();
-            IoCManager.Register<JobRequirementsManager>();
-            IoCManager.Register<DocumentParsingManager>();
-            IoCManager.Register<ContentReplayPlaybackManager, ContentReplayPlaybackManager>();
+            collection.Register<IParallaxManager, ParallaxManager>();
+            collection.Register<IChatManager, ChatManager>();
+            collection.Register<IClientPreferencesManager, ClientPreferencesManager>();
+            collection.Register<IStylesheetManager, StylesheetManager>();
+            collection.Register<IScreenshotHook, ScreenshotHook>();
+            collection.Register<FullscreenHook, FullscreenHook>();
+            collection.Register<IClickMapManager, ClickMapManager>();
+            collection.Register<IClientAdminManager, ClientAdminManager>();
+            collection.Register<ISharedAdminManager, ClientAdminManager>();
+            collection.Register<EuiManager, EuiManager>();
+            collection.Register<IVoteManager, VoteManager>();
+            collection.Register<ChangelogManager, ChangelogManager>();
+            collection.Register<ViewportManager, ViewportManager>();
+            collection.Register<ISharedAdminLogManager, SharedAdminLogManager>();
+            collection.Register<GhostKickManager>();
+            collection.Register<ExtendedDisconnectInformationManager>();
+            collection.Register<JobRequirementsManager>();
+            collection.Register<DocumentParsingManager>();
+            collection.Register<ContentReplayPlaybackManager, ContentReplayPlaybackManager>();
             collection.Register<ISharedPlaytimeManager, JobRequirementsManager>();
             IoCManager.Register<JoinQueueManager>();
             IoCManager.Register<DiscordAuthManager>();
diff --git a/Content.Client/Movement/Systems/WaddleAnimationSystem.cs b/Content.Client/Movement/Systems/WaddleAnimationSystem.cs
index 9555c1f6b9e..9b45e445fbb 100644
--- a/Content.Client/Movement/Systems/WaddleAnimationSystem.cs
+++ b/Content.Client/Movement/Systems/WaddleAnimationSystem.cs
@@ -32,7 +32,7 @@ public override void Initialize()
         SubscribeLocalEvent<WaddleAnimationComponent, AnimationCompletedEvent>(OnAnimationCompleted);
         SubscribeLocalEvent<WaddleAnimationComponent, StunnedEvent>(OnStunned);
         SubscribeLocalEvent<WaddleAnimationComponent, KnockedDownEvent>(OnKnockedDown);
-        SubscribeLocalEvent<WaddleAnimationComponent, BuckleChangeEvent>(OnBuckleChange);
+        SubscribeLocalEvent<WaddleAnimationComponent, BuckledEvent>(OnBuckled);
     }
 
     private void OnMovementInput(EntityUid entity, WaddleAnimationComponent component, MoveInputEvent args)
@@ -148,7 +148,7 @@ private void OnKnockedDown(EntityUid uid, WaddleAnimationComponent component, Kn
         StopWaddling(uid, component);
     }
 
-    private void OnBuckleChange(EntityUid uid, WaddleAnimationComponent component, BuckleChangeEvent args)
+    private void OnBuckled(EntityUid uid, WaddleAnimationComponent component, BuckledEvent args)
     {
         StopWaddling(uid, component);
     }
diff --git a/Content.Client/Nyanotrasen/Kitchen/Visualizers/DeepFriedVisualizer.cs b/Content.Client/Nyanotrasen/Kitchen/Visualizers/DeepFriedVisualizer.cs
index 97fea3d0ca9..c5f0123bddb 100644
--- a/Content.Client/Nyanotrasen/Kitchen/Visualizers/DeepFriedVisualizer.cs
+++ b/Content.Client/Nyanotrasen/Kitchen/Visualizers/DeepFriedVisualizer.cs
@@ -11,6 +11,7 @@ namespace Content.Client.Kitchen.Visualizers
 {
     public sealed class DeepFriedVisualizerSystem : VisualizerSystem<DeepFriedComponent>
     {
+        [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
         private readonly static string ShaderName = "Crispy";
 
         public override void Initialize()
@@ -26,7 +27,7 @@ protected override void OnAppearanceChange(EntityUid uid, DeepFriedComponent com
             if (args.Sprite == null)
                 return;
 
-            if (!args.Component.TryGetData(DeepFriedVisuals.Fried, out bool isFried))
+            if (!_appearanceSystem.TryGetData(uid, DeepFriedVisuals.Fried, out bool isFried))
                 return;
 
             for (var i = 0; i < args.Sprite.AllLayers.Count(); ++i)
diff --git a/Content.Client/Nyanotrasen/Kitchen/Visualizers/DeepFryerVisualizer.cs b/Content.Client/Nyanotrasen/Kitchen/Visualizers/DeepFryerVisualizer.cs
index 5d208d09598..ae7a89d6869 100644
--- a/Content.Client/Nyanotrasen/Kitchen/Visualizers/DeepFryerVisualizer.cs
+++ b/Content.Client/Nyanotrasen/Kitchen/Visualizers/DeepFryerVisualizer.cs
@@ -9,9 +9,11 @@ namespace Content.Client.Kitchen.Visualizers
 {
     public sealed class DeepFryerVisualizerSystem : VisualizerSystem<DeepFryerComponent>
     {
+        [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
+
         protected override void OnAppearanceChange(EntityUid uid, DeepFryerComponent component, ref AppearanceChangeEvent args)
         {
-            if (!args.Component.TryGetData(DeepFryerVisuals.Bubbling, out bool isBubbling) ||
+            if (!_appearanceSystem.TryGetData(uid, DeepFryerVisuals.Bubbling, out bool isBubbling) ||
                 !TryComp<SolutionContainerVisualsComponent>(uid, out var scvComponent))
             {
                 return;
diff --git a/Content.Client/Nyanotrasen/Mail/MailSystem.cs b/Content.Client/Nyanotrasen/Mail/MailSystem.cs
index c8d764a22f4..de63d74099b 100644
--- a/Content.Client/Nyanotrasen/Mail/MailSystem.cs
+++ b/Content.Client/Nyanotrasen/Mail/MailSystem.cs
@@ -28,13 +28,14 @@ public sealed class MailJobVisualizerSystem : VisualizerSystem<MailComponent>
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
     [Dependency] private readonly SpriteSystem _stateManager = default!;
     [Dependency] private readonly SpriteSystem _spriteSystem = default!;
+    [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
 
     protected override void OnAppearanceChange(EntityUid uid, MailComponent component, ref AppearanceChangeEvent args)
     {
         if (args.Sprite == null)
             return;
 
-        args.Component.TryGetData(MailVisuals.JobIcon, out string job);
+        _appearanceSystem.TryGetData(uid, MailVisuals.JobIcon, out string job);
 
         if (string.IsNullOrEmpty(job))
             job = "JobIconUnknown";
diff --git a/Content.Client/Outline/InteractionOutlineSystem.cs b/Content.Client/Outline/InteractionOutlineSystem.cs
index 3dbbafbcaa3..40cb5dfd4a6 100644
--- a/Content.Client/Outline/InteractionOutlineSystem.cs
+++ b/Content.Client/Outline/InteractionOutlineSystem.cs
@@ -110,11 +110,15 @@ public override void FrameUpdate(float frameTime)
             && _inputManager.MouseScreenPosition.IsValid)
         {
             var mousePosWorld = vp.PixelToMap(_inputManager.MouseScreenPosition.Position);
-            entityToClick = screen.GetClickedEntity(mousePosWorld);
 
             if (vp is ScalingViewport svp)
             {
                 renderScale = svp.CurrentRenderScale;
+                entityToClick = screen.GetClickedEntity(mousePosWorld, svp.Eye);
+            }
+            else
+            {
+                entityToClick = screen.GetClickedEntity(mousePosWorld);
             }
         }
         else if (_uiManager.CurrentlyHovered is EntityMenuElement element)
diff --git a/Content.Client/Revenant/RevenantSystem.cs b/Content.Client/Revenant/RevenantSystem.cs
index 49d29d8a5f4..e050fe35aa2 100644
--- a/Content.Client/Revenant/RevenantSystem.cs
+++ b/Content.Client/Revenant/RevenantSystem.cs
@@ -1,5 +1,4 @@
 using Content.Client.Alerts;
-using Content.Shared.Alert;
 using Content.Shared.Revenant;
 using Content.Shared.Revenant.Components;
 using Robust.Client.GameObjects;
@@ -42,7 +41,7 @@ private void OnAppearanceChange(EntityUid uid, RevenantComponent component, ref
 
     private void OnUpdateAlert(Entity<RevenantComponent> ent, ref UpdateAlertSpriteEvent args)
     {
-        if (args.Alert.AlertType != AlertType.Essence)
+        if (args.Alert.ID != ent.Comp.EssenceAlert)
             return;
 
         var sprite = args.SpriteViewEnt.Comp;
diff --git a/Content.Client/Sandbox/SandboxSystem.cs b/Content.Client/Sandbox/SandboxSystem.cs
index 6a1129bb75d..b4b7d854412 100644
--- a/Content.Client/Sandbox/SandboxSystem.cs
+++ b/Content.Client/Sandbox/SandboxSystem.cs
@@ -92,7 +92,7 @@ public bool Copy(ICommonSession? session, EntityCoordinates coords, EntityUid ui
                 && EntityManager.TryGetComponent(uid, out MetaDataComponent? comp)
                 && !comp.EntityDeleted)
             {
-                if (comp.EntityPrototype == null || comp.EntityPrototype.NoSpawn || comp.EntityPrototype.Abstract)
+                if (comp.EntityPrototype == null || comp.EntityPrototype.HideSpawnMenu || comp.EntityPrototype.Abstract)
                     return false;
 
                 if (_placement.Eraser)
diff --git a/Content.Client/Shuttles/FtlArrivalOverlay.cs b/Content.Client/Shuttles/FtlArrivalOverlay.cs
new file mode 100644
index 00000000000..f24a1e96488
--- /dev/null
+++ b/Content.Client/Shuttles/FtlArrivalOverlay.cs
@@ -0,0 +1,82 @@
+using System.Numerics;
+using Content.Shared.Shuttles.Components;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Shared.Enums;
+using Robust.Shared.Map.Components;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Shuttles;
+
+/// <summary>
+/// Plays a visualization whenever a shuttle is arriving from FTL.
+/// </summary>
+public sealed class FtlArrivalOverlay : Overlay
+{
+    public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowEntities;
+
+    private EntityLookupSystem _lookups;
+    private SharedMapSystem _maps;
+    private SharedTransformSystem _transforms;
+    private SpriteSystem _sprites;
+    [Dependency] private readonly IEntityManager _entManager = default!;
+    [Dependency] private readonly IGameTiming _timing = default!;
+    [Dependency] private readonly IPrototypeManager _protos = default!;
+
+    private readonly HashSet<Entity<FtlVisualizerComponent>> _visualizers = new();
+
+    private ShaderInstance _shader;
+
+    public FtlArrivalOverlay()
+    {
+        IoCManager.InjectDependencies(this);
+        _lookups = _entManager.System<EntityLookupSystem>();
+        _transforms = _entManager.System<SharedTransformSystem>();
+        _maps = _entManager.System<SharedMapSystem>();
+        _sprites = _entManager.System<SpriteSystem>();
+
+        _shader = _protos.Index<ShaderPrototype>("unshaded").Instance();
+    }
+
+    protected override bool BeforeDraw(in OverlayDrawArgs args)
+    {
+        _visualizers.Clear();
+        _lookups.GetEntitiesOnMap(args.MapId, _visualizers);
+
+        return _visualizers.Count > 0;
+    }
+
+    protected override void Draw(in OverlayDrawArgs args)
+    {
+        args.WorldHandle.UseShader(_shader);
+
+        foreach (var (uid, comp) in _visualizers)
+        {
+            var grid = comp.Grid;
+
+            if (!_entManager.TryGetComponent(grid, out MapGridComponent? mapGrid))
+                continue;
+
+            var texture = _sprites.GetFrame(comp.Sprite, TimeSpan.FromSeconds(comp.Elapsed), loop: false);
+            comp.Elapsed += (float) _timing.FrameTime.TotalSeconds;
+
+            // Need to manually transform the viewport in terms of the visualizer entity as the grid isn't in position.
+            var (_, _, worldMatrix, invMatrix) = _transforms.GetWorldPositionRotationMatrixWithInv(uid);
+            args.WorldHandle.SetTransform(worldMatrix);
+            var localAABB = invMatrix.TransformBox(args.WorldBounds);
+
+            var tilesEnumerator = _maps.GetLocalTilesEnumerator(grid, mapGrid, localAABB);
+
+            while (tilesEnumerator.MoveNext(out var tile))
+            {
+                var bounds = _lookups.GetLocalBounds(tile, mapGrid.TileSize);
+
+                args.WorldHandle.DrawTextureRect(texture, bounds);
+            }
+        }
+
+        args.WorldHandle.UseShader(null);
+        args.WorldHandle.SetTransform(Matrix3x2.Identity);
+    }
+}
diff --git a/Content.Client/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs b/Content.Client/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs
index d5154a87bef..73c11de2795 100644
--- a/Content.Client/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs
+++ b/Content.Client/Shuttles/Systems/ShuttleSystem.EmergencyConsole.cs
@@ -38,9 +38,8 @@ public bool EnableShuttlePosition
     private bool _enableShuttlePosition;
     private EmergencyShuttleOverlay? _overlay;
 
-    public override void Initialize()
+    private void InitializeEmergency()
     {
-        base.Initialize();
         SubscribeNetworkEvent<EmergencyShuttlePositionMessage>(OnShuttlePosMessage);
     }
 
diff --git a/Content.Client/Shuttles/Systems/ShuttleSystem.cs b/Content.Client/Shuttles/Systems/ShuttleSystem.cs
new file mode 100644
index 00000000000..a2c048ff90e
--- /dev/null
+++ b/Content.Client/Shuttles/Systems/ShuttleSystem.cs
@@ -0,0 +1,21 @@
+using Robust.Client.Graphics;
+
+namespace Content.Client.Shuttles.Systems;
+
+public sealed partial class ShuttleSystem
+{
+    [Dependency] private readonly IOverlayManager _overlays = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        InitializeEmergency();
+        _overlays.AddOverlay(new FtlArrivalOverlay());
+    }
+
+    public override void Shutdown()
+    {
+        base.Shutdown();
+        _overlays.RemoveOverlay<FtlArrivalOverlay>();
+    }
+}
diff --git a/Content.Client/Traits/ParacusiaSystem.cs b/Content.Client/Traits/ParacusiaSystem.cs
index d01c7c0005c..da60f1d65e1 100644
--- a/Content.Client/Traits/ParacusiaSystem.cs
+++ b/Content.Client/Traits/ParacusiaSystem.cs
@@ -68,9 +68,13 @@ private void PlayParacusiaSounds(EntityUid uid)
             );
 
         var newCoords = Transform(uid).Coordinates.Offset(randomOffset);
+        var sound = _audio.PlayStatic(paracusia.Sounds, uid, newCoords);
+
+        if (sound == null)
+            return;
 
         // Play the sound
-        paracusia.Stream = _audio.PlayStatic(paracusia.Sounds, uid, newCoords).Value.Entity;
+        paracusia.Stream = sound!.Value.Entity;
     }
 
 }
diff --git a/Content.Client/UserInterface/Systems/Alerts/AlertsUIController.cs b/Content.Client/UserInterface/Systems/Alerts/AlertsUIController.cs
index 3b85972a9b2..5c195120389 100644
--- a/Content.Client/UserInterface/Systems/Alerts/AlertsUIController.cs
+++ b/Content.Client/UserInterface/Systems/Alerts/AlertsUIController.cs
@@ -7,6 +7,7 @@
 using Robust.Client.Player;
 using Robust.Client.UserInterface;
 using Robust.Client.UserInterface.Controllers;
+using Robust.Shared.Prototypes;
 
 namespace Content.Client.UserInterface.Systems.Alerts;
 
@@ -43,7 +44,7 @@ private void OnScreenLoad()
         SyncAlerts();
     }
 
-    private void OnAlertPressed(object? sender, AlertType e)
+    private void OnAlertPressed(object? sender, ProtoId<AlertPrototype> e)
     {
         _alertsSystem?.AlertClicked(e);
     }
diff --git a/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml.cs b/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml.cs
index a1a494c47b3..d6a79a81c46 100644
--- a/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml.cs
+++ b/Content.Client/UserInterface/Systems/Alerts/Widgets/AlertsUI.xaml.cs
@@ -4,6 +4,7 @@
 using Robust.Client.UserInterface.Controls;
 using Robust.Client.UserInterface.XAML;
 using Robust.Shared.Input;
+using Robust.Shared.Prototypes;
 
 namespace Content.Client.UserInterface.Systems.Alerts.Widgets;
 
@@ -21,8 +22,10 @@ public AlertsUI()
         RobustXamlLoader.Load(this);
     }
 
-    public void SyncControls(AlertsSystem alertsSystem, AlertOrderPrototype? alertOrderPrototype,
-        IReadOnlyDictionary<AlertKey, AlertState> alertStates)
+    public void SyncControls(AlertsSystem alertsSystem,
+        AlertOrderPrototype? alertOrderPrototype,
+        IReadOnlyDictionary<AlertKey,
+        AlertState> alertStates)
     {
         // remove any controls with keys no longer present
         if (SyncRemoveControls(alertStates))
@@ -46,7 +49,7 @@ public void ClearAllControls()
         _alertControls.Clear();
     }
 
-    public event EventHandler<AlertType>? AlertPressed;
+    public event EventHandler<ProtoId<AlertPrototype>>? AlertPressed;
 
     private bool SyncRemoveControls(IReadOnlyDictionary<AlertKey, AlertState> alertStates)
     {
@@ -88,7 +91,7 @@ private void SyncUpdateControls(AlertsSystem alertsSystem, AlertOrderPrototype?
             }
 
             if (_alertControls.TryGetValue(newAlert.AlertKey, out var existingAlertControl) &&
-                existingAlertControl.Alert.AlertType == newAlert.AlertType)
+                existingAlertControl.Alert.ID == newAlert.ID)
             {
                 // key is the same, simply update the existing control severity / cooldown
                 existingAlertControl.SetSeverity(alertState.Severity);
@@ -155,6 +158,6 @@ private void AlertControlPressed(BaseButton.ButtonEventArgs args)
         if (args.Event.Function != EngineKeyFunctions.UIClick)
             return;
 
-        AlertPressed?.Invoke(this, control.Alert.AlertType);
+        AlertPressed?.Invoke(this, control.Alert.ID);
     }
 }
diff --git a/Content.Client/UserInterface/Systems/Info/InfoUIController.cs b/Content.Client/UserInterface/Systems/Info/InfoUIController.cs
index d7f2d8e5fed..6d398ec6aa5 100644
--- a/Content.Client/UserInterface/Systems/Info/InfoUIController.cs
+++ b/Content.Client/UserInterface/Systems/Info/InfoUIController.cs
@@ -1,13 +1,73 @@
-using Content.Client.Gameplay;
+using System.Globalization;
+using Content.Client.Gameplay;
+using Content.Client.Guidebook;
 using Content.Client.Info;
+using Content.Shared.Administration.Managers;
+using Content.Shared.CCVar;
+using Content.Shared.Info;
+using Robust.Client;
+using Robust.Client.Console;
+using Robust.Client.Player;
 using Robust.Client.UserInterface.Controllers;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Configuration;
+using Robust.Shared.Network;
+using Robust.Shared.Prototypes;
 
 namespace Content.Client.UserInterface.Systems.Info;
 
 public sealed class InfoUIController : UIController, IOnStateExited<GameplayState>
 {
+    [Dependency] private readonly IBaseClient _client = default!;
+    [Dependency] private readonly IConfigurationManager _cfg = default!;
+    [Dependency] private readonly IClientConsoleHost _consoleHost = default!;
+    [Dependency] private readonly INetManager _netManager = default!;
+    [Dependency] private readonly ISharedAdminManager _adminManager = default!;
+    [Dependency] private readonly IPlayerManager _playerManager = default!;
+    [Dependency] private readonly IPrototypeManager _prototype = default!;
+
+    private RulesPopup? _rulesPopup;
     private RulesAndInfoWindow? _infoWindow;
 
+    private static DateTime NextRulesReadTime => DateTime.UtcNow + TimeSpan.FromDays(60);
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        _client.PlayerJoinedServer += OnJoinedServer;
+        _netManager.RegisterNetMessage<ShowRulesPopupMessage>(OnShowRulesPopupMessage);
+
+        _consoleHost.RegisterCommand("fuckrules",
+            "",
+            "",
+            (_, _, _) =>
+        {
+            OnAcceptPressed();
+        });
+    }
+
+    private void OnJoinedServer(object? sender, PlayerEventArgs args)
+    {
+        if (_playerManager.LocalSession is not { } localSession)
+            return;
+
+        if (_adminManager.IsAdmin(localSession) && _cfg.GetCVar(CCVars.RulesExemptLocal))
+            return;
+
+        var nextReadTarget = DateTime.Parse(_cfg.GetCVar(CCVars.RulesNextPopupTime));
+        if (nextReadTarget >= DateTime.UtcNow)
+            return;
+
+        var time = _cfg.GetCVar(CCVars.RulesWaitTime);
+        ShowRules(time);
+    }
+
+    private void OnShowRulesPopupMessage(ShowRulesPopupMessage message)
+    {
+        ShowRules(message.PopupTime);
+    }
+
     public void OnStateExited(GameplayState state)
     {
         if (_infoWindow == null)
@@ -17,12 +77,47 @@ public void OnStateExited(GameplayState state)
         _infoWindow = null;
     }
 
+    private void ShowRules(float time)
+    {
+        if (_rulesPopup != null)
+            return;
+
+        _rulesPopup = new RulesPopup
+        {
+            Timer = time
+        };
+
+        _rulesPopup.OnQuitPressed += OnQuitPressed;
+        _rulesPopup.OnAcceptPressed += OnAcceptPressed;
+        UIManager.WindowRoot.AddChild(_rulesPopup);
+        LayoutContainer.SetAnchorPreset(_rulesPopup, LayoutContainer.LayoutPreset.Wide);
+    }
+
+    private void OnQuitPressed()
+    {
+        _consoleHost.ExecuteCommand("quit");
+    }
+
+    private void OnAcceptPressed()
+    {
+        _cfg.SetCVar(CCVars.RulesNextPopupTime, NextRulesReadTime.ToString(CultureInfo.InvariantCulture));
+        _cfg.SaveToFile();
+
+        _rulesPopup?.Orphan();
+        _rulesPopup = null;
+    }
+
+    public GuideEntryPrototype GetCoreRuleEntry()
+    {
+        var guide = _cfg.GetCVar(CCVars.RulesFile);
+        var guideEntryPrototype = _prototype.Index<GuideEntryPrototype>(guide);
+        return guideEntryPrototype;
+    }
+
     public void OpenWindow()
     {
         if (_infoWindow == null || _infoWindow.Disposed)
-        {
             _infoWindow = UIManager.CreateWindow<RulesAndInfoWindow>();
-        }
 
         _infoWindow?.OpenCentered();
     }
diff --git a/Content.Client/UserInterface/Systems/Storage/Controls/ItemGridPiece.cs b/Content.Client/UserInterface/Systems/Storage/Controls/ItemGridPiece.cs
index f436cc8c39b..dd9986e4c6e 100644
--- a/Content.Client/UserInterface/Systems/Storage/Controls/ItemGridPiece.cs
+++ b/Content.Client/UserInterface/Systems/Storage/Controls/ItemGridPiece.cs
@@ -9,7 +9,7 @@
 
 namespace Content.Client.UserInterface.Systems.Storage.Controls;
 
-public sealed class ItemGridPiece : Control
+public sealed class ItemGridPiece : Control, IEntityControl
 {
     private readonly IEntityManager _entityManager;
     private readonly StorageUIController _storageController;
@@ -287,6 +287,8 @@ public static Vector2 GetCenterOffset(Entity<ItemComponent?> entity, ItemStorage
         var actualSize = new Vector2(boxSize.X + 1, boxSize.Y + 1);
         return actualSize * new Vector2i(8, 8);
     }
+
+    public EntityUid? UiEntity => Entity;
 }
 
 public enum ItemGridPieceMarks
diff --git a/Content.Client/Weather/WeatherSystem.cs b/Content.Client/Weather/WeatherSystem.cs
index 24de0bc8c4c..64ce64415d9 100644
--- a/Content.Client/Weather/WeatherSystem.cs
+++ b/Content.Client/Weather/WeatherSystem.cs
@@ -2,16 +2,11 @@
 using Content.Shared.Weather;
 using Robust.Client.Audio;
 using Robust.Client.GameObjects;
-using Robust.Client.Graphics;
 using Robust.Client.Player;
-using Robust.Shared.Audio;
 using Robust.Shared.Audio.Systems;
 using Robust.Shared.GameStates;
 using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
-using Robust.Shared.Physics;
-using Robust.Shared.Physics.Components;
-using Robust.Shared.Physics.Systems;
 using Robust.Shared.Player;
 using AudioComponent = Robust.Shared.Audio.Components.AudioComponent;
 
@@ -52,7 +47,8 @@ protected override void Run(EntityUid uid, WeatherData weather, WeatherPrototype
         if (!Timing.IsFirstTimePredicted || weatherProto.Sound == null)
             return;
 
-        weather.Stream ??= _audio.PlayGlobal(weatherProto.Sound, Filter.Local(), true).Value.Entity;
+        var playStream = _audio.PlayGlobal(weatherProto.Sound, Filter.Local(), true);
+        weather.Stream ??= playStream!.Value.Entity;
 
         var stream = weather.Stream.Value;
         var comp = Comp<AudioComponent>(stream);
@@ -124,9 +120,9 @@ protected override void Run(EntityUid uid, WeatherData weather, WeatherPrototype
         comp.Occlusion = occlusion;
     }
 
-    protected override bool SetState(WeatherState state, WeatherComponent comp, WeatherData weather, WeatherPrototype weatherProto)
+    protected override bool SetState(EntityUid uid, WeatherState state, WeatherComponent comp, WeatherData weather, WeatherPrototype weatherProto)
     {
-        if (!base.SetState(state, comp, weather, weatherProto))
+        if (!base.SetState(uid, state, comp, weather, weatherProto))
             return false;
 
         if (!Timing.IsFirstTimePredicted)
@@ -164,7 +160,7 @@ private void OnWeatherHandleState(EntityUid uid, WeatherComponent component, ref
                 continue;
 
             // New weather
-            StartWeather(component, ProtoMan.Index<WeatherPrototype>(proto), weather.EndTime);
+            StartWeather(uid, component, ProtoMan.Index<WeatherPrototype>(proto), weather.EndTime);
         }
     }
 }
diff --git a/Content.IntegrationTests/Tests/Actions/ActionPvsDetachTest.cs b/Content.IntegrationTests/Tests/Actions/ActionPvsDetachTest.cs
index 420a90a50bd..45addff00bf 100644
--- a/Content.IntegrationTests/Tests/Actions/ActionPvsDetachTest.cs
+++ b/Content.IntegrationTests/Tests/Actions/ActionPvsDetachTest.cs
@@ -32,11 +32,14 @@ public async Task TestActionDetach()
         // PVS-detach action entities
         // We do this by just giving them the ghost layer
         var visSys = server.System<VisibilitySystem>();
-        var enumerator = server.Transform(ent).ChildEnumerator;
-        while (enumerator.MoveNext(out var child))
+        server.Post(() =>
         {
-            visSys.AddLayer(child, (int) VisibilityFlags.Ghost);
-        }
+            var enumerator = server.Transform(ent).ChildEnumerator;
+            while (enumerator.MoveNext(out var child))
+            {
+                visSys.AddLayer(child, (int) VisibilityFlags.Ghost);
+            }
+        });
         await pair.RunTicksSync(5);
 
         // Client's actions have left been detached / are out of view, but action comp state has not changed
@@ -44,11 +47,14 @@ public async Task TestActionDetach()
         Assert.That(cSys.GetActions(cEnt).Count(), Is.EqualTo(initActions));
 
         // Re-enter PVS view
-        enumerator = server.Transform(ent).ChildEnumerator;
-        while (enumerator.MoveNext(out var child))
+        server.Post(() =>
         {
-            visSys.RemoveLayer(child, (int) VisibilityFlags.Ghost);
-        }
+            var enumerator = server.Transform(ent).ChildEnumerator;
+            while (enumerator.MoveNext(out var child))
+            {
+                visSys.RemoveLayer(child, (int) VisibilityFlags.Ghost);
+            }
+        });
         await pair.RunTicksSync(5);
         Assert.That(sys.GetActions(ent).Count(), Is.EqualTo(initActions));
         Assert.That(cSys.GetActions(cEnt).Count(), Is.EqualTo(initActions));
diff --git a/Content.IntegrationTests/Tests/Actions/ActionsAddedTest.cs b/Content.IntegrationTests/Tests/Actions/ActionsAddedTest.cs
index 32b15252261..c232e823132 100644
--- a/Content.IntegrationTests/Tests/Actions/ActionsAddedTest.cs
+++ b/Content.IntegrationTests/Tests/Actions/ActionsAddedTest.cs
@@ -18,7 +18,7 @@ public sealed class ActionsAddedTest
     [Test]
     public async Task TestCombatActionsAdded()
     {
-        await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true, DummyTicker = false});
+        await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true, DummyTicker = false });
         var server = pair.Server;
         var client = pair.Client;
         var sEntMan = server.ResolveDependency<IEntityManager>();
diff --git a/Content.IntegrationTests/Tests/Body/GibTest.cs b/Content.IntegrationTests/Tests/Body/GibTest.cs
index c0032a85244..4627c79f64d 100644
--- a/Content.IntegrationTests/Tests/Body/GibTest.cs
+++ b/Content.IntegrationTests/Tests/Body/GibTest.cs
@@ -5,7 +5,7 @@
 namespace Content.IntegrationTests.Tests.Body;
 
 [TestFixture]
-public sealed  class GibTest
+public sealed class GibTest
 {
     [Test]
     public async Task TestGib()
diff --git a/Content.IntegrationTests/Tests/Body/LegTest.cs b/Content.IntegrationTests/Tests/Body/LegTest.cs
index e86966f8f54..7b49bbe84a3 100644
--- a/Content.IntegrationTests/Tests/Body/LegTest.cs
+++ b/Content.IntegrationTests/Tests/Body/LegTest.cs
@@ -5,7 +5,6 @@
 using Content.Shared.Rotation;
 using Robust.Shared.GameObjects;
 using Robust.Shared.Map;
-using Robust.Shared.Maths;
 
 namespace Content.IntegrationTests.Tests.Body
 {
@@ -40,13 +39,14 @@ public async Task RemoveLegsFallTest()
             var appearanceSystem = entityManager.System<SharedAppearanceSystem>();
             var xformSystem = entityManager.System<SharedTransformSystem>();
 
+            var map = await pair.CreateTestMap();
+
             await server.WaitAssertion(() =>
             {
-                var mapId = mapManager.CreateMap();
                 BodyComponent body = null;
 
                 human = entityManager.SpawnEntity("HumanBodyAndAppearanceDummy",
-                    new MapCoordinates(Vector2.Zero, mapId));
+                    new MapCoordinates(Vector2.Zero, map.MapId));
 
                 Assert.Multiple(() =>
                 {
@@ -61,7 +61,7 @@ await server.WaitAssertion(() =>
 
                 foreach (var leg in legs)
                 {
-                    xformSystem.DetachParentToNull(leg.Id, entityManager.GetComponent<TransformComponent>(leg.Id));
+                    xformSystem.DetachEntity(leg.Id, entityManager.GetComponent<TransformComponent>(leg.Id));
                 }
             });
 
diff --git a/Content.IntegrationTests/Tests/Body/LungTest.cs b/Content.IntegrationTests/Tests/Body/LungTest.cs
index dce3741c98d..9b5ee431f1f 100644
--- a/Content.IntegrationTests/Tests/Body/LungTest.cs
+++ b/Content.IntegrationTests/Tests/Body/LungTest.cs
@@ -60,8 +60,8 @@ public async Task AirConsistencyTest()
             var mapManager = server.ResolveDependency<IMapManager>();
             var entityManager = server.ResolveDependency<IEntityManager>();
             var mapLoader = entityManager.System<MapLoaderSystem>();
+            var mapSys = entityManager.System<SharedMapSystem>();
 
-            MapId mapId;
             EntityUid? grid = null;
             BodyComponent body = default;
             RespiratorComponent resp = default;
@@ -73,7 +73,7 @@ public async Task AirConsistencyTest()
 
             await server.WaitPost(() =>
             {
-                mapId = mapManager.CreateMap();
+                mapSys.CreateMap(out var mapId);
                 Assert.That(mapLoader.TryLoad(mapId, testMapName, out var roots));
 
                 var query = entityManager.GetEntityQuery<MapGridComponent>();
@@ -142,8 +142,8 @@ public async Task NoSuffocationTest()
             var entityManager = server.ResolveDependency<IEntityManager>();
             var cfg = server.ResolveDependency<IConfigurationManager>();
             var mapLoader = entityManager.System<MapLoaderSystem>();
+            var mapSys = entityManager.System<SharedMapSystem>();
 
-            MapId mapId;
             EntityUid? grid = null;
             RespiratorComponent respirator = null;
             EntityUid human = default;
@@ -152,7 +152,7 @@ public async Task NoSuffocationTest()
 
             await server.WaitPost(() =>
             {
-                mapId = mapManager.CreateMap();
+                mapSys.CreateMap(out var mapId);
 
                 Assert.That(mapLoader.TryLoad(mapId, testMapName, out var ents), Is.True);
                 var query = entityManager.GetEntityQuery<MapGridComponent>();
diff --git a/Content.IntegrationTests/Tests/Body/SaveLoadReparentTest.cs b/Content.IntegrationTests/Tests/Body/SaveLoadReparentTest.cs
index 670ce1a474d..01482ba8ee2 100644
--- a/Content.IntegrationTests/Tests/Body/SaveLoadReparentTest.cs
+++ b/Content.IntegrationTests/Tests/Body/SaveLoadReparentTest.cs
@@ -33,10 +33,11 @@ public async Task Test()
         var mapLoader = entities.System<MapLoaderSystem>();
         var bodySystem = entities.System<SharedBodySystem>();
         var containerSystem = entities.System<SharedContainerSystem>();
+        var mapSys = entities.System<SharedMapSystem>();
 
         await server.WaitAssertion(() =>
         {
-            var mapId = maps.CreateMap();
+            mapSys.CreateMap(out var mapId);
             maps.CreateGrid(mapId);
             var human = entities.SpawnEntity("HumanBodyDummy", new MapCoordinates(0, 0, mapId));
 
@@ -115,7 +116,7 @@ await server.WaitAssertion(() =>
             mapLoader.SaveMap(mapId, mapPath);
             maps.DeleteMap(mapId);
 
-            mapId = maps.CreateMap();
+            mapSys.CreateMap(out mapId);
             Assert.That(mapLoader.TryLoad(mapId, mapPath, out _), Is.True);
 
             var query = EnumerateQueryEnumerator(
diff --git a/Content.IntegrationTests/Tests/Buckle/BuckleDragTest.cs b/Content.IntegrationTests/Tests/Buckle/BuckleDragTest.cs
new file mode 100644
index 00000000000..19e8aba1824
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Buckle/BuckleDragTest.cs
@@ -0,0 +1,60 @@
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Shared.Buckle;
+using Content.Shared.Buckle.Components;
+using Content.Shared.Input;
+using Content.Shared.Movement.Pulling.Components;
+
+namespace Content.IntegrationTests.Tests.Buckle;
+
+public sealed class BuckleDragTest : InteractionTest
+{
+    // Check that dragging a buckled player unbuckles them.
+    [Test]
+    public async Task BucklePullTest()
+    {
+        var urist = await SpawnTarget("MobHuman");
+        var sUrist = ToServer(urist);
+        await SpawnTarget("Chair");
+
+        var buckle = Comp<BuckleComponent>(urist);
+        var strap = Comp<StrapComponent>(Target);
+        var puller = Comp<PullerComponent>(Player);
+        var pullable = Comp<PullableComponent>(urist);
+
+#pragma warning disable RA0002
+        buckle.Delay = TimeSpan.Zero;
+#pragma warning restore RA0002
+
+        // Initially not buckled to the chair and not pulling anything
+        Assert.That(buckle.Buckled, Is.False);
+        Assert.That(buckle.BuckledTo, Is.Null);
+        Assert.That(strap.BuckledEntities, Is.Empty);
+        Assert.That(puller.Pulling, Is.Null);
+        Assert.That(pullable.Puller, Is.Null);
+        Assert.That(pullable.BeingPulled, Is.False);
+
+        // Strap the human to the chair
+        await Server.WaitAssertion(() =>
+        {
+            Assert.That(Server.System<SharedBuckleSystem>().TryBuckle(sUrist, SPlayer, STarget.Value));
+        });
+
+        await RunTicks(5);
+        Assert.That(buckle.Buckled, Is.True);
+        Assert.That(buckle.BuckledTo, Is.EqualTo(STarget));
+        Assert.That(strap.BuckledEntities, Is.EquivalentTo(new[] { sUrist }));
+        Assert.That(puller.Pulling, Is.Null);
+        Assert.That(pullable.Puller, Is.Null);
+        Assert.That(pullable.BeingPulled, Is.False);
+
+        // Start pulling, and thus unbuckle them
+        await PressKey(ContentKeyFunctions.TryPullObject, cursorEntity: urist);
+        await RunTicks(5);
+        Assert.That(buckle.Buckled, Is.False);
+        Assert.That(buckle.BuckledTo, Is.Null);
+        Assert.That(strap.BuckledEntities, Is.Empty);
+        Assert.That(puller.Pulling, Is.EqualTo(sUrist));
+        Assert.That(pullable.Puller, Is.EqualTo(SPlayer));
+        Assert.That(pullable.BeingPulled, Is.True);
+    }
+}
diff --git a/Content.IntegrationTests/Tests/Buckle/BuckleTest.Interact.cs b/Content.IntegrationTests/Tests/Buckle/BuckleTest.Interact.cs
new file mode 100644
index 00000000000..d9cce764ab7
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Buckle/BuckleTest.Interact.cs
@@ -0,0 +1,108 @@
+using Content.Shared.Buckle;
+using Content.Shared.Buckle.Components;
+using Content.Shared.Interaction;
+using Robust.Server.GameObjects;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+
+namespace Content.IntegrationTests.Tests.Buckle;
+
+public sealed partial class BuckleTest
+{
+    [Test]
+    public async Task BuckleInteractUnbuckleOther()
+    {
+        await using var pair = await PoolManager.GetServerClient();
+        var server = pair.Server;
+
+        var entMan = server.ResolveDependency<IServerEntityManager>();
+        var buckleSystem = entMan.System<SharedBuckleSystem>();
+
+        EntityUid user = default;
+        EntityUid victim = default;
+        EntityUid chair = default;
+        BuckleComponent buckle = null;
+        StrapComponent strap = null;
+
+        await server.WaitAssertion(() =>
+        {
+            user = entMan.SpawnEntity(BuckleDummyId, MapCoordinates.Nullspace);
+            victim = entMan.SpawnEntity(BuckleDummyId, MapCoordinates.Nullspace);
+            chair = entMan.SpawnEntity(StrapDummyId, MapCoordinates.Nullspace);
+
+            Assert.That(entMan.TryGetComponent(victim, out buckle));
+            Assert.That(entMan.TryGetComponent(chair, out strap));
+
+#pragma warning disable RA0002
+            buckle.Delay = TimeSpan.Zero;
+#pragma warning restore RA0002
+
+            // Buckle victim to chair
+            Assert.That(buckleSystem.TryBuckle(victim, user, chair, buckle));
+            Assert.Multiple(() =>
+            {
+                Assert.That(buckle.BuckledTo, Is.EqualTo(chair), "Victim did not get buckled to the chair.");
+                Assert.That(buckle.Buckled, "Victim is not buckled.");
+                Assert.That(strap.BuckledEntities, Does.Contain(victim), "Chair does not have victim buckled to it.");
+            });
+
+            // InteractHand with chair to unbuckle victim
+            entMan.EventBus.RaiseLocalEvent(chair, new InteractHandEvent(user, chair));
+            Assert.Multiple(() =>
+            {
+                Assert.That(buckle.BuckledTo, Is.Null);
+                Assert.That(buckle.Buckled, Is.False);
+                Assert.That(strap.BuckledEntities, Does.Not.Contain(victim));
+            });
+        });
+
+        await pair.CleanReturnAsync();
+    }
+
+    [Test]
+    public async Task BuckleInteractBuckleUnbuckleSelf()
+    {
+        await using var pair = await PoolManager.GetServerClient();
+        var server = pair.Server;
+
+        var entMan = server.ResolveDependency<IServerEntityManager>();
+
+        EntityUid user = default;
+        EntityUid chair = default;
+        BuckleComponent buckle = null;
+        StrapComponent strap = null;
+
+        await server.WaitAssertion(() =>
+        {
+            user = entMan.SpawnEntity(BuckleDummyId, MapCoordinates.Nullspace);
+            chair = entMan.SpawnEntity(StrapDummyId, MapCoordinates.Nullspace);
+
+            Assert.That(entMan.TryGetComponent(user, out buckle));
+            Assert.That(entMan.TryGetComponent(chair, out strap));
+
+#pragma warning disable RA0002
+            buckle.Delay = TimeSpan.Zero;
+#pragma warning restore RA0002
+
+            // Buckle user to chair
+            entMan.EventBus.RaiseLocalEvent(chair, new InteractHandEvent(user, chair));
+            Assert.Multiple(() =>
+            {
+                Assert.That(buckle.BuckledTo, Is.EqualTo(chair), "Victim did not get buckled to the chair.");
+                Assert.That(buckle.Buckled, "Victim is not buckled.");
+                Assert.That(strap.BuckledEntities, Does.Contain(user), "Chair does not have victim buckled to it.");
+            });
+
+            // InteractHand with chair to unbuckle
+            entMan.EventBus.RaiseLocalEvent(chair, new InteractHandEvent(user, chair));
+            Assert.Multiple(() =>
+            {
+                Assert.That(buckle.BuckledTo, Is.Null);
+                Assert.That(buckle.Buckled, Is.False);
+                Assert.That(strap.BuckledEntities, Does.Not.Contain(user));
+            });
+        });
+
+        await pair.CleanReturnAsync();
+    }
+}
diff --git a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs
index 7c700d9fb8a..94572da4989 100644
--- a/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs
+++ b/Content.IntegrationTests/Tests/Buckle/BuckleTest.cs
@@ -15,7 +15,7 @@ namespace Content.IntegrationTests.Tests.Buckle
     [TestFixture]
     [TestOf(typeof(BuckleComponent))]
     [TestOf(typeof(StrapComponent))]
-    public sealed class BuckleTest
+    public sealed partial class BuckleTest
     {
         private const string BuckleDummyId = "BuckleDummy";
         private const string StrapDummyId = "StrapDummy";
@@ -90,7 +90,6 @@ await server.WaitAssertion(() =>
                 {
                     Assert.That(strap, Is.Not.Null);
                     Assert.That(strap.BuckledEntities, Is.Empty);
-                    Assert.That(strap.OccupiedSize, Is.Zero);
                 });
 
                 // Side effects of buckling
@@ -110,8 +109,6 @@ await server.WaitAssertion(() =>
 
                     // Side effects of buckling for the strap
                     Assert.That(strap.BuckledEntities, Does.Contain(human));
-                    Assert.That(strap.OccupiedSize, Is.EqualTo(buckle.Size));
-                    Assert.That(strap.OccupiedSize, Is.Positive);
                 });
 
 #pragma warning disable NUnit2045 // Interdependent asserts.
@@ -121,7 +118,7 @@ await server.WaitAssertion(() =>
                 // Trying to unbuckle too quickly fails
                 Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False);
                 Assert.That(buckle.Buckled);
-                Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False);
+                Assert.That(buckleSystem.TryUnbuckle(human, human), Is.False);
                 Assert.That(buckle.Buckled);
 #pragma warning restore NUnit2045
             });
@@ -148,7 +145,6 @@ await server.WaitAssertion(() =>
 
                     // Unbuckle, strap
                     Assert.That(strap.BuckledEntities, Is.Empty);
-                    Assert.That(strap.OccupiedSize, Is.Zero);
                 });
 
 #pragma warning disable NUnit2045 // Interdependent asserts.
@@ -159,9 +155,9 @@ await server.WaitAssertion(() =>
                 // On cooldown
                 Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False);
                 Assert.That(buckle.Buckled);
-                Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False);
+                Assert.That(buckleSystem.TryUnbuckle(human, human), Is.False);
                 Assert.That(buckle.Buckled);
-                Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False);
+                Assert.That(buckleSystem.TryUnbuckle(human, human), Is.False);
                 Assert.That(buckle.Buckled);
 #pragma warning restore NUnit2045
             });
@@ -188,7 +184,6 @@ await server.WaitAssertion(() =>
 #pragma warning disable NUnit2045 // Interdependent asserts.
                 Assert.That(buckleSystem.TryBuckle(human, human, chair, buckleComp: buckle), Is.False);
                 Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False);
-                Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False);
 #pragma warning restore NUnit2045
 
                 // Move near the chair
@@ -201,12 +196,10 @@ await server.WaitAssertion(() =>
                 Assert.That(buckle.Buckled);
                 Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False);
                 Assert.That(buckle.Buckled);
-                Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False);
-                Assert.That(buckle.Buckled);
 #pragma warning restore NUnit2045
 
                 // Force unbuckle
-                Assert.That(buckleSystem.TryUnbuckle(human, human, true, buckleComp: buckle));
+                buckleSystem.Unbuckle(human, human);
                 Assert.Multiple(() =>
                 {
                     Assert.That(buckle.Buckled, Is.False);
@@ -310,7 +303,7 @@ await server.WaitAssertion(() =>
                 // Break our guy's kneecaps
                 foreach (var leg in legs)
                 {
-                    xformSystem.DetachParentToNull(leg.Id, entityManager.GetComponent<TransformComponent>(leg.Id));
+                    entityManager.DeleteEntity(leg.Id);
                 }
             });
 
@@ -327,7 +320,8 @@ await server.WaitAssertion(() =>
                     Assert.That(hand.HeldEntity, Is.Null);
                 }
 
-                buckleSystem.TryUnbuckle(human, human, true, buckleComp: buckle);
+                buckleSystem.Unbuckle(human, human);
+                Assert.That(buckle.Buckled, Is.False);
             });
 
             await pair.CleanReturnAsync();
diff --git a/Content.IntegrationTests/Tests/CargoTest.cs b/Content.IntegrationTests/Tests/CargoTest.cs
index 09f179cf4f5..99160df3c66 100644
--- a/Content.IntegrationTests/Tests/CargoTest.cs
+++ b/Content.IntegrationTests/Tests/CargoTest.cs
@@ -14,11 +14,11 @@ namespace Content.IntegrationTests.Tests;
 [TestFixture]
 public sealed class CargoTest
 {
-    public static HashSet<ProtoId<CargoProductPrototype>> Ignored = new ()
-    {
+    private static readonly HashSet<ProtoId<CargoProductPrototype>> Ignored =
+    [
         // This is ignored because it is explicitly intended to be able to sell for more than it costs.
         new("FunCrateGambling")
-    };
+    ];
 
     [Test]
     public async Task NoCargoOrderArbitrage()
@@ -174,13 +174,16 @@ public async Task StackPrice()
     {
         await using var pair = await PoolManager.GetServerClient();
         var server = pair.Server;
-
         var entManager = server.ResolveDependency<IEntityManager>();
-        var priceSystem = entManager.System<PricingSystem>();
 
-        var ent = entManager.SpawnEntity("StackEnt", MapCoordinates.Nullspace);
-        var price = priceSystem.GetPrice(ent);
-        Assert.That(price, Is.EqualTo(100.0));
+        await server.WaitAssertion(() =>
+        {
+            var priceSystem = entManager.System<PricingSystem>();
+
+            var ent = entManager.SpawnEntity("StackEnt", MapCoordinates.Nullspace);
+            var price = priceSystem.GetPrice(ent);
+            Assert.That(price, Is.EqualTo(100.0));
+        });
 
         await pair.CleanReturnAsync();
     }
diff --git a/Content.IntegrationTests/Tests/Chemistry/DispenserTest.cs b/Content.IntegrationTests/Tests/Chemistry/DispenserTest.cs
index a5449308be4..52b7e555a9d 100644
--- a/Content.IntegrationTests/Tests/Chemistry/DispenserTest.cs
+++ b/Content.IntegrationTests/Tests/Chemistry/DispenserTest.cs
@@ -18,7 +18,7 @@ public async Task InsertEjectBuiTest()
         ToggleNeedPower();
 
         // Insert beaker
-        await Interact("Beaker");
+        await InteractUsing("Beaker");
         Assert.That(Hands.ActiveHandEntity, Is.Null);
 
         // Open BUI
diff --git a/Content.IntegrationTests/Tests/Chemistry/FixedPoint2SerializationTest.cs b/Content.IntegrationTests/Tests/Chemistry/FixedPoint2SerializationTest.cs
index 8e3b89bff11..0e3f89c2825 100644
--- a/Content.IntegrationTests/Tests/Chemistry/FixedPoint2SerializationTest.cs
+++ b/Content.IntegrationTests/Tests/Chemistry/FixedPoint2SerializationTest.cs
@@ -9,10 +9,10 @@ namespace Content.IntegrationTests.Tests.Chemistry
 {
     public sealed class FixedPoint2SerializationTest : SerializationTest
     {
-        protected override Assembly[] Assemblies => new[]
-        {
+        protected override Assembly[] Assemblies =>
+        [
             typeof(FixedPoint2SerializationTest).Assembly
-        };
+        ];
 
         [Test]
         public void DeserializeNullTest()
@@ -53,6 +53,6 @@ public void DeserializeNullDefinitionTest()
     [DataDefinition]
     public sealed partial class FixedPoint2TestDefinition
     {
-        [DataField("unit")] public FixedPoint2? Unit { get; set; } = FixedPoint2.New(5);
+        [DataField] public FixedPoint2? Unit { get; set; } = FixedPoint2.New(5);
     }
 }
diff --git a/Content.IntegrationTests/Tests/Chemistry/SolutionRoundingTest.cs b/Content.IntegrationTests/Tests/Chemistry/SolutionRoundingTest.cs
index 4d19a96d9e7..89d33186a27 100644
--- a/Content.IntegrationTests/Tests/Chemistry/SolutionRoundingTest.cs
+++ b/Content.IntegrationTests/Tests/Chemistry/SolutionRoundingTest.cs
@@ -1,5 +1,5 @@
-using Content.Server.Chemistry.Containers.EntitySystems;
 using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.EntitySystems;
 using Content.Shared.Chemistry.Reaction;
 using Content.Shared.Chemistry.Reagent;
 using Content.Shared.FixedPoint;
@@ -76,7 +76,7 @@ public async Task Test()
 
         await server.WaitPost(() =>
         {
-            var system = server.System<SolutionContainerSystem>();
+            var system = server.System<SharedSolutionContainerSystem>();
             var beaker = server.EntMan.SpawnEntity("SolutionRoundingTestContainer", testMap.GridCoords);
 
             system.TryGetSolution(beaker, "beaker", out var newSolutionEnt, out var newSolution);
diff --git a/Content.IntegrationTests/Tests/Chemistry/SolutionSystemTests.cs b/Content.IntegrationTests/Tests/Chemistry/SolutionSystemTests.cs
index d96a035b2dc..6b71dd08be0 100644
--- a/Content.IntegrationTests/Tests/Chemistry/SolutionSystemTests.cs
+++ b/Content.IntegrationTests/Tests/Chemistry/SolutionSystemTests.cs
@@ -1,5 +1,5 @@
-using Content.Server.Chemistry.Containers.EntitySystems;
 using Content.Shared.Chemistry.Components;
+using Content.Shared.Chemistry.EntitySystems;
 using Content.Shared.FixedPoint;
 using Robust.Shared.GameObjects;
 using Robust.Shared.Prototypes;
@@ -11,7 +11,7 @@ namespace Content.IntegrationTests.Tests.Chemistry;
 // To ensure volume(A) + volume(B) = volume(A+B)
 // reactions can change this assumption
 [TestFixture]
-[TestOf(typeof(SolutionContainerSystem))]
+[TestOf(typeof(SharedSolutionContainerSystem))]
 public sealed class SolutionSystemTests
 {
     [TestPrototypes]
@@ -51,7 +51,7 @@ public async Task TryAddTwoNonReactiveReagent()
 
         var entityManager = server.ResolveDependency<IEntityManager>();
         var protoMan = server.ResolveDependency<IPrototypeManager>();
-        var containerSystem = entityManager.System<SolutionContainerSystem>();
+        var containerSystem = entityManager.System<SharedSolutionContainerSystem>();
         var testMap = await pair.CreateTestMap();
         var coordinates = testMap.GridCoords;
 
@@ -97,7 +97,7 @@ public async Task TryAddTooMuchNonReactiveReagent()
 
         var entityManager = server.ResolveDependency<IEntityManager>();
         var protoMan = server.ResolveDependency<IPrototypeManager>();
-        var containerSystem = entityManager.System<SolutionContainerSystem>();
+        var containerSystem = entityManager.System<SharedSolutionContainerSystem>();
         var coordinates = testMap.GridCoords;
 
         EntityUid beaker;
@@ -141,7 +141,7 @@ public async Task TryMixAndOverflowTooMuchReagent()
         var entityManager = server.ResolveDependency<IEntityManager>();
         var protoMan = server.ResolveDependency<IPrototypeManager>();
         var testMap = await pair.CreateTestMap();
-        var containerSystem = entityManager.System<SolutionContainerSystem>();
+        var containerSystem = entityManager.System<SharedSolutionContainerSystem>();
         var coordinates = testMap.GridCoords;
 
         EntityUid beaker;
@@ -194,7 +194,7 @@ public async Task TryMixAndOverflowTooBigOverflow()
 
         var entityManager = server.ResolveDependency<IEntityManager>();
         var protoMan = server.ResolveDependency<IPrototypeManager>();
-        var containerSystem = entityManager.System<SolutionContainerSystem>();
+        var containerSystem = entityManager.System<SharedSolutionContainerSystem>();
         var testMap = await pair.CreateTestMap();
         var coordinates = testMap.GridCoords;
 
diff --git a/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs b/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs
index ddfe7b3481e..3664cda922a 100644
--- a/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs
+++ b/Content.IntegrationTests/Tests/Chemistry/TryAllReactionsTest.cs
@@ -1,4 +1,3 @@
-using Content.Server.Chemistry.Containers.EntitySystems;
 using Content.Shared.Chemistry.Reaction;
 using Content.Shared.Chemistry.Components;
 using Robust.Shared.GameObjects;
@@ -6,6 +5,7 @@
 using Robust.Shared.Prototypes;
 using Robust.Shared.Utility;
 using System.Linq;
+using Content.Shared.Chemistry.EntitySystems;
 
 namespace Content.IntegrationTests.Tests.Chemistry
 {
@@ -34,7 +34,7 @@ public async Task TryAllTest()
             var prototypeManager = server.ResolveDependency<IPrototypeManager>();
             var testMap = await pair.CreateTestMap();
             var coordinates = testMap.GridCoords;
-            var solutionContainerSystem = entityManager.System<SolutionContainerSystem>();
+            var solutionContainerSystem = entityManager.System<SharedSolutionContainerSystem>();
 
             foreach (var reactionPrototype in prototypeManager.EnumeratePrototypes<ReactionPrototype>())
             {
diff --git a/Content.IntegrationTests/Tests/ClickableTest.cs b/Content.IntegrationTests/Tests/ClickableTest.cs
index 76085381852..59836509081 100644
--- a/Content.IntegrationTests/Tests/ClickableTest.cs
+++ b/Content.IntegrationTests/Tests/ClickableTest.cs
@@ -52,7 +52,6 @@ public async Task<bool> Test(string prototype, float clickPosX, float clickPosY,
             var serverEntManager = server.ResolveDependency<IEntityManager>();
             var eyeManager = client.ResolveDependency<IEyeManager>();
             var spriteQuery = clientEntManager.GetEntityQuery<SpriteComponent>();
-            var xformQuery = clientEntManager.GetEntityQuery<TransformComponent>();
             var eye = client.ResolveDependency<IEyeManager>().CurrentEye;
 
             var testMap = await pair.CreateTestMap();
@@ -80,9 +79,8 @@ await client.WaitPost(() =>
                 eyeManager.CurrentEye.Rotation = 0;
 
                 var pos = clientEntManager.System<SharedTransformSystem>().GetWorldPosition(clientEnt);
-                var clickable = clientEntManager.GetComponent<ClickableComponent>(clientEnt);
 
-                hit = clickable.CheckClick(sprite, xformQuery.GetComponent(clientEnt), xformQuery, new Vector2(clickPosX, clickPosY) + pos, eye, out _, out _, out _);
+                hit = clientEntManager.System<ClickableSystem>().CheckClick((clientEnt, null, sprite, null), new Vector2(clickPosX, clickPosY) + pos, eye, out _, out _, out _);
             });
 
             await server.WaitPost(() =>
diff --git a/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs b/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs
index d8d3086520e..2db0a9acd3d 100644
--- a/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs
+++ b/Content.IntegrationTests/Tests/Climbing/ClimbingTest.cs
@@ -1,5 +1,6 @@
 #nullable enable
 using Content.IntegrationTests.Tests.Interaction;
+using Content.IntegrationTests.Tests.Movement;
 using Robust.Shared.Maths;
 using ClimbingComponent = Content.Shared.Climbing.Components.ClimbingComponent;
 using ClimbSystem = Content.Shared.Climbing.Systems.ClimbSystem;
diff --git a/Content.IntegrationTests/Tests/Commands/PardonCommand.cs b/Content.IntegrationTests/Tests/Commands/PardonCommand.cs
index b3a66e3211c..4db9eabf5c6 100644
--- a/Content.IntegrationTests/Tests/Commands/PardonCommand.cs
+++ b/Content.IntegrationTests/Tests/Commands/PardonCommand.cs
@@ -28,7 +28,7 @@ public async Task PardonTest()
 
             Assert.That(netMan.IsConnected);
 
-            Assert.That(sPlayerManager.Sessions.Count(), Is.EqualTo(1));
+            Assert.That(sPlayerManager.Sessions, Has.Length.EqualTo(1));
             // No bans on record
             Assert.Multiple(async () =>
             {
@@ -50,7 +50,7 @@ public async Task PardonTest()
 
             var banReason = "test";
 
-            Assert.That(sPlayerManager.Sessions.Count(), Is.EqualTo(1));
+            Assert.That(sPlayerManager.Sessions, Has.Length.EqualTo(1));
             // Ban the client for 24 hours
             await server.WaitPost(() => sConsole.ExecuteCommand($"ban {clientSession.Name} {banReason} 1440"));
 
@@ -63,7 +63,7 @@ public async Task PardonTest()
             });
 
             await pair.RunTicksSync(5);
-            Assert.That(sPlayerManager.Sessions.Count(), Is.EqualTo(0));
+            Assert.That(sPlayerManager.Sessions, Has.Length.EqualTo(0));
             Assert.That(!netMan.IsConnected);
 
             // Try to pardon a ban that does not exist
@@ -143,11 +143,11 @@ public async Task PardonTest()
             });
 
             // Reconnect client. Slightly faster than dirtying the pair.
-            Assert.That(sPlayerManager.Sessions.Count(), Is.EqualTo(0));
+            Assert.That(sPlayerManager.Sessions, Is.Empty);
             client.SetConnectTarget(server);
             await client.WaitPost(() => netMan.ClientConnect(null!, 0, null!));
             await pair.RunTicksSync(5);
-            Assert.That(sPlayerManager.Sessions.Count(), Is.EqualTo(1));
+            Assert.That(sPlayerManager.Sessions, Has.Length.EqualTo(1));
 
             await pair.CleanReturnAsync();
         }
diff --git a/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs b/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs
index 2fda3ad58e6..cfc80073066 100644
--- a/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs
+++ b/Content.IntegrationTests/Tests/Commands/RejuvenateTest.cs
@@ -37,9 +37,9 @@ public async Task RejuvenateDeadTest()
             var server = pair.Server;
             var entManager = server.ResolveDependency<IEntityManager>();
             var prototypeManager = server.ResolveDependency<IPrototypeManager>();
-            var mobStateSystem = entManager.EntitySysManager.GetEntitySystem<MobStateSystem>();
-            var damSystem = entManager.EntitySysManager.GetEntitySystem<DamageableSystem>();
-            var rejuvenateSystem = entManager.EntitySysManager.GetEntitySystem<RejuvenateSystem>();
+            var mobStateSystem = entManager.System<MobStateSystem>();
+            var damSystem = entManager.System<DamageableSystem>();
+            var rejuvenateSystem = entManager.System<RejuvenateSystem>();
 
             await server.WaitAssertion(() =>
             {
diff --git a/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs b/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs
index 74d014b7724..b94cd7807cf 100644
--- a/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs
+++ b/Content.IntegrationTests/Tests/Commands/RestartRoundTest.cs
@@ -25,7 +25,7 @@ public async Task RestartRoundAfterStart(bool lobbyEnabled)
 
             var configManager = server.ResolveDependency<IConfigurationManager>();
             var entityManager = server.ResolveDependency<IEntityManager>();
-            var gameTicker = entityManager.EntitySysManager.GetEntitySystem<GameTicker>();
+            var gameTicker = entityManager.System<GameTicker>();
 
             await pair.RunTicksSync(5);
 
diff --git a/Content.IntegrationTests/Tests/Construction/Interaction/ComputerContruction.cs b/Content.IntegrationTests/Tests/Construction/Interaction/ComputerContruction.cs
index 5412469ac5d..8af5edaf316 100644
--- a/Content.IntegrationTests/Tests/Construction/Interaction/ComputerContruction.cs
+++ b/Content.IntegrationTests/Tests/Construction/Interaction/ComputerContruction.cs
@@ -16,10 +16,8 @@ public async Task ConstructComputer()
         await StartConstruction(Computer);
 
         // Initial interaction (ghost turns into real entity)
-        await Interact(Steel, 5);
-        ClientAssertPrototype(ComputerFrame, ClientTarget);
-        Target = CTestSystem.Ghosts[ClientTarget!.Value.GetHashCode()];
-        ClientTarget = null;
+        await InteractUsing(Steel, 5);
+        ClientAssertPrototype(ComputerFrame, Target);
 
         // Perform construction steps
         await Interact(
@@ -41,7 +39,7 @@ public async Task DeconstructComputer()
         await StartDeconstruction(ComputerId);
 
         // Initial interaction turns id computer into generic computer
-        await Interact(Screw);
+        await InteractUsing(Screw);
         AssertPrototype(ComputerFrame);
 
         // Perform deconstruction steps
@@ -71,7 +69,7 @@ public async Task ChangeComputer()
         await SpawnTarget(ComputerId);
 
         // Initial interaction turns id computer into generic computer
-        await Interact(Screw);
+        await InteractUsing(Screw);
         AssertPrototype(ComputerFrame);
 
         // Perform partial deconstruction steps
diff --git a/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs b/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs
index 76911eba5f7..74d0e924217 100644
--- a/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs
+++ b/Content.IntegrationTests/Tests/Construction/Interaction/CraftingTests.cs
@@ -59,11 +59,6 @@ public async Task CraftSpear()
         await AssertEntityLookup((Rod, 2), (Cable, 7), (ShardGlass, 2), (Spear, 1));
     }
 
-    // The following is wrapped in an if DEBUG. This is because of cursed state handling bugs. Tests don't (de)serialize
-    // net messages and just copy objects by reference. This means that the server will directly modify cached server
-    // states on the client's end. Crude fix at the moment is to used modified state handling while in debug mode
-    // Otherwise, this test cannot work.
-#if DEBUG
     /// <summary>
     /// Cancel crafting a complex recipe.
     /// </summary>
@@ -93,28 +88,22 @@ public async Task CancelCraft()
         await RunTicks(1);
 
         // DoAfter is in progress. Entity not spawned, stacks have been split and someingredients are in a container.
-        Assert.Multiple(async () =>
-        {
-            Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1));
-            Assert.That(sys.IsEntityInContainer(shard), Is.True);
-            Assert.That(sys.IsEntityInContainer(rods), Is.False);
-            Assert.That(sys.IsEntityInContainer(wires), Is.False);
-            Assert.That(rodStack, Has.Count.EqualTo(8));
-            Assert.That(wireStack, Has.Count.EqualTo(7));
+        Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1));
+        Assert.That(sys.IsEntityInContainer(shard), Is.True);
+        Assert.That(sys.IsEntityInContainer(rods), Is.False);
+        Assert.That(sys.IsEntityInContainer(wires), Is.False);
+        Assert.That(rodStack, Has.Count.EqualTo(8));
+        Assert.That(wireStack, Has.Count.EqualTo(7));
 
-            await FindEntity(Spear, shouldSucceed: false);
-        });
+        await FindEntity(Spear, shouldSucceed: false);
 
         // Cancel the DoAfter. Should drop ingredients to the floor.
         await CancelDoAfters();
-        Assert.Multiple(async () =>
-        {
-            Assert.That(sys.IsEntityInContainer(rods), Is.False);
-            Assert.That(sys.IsEntityInContainer(wires), Is.False);
-            Assert.That(sys.IsEntityInContainer(shard), Is.False);
-            await FindEntity(Spear, shouldSucceed: false);
-            await AssertEntityLookup((Rod, 10), (Cable, 10), (ShardGlass, 1));
-        });
+        Assert.That(sys.IsEntityInContainer(rods), Is.False);
+        Assert.That(sys.IsEntityInContainer(wires), Is.False);
+        Assert.That(sys.IsEntityInContainer(shard), Is.False);
+        await FindEntity(Spear, shouldSucceed: false);
+        await AssertEntityLookup((Rod, 10), (Cable, 10), (ShardGlass, 1));
 
         // Re-attempt the do-after
 #pragma warning disable CS4014 // Legacy construction code uses DoAfterAwait. See above.
@@ -123,24 +112,17 @@ public async Task CancelCraft()
         await RunTicks(1);
 
         // DoAfter is in progress. Entity not spawned, ingredients are in a container.
-        Assert.Multiple(async () =>
-        {
-            Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1));
-            Assert.That(sys.IsEntityInContainer(shard), Is.True);
-            await FindEntity(Spear, shouldSucceed: false);
-        });
+        Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1));
+        Assert.That(sys.IsEntityInContainer(shard), Is.True);
+        await FindEntity(Spear, shouldSucceed: false);
 
         // Finish the DoAfter
         await AwaitDoAfters();
 
         // Spear has been crafted. Rods and wires are no longer contained. Glass has been consumed.
-        Assert.Multiple(async () =>
-        {
-            await FindEntity(Spear);
-            Assert.That(sys.IsEntityInContainer(rods), Is.False);
-            Assert.That(sys.IsEntityInContainer(wires), Is.False);
-            Assert.That(SEntMan.Deleted(shard));
-        });
+        await FindEntity(Spear);
+        Assert.That(sys.IsEntityInContainer(rods), Is.False);
+        Assert.That(sys.IsEntityInContainer(wires), Is.False);
+        Assert.That(SEntMan.Deleted(shard));
     }
-#endif
 }
diff --git a/Content.IntegrationTests/Tests/Construction/Interaction/GrilleWindowConstruction.cs b/Content.IntegrationTests/Tests/Construction/Interaction/GrilleWindowConstruction.cs
index 0de39d27577..ef6a7b09ae3 100644
--- a/Content.IntegrationTests/Tests/Construction/Interaction/GrilleWindowConstruction.cs
+++ b/Content.IntegrationTests/Tests/Construction/Interaction/GrilleWindowConstruction.cs
@@ -17,17 +17,14 @@ public async Task WindowOnGrille()
     {
         // Construct Grille
         await StartConstruction(Grille);
-        await Interact(Rod, 10);
-        ClientAssertPrototype(Grille, ClientTarget);
-
-        Target = CTestSystem.Ghosts[ClientTarget!.Value.GetHashCode()];
+        await InteractUsing(Rod, 10);
+        ClientAssertPrototype(Grille, Target);
         var grille = Target;
 
         // Construct Window
         await StartConstruction(Window);
-        await Interact(Glass, 10);
-        ClientAssertPrototype(Window, ClientTarget);
-        Target = CTestSystem.Ghosts[ClientTarget!.Value.GetHashCode()];
+        await InteractUsing(Glass, 10);
+        ClientAssertPrototype(Window, Target);
 
         // Deconstruct Window
         await Interact(Screw, Wrench);
@@ -35,7 +32,7 @@ public async Task WindowOnGrille()
 
         // Deconstruct Grille
         Target = grille;
-        await Interact(Cut);
+        await InteractUsing(Cut);
         AssertDeleted();
     }
 
diff --git a/Content.IntegrationTests/Tests/Construction/Interaction/MachineConstruction.cs b/Content.IntegrationTests/Tests/Construction/Interaction/MachineConstruction.cs
index cd95a85f205..98db51b4078 100644
--- a/Content.IntegrationTests/Tests/Construction/Interaction/MachineConstruction.cs
+++ b/Content.IntegrationTests/Tests/Construction/Interaction/MachineConstruction.cs
@@ -14,9 +14,8 @@ public sealed class MachineConstruction : InteractionTest
     public async Task ConstructProtolathe()
     {
         await StartConstruction(MachineFrame);
-        await Interact(Steel, 5);
-        ClientAssertPrototype(Unfinished, ClientTarget);
-        Target = CTestSystem.Ghosts[ClientTarget!.Value.GetHashCode()];
+        await InteractUsing(Steel, 5);
+        ClientAssertPrototype(Unfinished, Target);
         await Interact(Wrench, Cable);
         AssertPrototype(MachineFrame);
         await Interact(ProtolatheBoard, Bin1, Bin1, Manipulator1, Manipulator1, Beaker, Beaker, Screw);
@@ -51,7 +50,7 @@ public async Task ChangeMachine()
         AssertPrototype(MachineFrame);
 
         // Change it into an autolathe
-        await Interact("AutolatheMachineCircuitboard");
+        await InteractUsing("AutolatheMachineCircuitboard");
         AssertPrototype(MachineFrame);
         await Interact(Bin1, Bin1, Bin1, Manipulator1, Glass, Screw);
         AssertPrototype("Autolathe");
diff --git a/Content.IntegrationTests/Tests/Construction/Interaction/PanelScrewing.cs b/Content.IntegrationTests/Tests/Construction/Interaction/PanelScrewing.cs
index b6d960e2882..636d58bf96f 100644
--- a/Content.IntegrationTests/Tests/Construction/Interaction/PanelScrewing.cs
+++ b/Content.IntegrationTests/Tests/Construction/Interaction/PanelScrewing.cs
@@ -19,21 +19,21 @@ public async Task WiresPanelScrewing(string prototype)
 
         // Open & close panel
         Assert.That(comp.Open, Is.False);
-        await Interact(Screw);
+        await InteractUsing(Screw);
         Assert.That(comp.Open, Is.True);
-        await Interact(Screw);
+        await InteractUsing(Screw);
         Assert.That(comp.Open, Is.False);
 
         // Interrupted DoAfters
-        await Interact(Screw, awaitDoAfters: false);
+        await InteractUsing(Screw, awaitDoAfters: false);
         await CancelDoAfters();
         Assert.That(comp.Open, Is.False);
-        await Interact(Screw);
+        await InteractUsing(Screw);
         Assert.That(comp.Open, Is.True);
-        await Interact(Screw, awaitDoAfters: false);
+        await InteractUsing(Screw, awaitDoAfters: false);
         await CancelDoAfters();
         Assert.That(comp.Open, Is.True);
-        await Interact(Screw);
+        await InteractUsing(Screw);
         Assert.That(comp.Open, Is.False);
     }
 }
diff --git a/Content.IntegrationTests/Tests/Construction/Interaction/PlaceableDeconstruction.cs b/Content.IntegrationTests/Tests/Construction/Interaction/PlaceableDeconstruction.cs
index bc0cb9bcef3..783c14c0682 100644
--- a/Content.IntegrationTests/Tests/Construction/Interaction/PlaceableDeconstruction.cs
+++ b/Content.IntegrationTests/Tests/Construction/Interaction/PlaceableDeconstruction.cs
@@ -13,9 +13,9 @@ public async Task DeconstructTable()
     {
         await StartDeconstruction("Table");
         Assert.That(Comp<PlaceableSurfaceComponent>().IsPlaceable);
-        await Interact(Wrench);
+        await InteractUsing(Wrench);
         AssertPrototype("TableFrame");
-        await Interact(Wrench);
+        await InteractUsing(Wrench);
         AssertDeleted();
         await AssertEntityLookup((Steel, 1), (Rod, 2));
     }
diff --git a/Content.IntegrationTests/Tests/Construction/Interaction/WallConstruction.cs b/Content.IntegrationTests/Tests/Construction/Interaction/WallConstruction.cs
index 67a2f8025dc..292bf0c55ab 100644
--- a/Content.IntegrationTests/Tests/Construction/Interaction/WallConstruction.cs
+++ b/Content.IntegrationTests/Tests/Construction/Interaction/WallConstruction.cs
@@ -12,11 +12,10 @@ public sealed class WallConstruction : InteractionTest
     public async Task ConstructWall()
     {
         await StartConstruction(Wall);
-        await Interact(Steel, 2);
+        await InteractUsing(Steel, 2);
         Assert.That(Hands.ActiveHandEntity, Is.Null);
-        ClientAssertPrototype(Girder, ClientTarget);
-        Target = CTestSystem.Ghosts[ClientTarget!.Value.GetHashCode()];
-        await Interact(Steel, 2);
+        ClientAssertPrototype(Girder, Target);
+        await InteractUsing(Steel, 2);
         Assert.That(Hands.ActiveHandEntity, Is.Null);
         AssertPrototype(WallSolid);
     }
@@ -25,7 +24,7 @@ public async Task ConstructWall()
     public async Task DeconstructWall()
     {
         await StartDeconstruction(WallSolid);
-        await Interact(Weld);
+        await InteractUsing(Weld);
         AssertPrototype(Girder);
         await Interact(Wrench, Screw);
         AssertDeleted();
diff --git a/Content.IntegrationTests/Tests/Construction/Interaction/WindowConstruction.cs b/Content.IntegrationTests/Tests/Construction/Interaction/WindowConstruction.cs
index 46bb892ed99..2ece6b3e397 100644
--- a/Content.IntegrationTests/Tests/Construction/Interaction/WindowConstruction.cs
+++ b/Content.IntegrationTests/Tests/Construction/Interaction/WindowConstruction.cs
@@ -11,8 +11,8 @@ public sealed class WindowConstruction : InteractionTest
     public async Task ConstructWindow()
     {
         await StartConstruction(Window);
-        await Interact(Glass, 5);
-        ClientAssertPrototype(Window, ClientTarget);
+        await InteractUsing(Glass, 5);
+        ClientAssertPrototype(Window, Target);
     }
 
     [Test]
@@ -28,8 +28,8 @@ public async Task DeconstructWindow()
     public async Task ConstructReinforcedWindow()
     {
         await StartConstruction(RWindow);
-        await Interact(RGlass, 5);
-        ClientAssertPrototype(RWindow, ClientTarget);
+        await InteractUsing(RGlass, 5);
+        ClientAssertPrototype(RWindow, Target);
     }
 
     [Test]
diff --git a/Content.IntegrationTests/Tests/Construction/Interaction/WindowRepair.cs b/Content.IntegrationTests/Tests/Construction/Interaction/WindowRepair.cs
index abd4bc265b3..6eea519af3b 100644
--- a/Content.IntegrationTests/Tests/Construction/Interaction/WindowRepair.cs
+++ b/Content.IntegrationTests/Tests/Construction/Interaction/WindowRepair.cs
@@ -24,7 +24,7 @@ public async Task RepairReinforcedWindow()
         Assert.That(comp.Damage.GetTotal(), Is.GreaterThan(FixedPoint2.Zero));
 
         // Repair the entity
-        await Interact(Weld);
+        await InteractUsing(Weld);
         Assert.That(comp.Damage.GetTotal(), Is.EqualTo(FixedPoint2.Zero));
 
         // Validate that we can still deconstruct the entity (i.e., that welding deconstruction is not blocked).
diff --git a/Content.IntegrationTests/Tests/ContainerOcclusionTest.cs b/Content.IntegrationTests/Tests/ContainerOcclusionTest.cs
index c61a70faf0b..c907f6bb1f3 100644
--- a/Content.IntegrationTests/Tests/ContainerOcclusionTest.cs
+++ b/Content.IntegrationTests/Tests/ContainerOcclusionTest.cs
@@ -43,11 +43,11 @@ public async Task TestA()
 
             EntityUid dummy = default;
             var mapManager = server.ResolveDependency<IMapManager>();
-            var mapId = mapManager.CreateMap();
+            var map = await pair.CreateTestMap();
 
             await server.WaitPost(() =>
             {
-                var pos = new MapCoordinates(Vector2.Zero, mapId);
+                var pos = new MapCoordinates(Vector2.Zero, map.MapId);
                 var entStorage = serverEntManager.EntitySysManager.GetEntitySystem<EntityStorageSystem>();
                 var container = serverEntManager.SpawnEntity("ContainerOcclusionA", pos);
                 dummy = serverEntManager.SpawnEntity("ContainerOcclusionDummy", pos);
@@ -85,11 +85,12 @@ public async Task TestB()
 
             EntityUid dummy = default;
             var mapManager = server.ResolveDependency<IMapManager>();
-            var mapId = mapManager.CreateMap();
+
+            var map = await pair.CreateTestMap();
 
             await server.WaitPost(() =>
             {
-                var pos = new MapCoordinates(Vector2.Zero, mapId);
+                var pos = new MapCoordinates(Vector2.Zero, map.MapId);
                 var entStorage = serverEntManager.EntitySysManager.GetEntitySystem<EntityStorageSystem>();
                 var container = serverEntManager.SpawnEntity("ContainerOcclusionB", pos);
                 dummy = serverEntManager.SpawnEntity("ContainerOcclusionDummy", pos);
@@ -99,10 +100,12 @@ await server.WaitPost(() =>
 
             await pair.RunTicksSync(5);
 
-            var clientEnt = clientEntManager.GetEntity(serverEntManager.GetNetEntity(dummy));
+            EntityUid clientEnt = default!;
 
             await client.WaitAssertion(() =>
             {
+                clientEnt = clientEntManager.GetEntity(serverEntManager.GetNetEntity(dummy));
+
                 var sprite = clientEntManager.GetComponent<SpriteComponent>(clientEnt);
                 var light = clientEntManager.GetComponent<PointLightComponent>(clientEnt);
                 Assert.Multiple(() =>
@@ -127,11 +130,12 @@ public async Task TestAb()
 
             EntityUid dummy = default;
             var mapManager = server.ResolveDependency<IMapManager>();
-            var mapId = mapManager.CreateMap();
+
+            var map = await pair.CreateTestMap();
 
             await server.WaitPost(() =>
             {
-                var pos = new MapCoordinates(Vector2.Zero, mapId);
+                var pos = new MapCoordinates(Vector2.Zero, map.MapId);
                 var entStorage = serverEntManager.EntitySysManager.GetEntitySystem<EntityStorageSystem>();
                 var containerA = serverEntManager.SpawnEntity("ContainerOcclusionA", pos);
                 var containerB = serverEntManager.SpawnEntity("ContainerOcclusionB", pos);
diff --git a/Content.IntegrationTests/Tests/Damageable/DamageSpecifierTest.cs b/Content.IntegrationTests/Tests/Damageable/DamageSpecifierTest.cs
index 41d17ddedae..bd5cac05dd1 100644
--- a/Content.IntegrationTests/Tests/Damageable/DamageSpecifierTest.cs
+++ b/Content.IntegrationTests/Tests/Damageable/DamageSpecifierTest.cs
@@ -14,39 +14,39 @@ public void TestDamageSpecifierOperations()
         // Test basic math operations.
         // I've already nearly broken these once. When editing the operators.
 
-        DamageSpecifier input1 = new() { DamageDict = _input1 };
-        DamageSpecifier input2 = new() { DamageDict = _input2 };
-        DamageSpecifier output1 = new() { DamageDict = _output1 };
-        DamageSpecifier output2 = new() { DamageDict = _output2 };
-        DamageSpecifier output3 = new() { DamageDict = _output3 };
-        DamageSpecifier output4 = new() { DamageDict = _output4 };
-        DamageSpecifier output5 = new() { DamageDict = _output5 };
+        DamageSpecifier input1 = new() { DamageDict = Input1 };
+        DamageSpecifier input2 = new() { DamageDict = Input2 };
+        DamageSpecifier output1 = new() { DamageDict = Output1 };
+        DamageSpecifier output2 = new() { DamageDict = Output2 };
+        DamageSpecifier output3 = new() { DamageDict = Output3 };
+        DamageSpecifier output4 = new() { DamageDict = Output4 };
+        DamageSpecifier output5 = new() { DamageDict = Output5 };
 
         Assert.Multiple(() =>
         {
-            Assert.That((-input1).Equals(output1));
-            Assert.That((input1 / 2).Equals(output2));
-            Assert.That((input1 * 2).Equals(output3));
+            Assert.That(-input1, Is.EqualTo(output1));
+            Assert.That(input1 / 2, Is.EqualTo(output2));
+            Assert.That(input1 * 2, Is.EqualTo(output3));
         });
 
-        var difference = (input1 - input2);
-        Assert.That(difference.Equals(output4));
+        var difference = input1 - input2;
+        Assert.That(difference, Is.EqualTo(output4));
 
-        var difference2 = (-input2) + input1;
-        Assert.That(difference.Equals(difference2));
+        var difference2 = -input2 + input1;
+        Assert.That(difference, Is.EqualTo(difference2));
 
         difference.Clamp(-0.25f, 0.25f);
-        Assert.That(difference.Equals(output5));
+        Assert.That(difference, Is.EqualTo(output5));
     }
 
-    static Dictionary<string, FixedPoint2> _input1 = new()
+    private static readonly Dictionary<string, FixedPoint2> Input1 = new()
     {
         { "A", 1.5f },
         { "B", 2 },
         { "C", 3 }
     };
 
-    static Dictionary<string, FixedPoint2> _input2 = new()
+    private static readonly Dictionary<string, FixedPoint2> Input2 = new()
     {
         { "A", 1 },
         { "B", 2 },
@@ -54,28 +54,28 @@ public void TestDamageSpecifierOperations()
         { "D", 0.05f }
     };
 
-    static Dictionary<string, FixedPoint2> _output1 = new()
+    private static readonly Dictionary<string, FixedPoint2> Output1 = new()
     {
         { "A", -1.5f },
         { "B", -2 },
         { "C", -3 }
     };
 
-    static Dictionary<string, FixedPoint2> _output2 = new()
+    private static readonly Dictionary<string, FixedPoint2> Output2 = new()
     {
         { "A", 0.75f },
         { "B", 1 },
         { "C", 1.5 }
     };
 
-    static Dictionary<string, FixedPoint2> _output3 = new()
+    private static readonly Dictionary<string, FixedPoint2> Output3 = new()
     {
         { "A", 3f },
         { "B", 4 },
         { "C", 6 }
     };
 
-    static Dictionary<string, FixedPoint2> _output4 = new()
+    private static readonly Dictionary<string, FixedPoint2> Output4 = new()
     {
         { "A", 0.5f },
         { "B", 0 },
@@ -83,7 +83,7 @@ public void TestDamageSpecifierOperations()
         { "D", -0.05f }
     };
 
-    static Dictionary<string, FixedPoint2> _output5 = new()
+    private static readonly Dictionary<string, FixedPoint2> Output5 = new()
     {
         { "A", 0.25f },
         { "B", 0 },
diff --git a/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs b/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs
index c40b8ed286f..69069fc82fe 100644
--- a/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs
+++ b/Content.IntegrationTests/Tests/Damageable/DamageableTest.cs
@@ -107,10 +107,11 @@ public async Task TestDamageableComponents()
 
             FixedPoint2 typeDamage;
 
+            var map = await pair.CreateTestMap();
+
             await server.WaitPost(() =>
             {
-                var map = sMapManager.CreateMap();
-                var coordinates = new MapCoordinates(0, 0, map);
+                var coordinates = map.MapCoords;
 
                 sDamageableEntity = sEntityManager.SpawnEntity("TestDamageableEntityId", coordinates);
                 sDamageableComponent = sEntityManager.GetComponent<DamageableComponent>(sDamageableEntity);
diff --git a/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs b/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs
index e14a8264678..a50238d8f50 100644
--- a/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs
+++ b/Content.IntegrationTests/Tests/Destructible/DestructibleDestructionTest.cs
@@ -3,6 +3,7 @@
 using Content.Server.Destructible.Thresholds.Behaviors;
 using Content.Shared.Damage;
 using Content.Shared.Damage.Prototypes;
+using Content.Shared.Destructible.Thresholds;
 using Robust.Shared.GameObjects;
 using Robust.Shared.Prototypes;
 using static Content.IntegrationTests.Tests.Destructible.DestructibleTestPrototypes;
diff --git a/Content.IntegrationTests/Tests/DoAfter/DoAfterCancellationTests.cs b/Content.IntegrationTests/Tests/DoAfter/DoAfterCancellationTests.cs
index 0ebd17d8879..1aaf4a5184c 100644
--- a/Content.IntegrationTests/Tests/DoAfter/DoAfterCancellationTests.cs
+++ b/Content.IntegrationTests/Tests/DoAfter/DoAfterCancellationTests.cs
@@ -16,31 +16,31 @@ public sealed class DoAfterCancellationTests : InteractionTest
     public async Task CancelWallDeconstruct()
     {
         await StartDeconstruction(WallConstruction.WallSolid);
-        await Interact(Weld, awaitDoAfters: false);
+        await InteractUsing(Weld, awaitDoAfters: false);
 
         // Failed do-after has no effect
         await CancelDoAfters();
         AssertPrototype(WallConstruction.WallSolid);
 
         // Second attempt works fine
-        await Interact(Weld);
+        await InteractUsing(Weld);
         AssertPrototype(WallConstruction.Girder);
 
         // Repeat for wrenching interaction
         AssertAnchored();
-        await Interact(Wrench, awaitDoAfters: false);
+        await InteractUsing(Wrench, awaitDoAfters: false);
         await CancelDoAfters();
         AssertAnchored();
         AssertPrototype(WallConstruction.Girder);
-        await Interact(Wrench);
+        await InteractUsing(Wrench);
         AssertAnchored(false);
 
         // Repeat for screwdriver interaction.
         AssertExists();
-        await Interact(Screw, awaitDoAfters: false);
+        await InteractUsing(Screw, awaitDoAfters: false);
         await CancelDoAfters();
         AssertExists();
-        await Interact(Screw);
+        await InteractUsing(Screw);
         AssertDeleted();
     }
 
@@ -48,17 +48,16 @@ public async Task CancelWallDeconstruct()
     public async Task CancelWallConstruct()
     {
         await StartConstruction(WallConstruction.Wall);
-        await Interact(Steel, 5, awaitDoAfters: false);
+        await InteractUsing(Steel, 5, awaitDoAfters: false);
         await CancelDoAfters();
 
-        await Interact(Steel, 5);
-        ClientAssertPrototype(WallConstruction.Girder, ClientTarget);
-        Target = CTestSystem.Ghosts[ClientTarget!.Value.GetHashCode()];
-        await Interact(Steel, 5, awaitDoAfters: false);
+        await InteractUsing(Steel, 5);
+        ClientAssertPrototype(WallConstruction.Girder, Target);
+        await InteractUsing(Steel, 5, awaitDoAfters: false);
         await CancelDoAfters();
         AssertPrototype(WallConstruction.Girder);
 
-        await Interact(Steel, 5);
+        await InteractUsing(Steel, 5);
         AssertPrototype(WallConstruction.WallSolid);
     }
 
@@ -66,11 +65,11 @@ public async Task CancelWallConstruct()
     public async Task CancelTilePry()
     {
         await SetTile(Floor);
-        await Interact(Pry, awaitDoAfters: false);
+        await InteractUsing(Pry, awaitDoAfters: false);
         await CancelDoAfters();
         await AssertTile(Floor);
 
-        await Interact(Pry);
+        await InteractUsing(Pry);
         await AssertTile(Plating);
     }
 
@@ -78,7 +77,7 @@ public async Task CancelTilePry()
     public async Task CancelRepeatedTilePry()
     {
         await SetTile(Floor);
-        await Interact(Pry, awaitDoAfters: false);
+        await InteractUsing(Pry, awaitDoAfters: false);
         await RunTicks(1);
         Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1));
         await AssertTile(Floor);
@@ -89,7 +88,7 @@ public async Task CancelRepeatedTilePry()
         await AssertTile(Floor);
 
         // Third do after will work fine
-        await Interact(Pry);
+        await InteractUsing(Pry);
         Assert.That(ActiveDoAfters.Count(), Is.EqualTo(0));
         await AssertTile(Plating);
     }
@@ -102,7 +101,7 @@ public async Task CancelRepeatedWeld()
 
         Assert.That(comp.IsWelded, Is.False);
 
-        await Interact(Weld, awaitDoAfters: false);
+        await InteractUsing(Weld, awaitDoAfters: false);
         await RunTicks(1);
         Assert.Multiple(() =>
         {
@@ -120,7 +119,7 @@ public async Task CancelRepeatedWeld()
         });
 
         // Third do after will work fine
-        await Interact(Weld);
+        await InteractUsing(Weld);
         Assert.Multiple(() =>
         {
             Assert.That(ActiveDoAfters.Count(), Is.EqualTo(0));
@@ -128,7 +127,7 @@ public async Task CancelRepeatedWeld()
         });
 
         // Repeat test for un-welding
-        await Interact(Weld, awaitDoAfters: false);
+        await InteractUsing(Weld, awaitDoAfters: false);
         await RunTicks(1);
         Assert.Multiple(() =>
         {
@@ -141,7 +140,7 @@ public async Task CancelRepeatedWeld()
             Assert.That(ActiveDoAfters.Count(), Is.EqualTo(0));
             Assert.That(comp.IsWelded, Is.True);
         });
-        await Interact(Weld);
+        await InteractUsing(Weld);
         Assert.Multiple(() =>
         {
             Assert.That(ActiveDoAfters.Count(), Is.EqualTo(0));
diff --git a/Content.IntegrationTests/Tests/Doors/AirlockTest.cs b/Content.IntegrationTests/Tests/Doors/AirlockTest.cs
index 9f31231091f..fb77bf18d83 100644
--- a/Content.IntegrationTests/Tests/Doors/AirlockTest.cs
+++ b/Content.IntegrationTests/Tests/Doors/AirlockTest.cs
@@ -123,24 +123,24 @@ public async Task AirlockBlockTest()
             var xformSystem = entityManager.System<SharedTransformSystem>();
 
             PhysicsComponent physBody = null;
-            EntityUid AirlockPhysicsDummy = default;
+            EntityUid airlockPhysicsDummy = default;
             EntityUid airlock = default;
             DoorComponent doorComponent = null;
 
-            var AirlockPhysicsDummyStartingX = -1;
+            var airlockPhysicsDummyStartingX = -1;
+
+            var map = await pair.CreateTestMap();
 
             await server.WaitAssertion(() =>
             {
-                var mapId = mapManager.CreateMap();
-
-                var humanCoordinates = new MapCoordinates(new Vector2(AirlockPhysicsDummyStartingX, 0), mapId);
-                AirlockPhysicsDummy = entityManager.SpawnEntity("AirlockPhysicsDummy", humanCoordinates);
+                var humanCoordinates = new MapCoordinates(new Vector2(airlockPhysicsDummyStartingX, 0), map.MapId);
+                airlockPhysicsDummy = entityManager.SpawnEntity("AirlockPhysicsDummy", humanCoordinates);
 
-                airlock = entityManager.SpawnEntity("AirlockDummy", new MapCoordinates(new Vector2(0, 0), mapId));
+                airlock = entityManager.SpawnEntity("AirlockDummy", new MapCoordinates(new Vector2(0, 0), map.MapId));
 
                 Assert.Multiple(() =>
                 {
-                    Assert.That(entityManager.TryGetComponent(AirlockPhysicsDummy, out physBody), Is.True);
+                    Assert.That(entityManager.TryGetComponent(airlockPhysicsDummy, out physBody), Is.True);
                     Assert.That(entityManager.TryGetComponent(airlock, out doorComponent), Is.True);
                 });
                 Assert.That(doorComponent.State, Is.EqualTo(DoorState.Closed));
@@ -152,7 +152,7 @@ await server.WaitAssertion(() =>
             await server.WaitAssertion(() => Assert.That(physBody, Is.Not.EqualTo(null)));
             await server.WaitPost(() =>
             {
-                physicsSystem.SetLinearVelocity(AirlockPhysicsDummy, new Vector2(0.5f, 0f), body: physBody);
+                physicsSystem.SetLinearVelocity(airlockPhysicsDummy, new Vector2(0.5f, 0f), body: physBody);
             });
 
             for (var i = 0; i < 240; i += 10)
@@ -176,7 +176,7 @@ await server.WaitPost(() =>
             // Blocked by the airlock
             await server.WaitAssertion(() =>
             {
-                Assert.That(Math.Abs(xformSystem.GetWorldPosition(AirlockPhysicsDummy).X - 1), Is.GreaterThan(0.01f));
+                Assert.That(Math.Abs(xformSystem.GetWorldPosition(airlockPhysicsDummy).X - 1), Is.GreaterThan(0.01f));
             });
             await pair.CleanReturnAsync();
         }
diff --git a/Content.IntegrationTests/Tests/DummyIconTest.cs b/Content.IntegrationTests/Tests/DummyIconTest.cs
index a11191a51ea..df2d28a2ea2 100644
--- a/Content.IntegrationTests/Tests/DummyIconTest.cs
+++ b/Content.IntegrationTests/Tests/DummyIconTest.cs
@@ -21,7 +21,7 @@ await client.WaitAssertion(() =>
             {
                 foreach (var proto in prototypeManager.EnumeratePrototypes<EntityPrototype>())
                 {
-                    if (proto.NoSpawn || proto.Abstract || pair.IsTestPrototype(proto) || !proto.Components.ContainsKey("Sprite"))
+                    if (proto.HideSpawnMenu || proto.Abstract || pair.IsTestPrototype(proto) || !proto.Components.ContainsKey("Sprite"))
                         continue;
 
                     Assert.DoesNotThrow(() =>
diff --git a/Content.IntegrationTests/Tests/EncryptionKeys/RemoveEncryptionKeys.cs b/Content.IntegrationTests/Tests/EncryptionKeys/RemoveEncryptionKeys.cs
index 9e3dbd8863e..f5e8c22242e 100644
--- a/Content.IntegrationTests/Tests/EncryptionKeys/RemoveEncryptionKeys.cs
+++ b/Content.IntegrationTests/Tests/EncryptionKeys/RemoveEncryptionKeys.cs
@@ -22,7 +22,7 @@ public async Task HeadsetKeys()
         });
 
         // Remove the key
-        await Interact(Screw);
+        await InteractUsing(Screw);
         Assert.Multiple(() =>
         {
             Assert.That(comp.KeyContainer.ContainedEntities, Has.Count.EqualTo(0));
@@ -34,7 +34,7 @@ public async Task HeadsetKeys()
         await AssertEntityLookup(("EncryptionKeyCommon", 1));
 
         // Re-insert a key.
-        await Interact("EncryptionKeyCentCom");
+        await InteractUsing("EncryptionKeyCentCom");
         Assert.Multiple(() =>
         {
             Assert.That(comp.KeyContainer.ContainedEntities, Has.Count.EqualTo(1));
@@ -59,7 +59,7 @@ public async Task CommsServerKeys()
         });
 
         // cannot remove keys without opening panel
-        await Interact(Pry);
+        await InteractUsing(Pry);
         Assert.Multiple(() =>
         {
             Assert.That(comp.KeyContainer.ContainedEntities, Has.Count.GreaterThan(0));
@@ -68,7 +68,7 @@ public async Task CommsServerKeys()
         });
 
         // Open panel
-        await Interact(Screw);
+        await InteractUsing(Screw);
         Assert.Multiple(() =>
         {
             Assert.That(panel.Open, Is.True);
@@ -79,7 +79,7 @@ public async Task CommsServerKeys()
         });
 
         // Now remove the keys
-        await Interact(Pry);
+        await InteractUsing(Pry);
         Assert.Multiple(() =>
         {
             Assert.That(comp.KeyContainer.ContainedEntities, Has.Count.EqualTo(0));
@@ -87,7 +87,7 @@ public async Task CommsServerKeys()
         });
 
         // Reinsert a key
-        await Interact("EncryptionKeyCentCom");
+        await InteractUsing("EncryptionKeyCentCom");
         Assert.Multiple(() =>
         {
             Assert.That(comp.KeyContainer.ContainedEntities, Has.Count.EqualTo(1));
@@ -97,7 +97,7 @@ public async Task CommsServerKeys()
         });
 
         // Remove it again
-        await Interact(Pry);
+        await InteractUsing(Pry);
         Assert.Multiple(() =>
         {
             Assert.That(comp.KeyContainer.ContainedEntities, Has.Count.EqualTo(0));
@@ -106,7 +106,7 @@ public async Task CommsServerKeys()
 
         // Prying again will start deconstructing the machine.
         AssertPrototype("TelecomServerFilled");
-        await Interact(Pry);
+        await InteractUsing(Pry);
         AssertPrototype("MachineFrame");
     }
 }
diff --git a/Content.IntegrationTests/Tests/EntityTest.cs b/Content.IntegrationTests/Tests/EntityTest.cs
index 926374cf050..56645660673 100644
--- a/Content.IntegrationTests/Tests/EntityTest.cs
+++ b/Content.IntegrationTests/Tests/EntityTest.cs
@@ -1,15 +1,11 @@
 using System.Collections.Generic;
 using System.Linq;
 using System.Numerics;
-using Content.Server.Humanoid.Components;
-using Content.Shared.Coordinates;
-using Content.Shared.Prototypes;
 using Robust.Shared;
 using Robust.Shared.Configuration;
 using Robust.Shared.GameObjects;
 using Robust.Shared.Log;
 using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
 using Robust.Shared.Maths;
 using Robust.Shared.Prototypes;
 
@@ -47,7 +43,7 @@ await server.WaitPost(() =>
 
                 foreach (var protoId in protoIds)
                 {
-                    var mapId = mapManager.CreateMap();
+                    mapSystem.CreateMap(out var mapId);
                     var grid = mapManager.CreateGridEntity(mapId);
                     // TODO: Fix this better in engine.
                     mapSystem.SetTile(grid.Owner, grid.Comp, Vector2i.Zero, new Tile(1));
@@ -155,6 +151,7 @@ public async Task SpawnAndDirtyAllEntities()
             var prototypeMan = server.ResolveDependency<IPrototypeManager>();
             var mapManager = server.ResolveDependency<IMapManager>();
             var sEntMan = server.ResolveDependency<IEntityManager>();
+            var mapSys = server.System<SharedMapSystem>();
 
             Assert.That(cfg.GetCVar(CVars.NetPVS), Is.False);
 
@@ -170,7 +167,7 @@ await server.WaitPost(() =>
             {
                 foreach (var protoId in protoIds)
                 {
-                    var mapId = mapManager.CreateMap();
+                    mapSys.CreateMap(out var mapId);
                     var grid = mapManager.CreateGridEntity(mapId);
                     var ent = sEntMan.SpawnEntity(protoId, new EntityCoordinates(grid.Owner, 0.5f, 0.5f));
                     foreach (var (_, component) in sEntMan.GetNetComponents(ent))
@@ -227,6 +224,7 @@ public async Task SpawnAndDeleteEntityCountTest()
             var settings = new PoolSettings { Connected = true, Dirty = true };
             await using var pair = await PoolManager.GetServerClient(settings);
             var mapManager = pair.Server.ResolveDependency<IMapManager>();
+            var mapSys = pair.Server.System<SharedMapSystem>();
             var server = pair.Server;
             var client = pair.Client;
 
@@ -256,7 +254,7 @@ public async Task SpawnAndDeleteEntityCountTest()
 
             await server.WaitPost(() =>
             {
-               mapId = mapManager.CreateMap();
+                mapSys.CreateMap(out mapId);
             });
 
             var coords = new MapCoordinates(Vector2.Zero, mapId);
diff --git a/Content.IntegrationTests/Tests/Fluids/FluidSpillTest.cs b/Content.IntegrationTests/Tests/Fluids/FluidSpillTest.cs
index 6e88d6928e6..d6f9bf35986 100644
--- a/Content.IntegrationTests/Tests/Fluids/FluidSpillTest.cs
+++ b/Content.IntegrationTests/Tests/Fluids/FluidSpillTest.cs
@@ -16,14 +16,15 @@ namespace Content.IntegrationTests.Tests.Fluids;
 [TestOf(typeof(SpreaderSystem))]
 public sealed class FluidSpill
 {
-    private static PuddleComponent? GetPuddle(IEntityManager entityManager, MapGridComponent mapGrid, Vector2i pos)
+    private static PuddleComponent? GetPuddle(IEntityManager entityManager, Entity<MapGridComponent> mapGrid, Vector2i pos)
     {
         return GetPuddleEntity(entityManager, mapGrid, pos)?.Comp;
     }
 
-    private static Entity<PuddleComponent>? GetPuddleEntity(IEntityManager entityManager, MapGridComponent mapGrid, Vector2i pos)
+    private static Entity<PuddleComponent>? GetPuddleEntity(IEntityManager entityManager, Entity<MapGridComponent> mapGrid, Vector2i pos)
     {
-        foreach (var uid in mapGrid.GetAnchoredEntities(pos))
+        var mapSys = entityManager.System<SharedMapSystem>();
+        foreach (var uid in mapSys.GetAnchoredEntities(mapGrid, mapGrid.Comp, pos))
         {
             if (entityManager.TryGetComponent(uid, out PuddleComponent? puddleComponent))
                 return (uid, puddleComponent);
@@ -39,9 +40,9 @@ public async Task SpillCorner()
         var server = pair.Server;
         var mapManager = server.ResolveDependency<IMapManager>();
         var entityManager = server.ResolveDependency<IEntityManager>();
-        var puddleSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<PuddleSystem>();
+        var puddleSystem = server.System<PuddleSystem>();
+        var mapSystem = server.System<SharedMapSystem>();
         var gameTiming = server.ResolveDependency<IGameTiming>();
-        MapId mapId;
         EntityUid gridId = default;
 
         /*
@@ -52,7 +53,7 @@ . . .
         */
         await server.WaitPost(() =>
         {
-            mapId = mapManager.CreateMap();
+            mapSystem.CreateMap(out var mapId);
             var grid = mapManager.CreateGridEntity(mapId);
             gridId = grid.Owner;
 
@@ -60,12 +61,12 @@ await server.WaitPost(() =>
             {
                 for (var y = 0; y < 3; y++)
                 {
-                    grid.Comp.SetTile(new Vector2i(x, y), new Tile(1));
+                    mapSystem.SetTile(grid, new Vector2i(x, y), new Tile(1));
                 }
             }
 
-            entityManager.SpawnEntity("WallReinforced", grid.Comp.GridTileToLocal(new Vector2i(0, 1)));
-            entityManager.SpawnEntity("WallReinforced", grid.Comp.GridTileToLocal(new Vector2i(1, 0)));
+            entityManager.SpawnEntity("WallReinforced", mapSystem.GridTileToLocal(grid, grid.Comp, new Vector2i(0, 1)));
+            entityManager.SpawnEntity("WallReinforced", mapSystem.GridTileToLocal(grid, grid.Comp, new Vector2i(1, 0)));
         });
 
 
@@ -74,10 +75,10 @@ await server.WaitAssertion(() =>
         {
             var grid = entityManager.GetComponent<MapGridComponent>(gridId);
             var solution = new Solution("Blood", FixedPoint2.New(100));
-            var tileRef = grid.GetTileRef(puddleOrigin);
+            var tileRef = mapSystem.GetTileRef(gridId, grid, puddleOrigin);
 #pragma warning disable NUnit2045 // Interdependent tests
             Assert.That(puddleSystem.TrySpillAt(tileRef, solution, out _), Is.True);
-            Assert.That(GetPuddle(entityManager, grid, puddleOrigin), Is.Not.Null);
+            Assert.That(GetPuddle(entityManager, (gridId, grid), puddleOrigin), Is.Not.Null);
 #pragma warning restore NUnit2045
         });
 
@@ -87,7 +88,7 @@ await server.WaitAssertion(() =>
         await server.WaitAssertion(() =>
         {
             var grid = entityManager.GetComponent<MapGridComponent>(gridId);
-            var puddle = GetPuddleEntity(entityManager, grid, puddleOrigin);
+            var puddle = GetPuddleEntity(entityManager, (gridId, grid), puddleOrigin);
 
 #pragma warning disable NUnit2045 // Interdependent tests
             Assert.That(puddle, Is.Not.Null);
@@ -104,7 +105,7 @@ await server.WaitAssertion(() =>
                     }
 
                     var newPos = new Vector2i(x, y);
-                    var sidePuddle = GetPuddle(entityManager, grid, newPos);
+                    var sidePuddle = GetPuddle(entityManager, (gridId, grid), newPos);
                     Assert.That(sidePuddle, Is.Null);
                 }
             }
diff --git a/Content.IntegrationTests/Tests/Fluids/PuddleTest.cs b/Content.IntegrationTests/Tests/Fluids/PuddleTest.cs
index a9069892dff..ee2d0cb1f7a 100644
--- a/Content.IntegrationTests/Tests/Fluids/PuddleTest.cs
+++ b/Content.IntegrationTests/Tests/Fluids/PuddleTest.cs
@@ -5,7 +5,6 @@
 using Content.Shared.Fluids.Components;
 using Robust.Shared.GameObjects;
 using Robust.Shared.Map;
-using Robust.Shared.Map.Components;
 
 namespace Content.IntegrationTests.Tests.Fluids
 {
@@ -21,8 +20,7 @@ public async Task TilePuddleTest()
 
             var testMap = await pair.CreateTestMap();
 
-            var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
-            var spillSystem = entitySystemManager.GetEntitySystem<PuddleSystem>();
+            var spillSystem = server.System<PuddleSystem>();
 
             await server.WaitAssertion(() =>
             {
@@ -46,17 +44,19 @@ public async Task SpaceNoPuddleTest()
             var server = pair.Server;
 
             var testMap = await pair.CreateTestMap();
-            var grid = testMap.Grid.Comp;
+            var grid = testMap.Grid;
 
             var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
-            var spillSystem = entitySystemManager.GetEntitySystem<PuddleSystem>();
+            var spillSystem = server.System<PuddleSystem>();
+            var mapSystem = server.System<SharedMapSystem>();
 
             // Remove all tiles
             await server.WaitPost(() =>
             {
-                foreach (var tile in grid.GetAllTiles())
+                var tiles = mapSystem.GetAllTiles(grid.Owner, grid.Comp);
+                foreach (var tile in tiles)
                 {
-                    grid.SetTile(tile.GridIndices, Tile.Empty);
+                    mapSystem.SetTile(grid, tile.GridIndices, Tile.Empty);
                 }
             });
 
diff --git a/Content.IntegrationTests/Tests/FollowerSystemTest.cs b/Content.IntegrationTests/Tests/FollowerSystemTest.cs
index 4d308c6d911..f4447426c77 100644
--- a/Content.IntegrationTests/Tests/FollowerSystemTest.cs
+++ b/Content.IntegrationTests/Tests/FollowerSystemTest.cs
@@ -22,6 +22,7 @@ public async Task FollowerMapDeleteTest()
         var mapMan = server.ResolveDependency<IMapManager>();
         var sysMan = server.ResolveDependency<IEntitySystemManager>();
         var logMan = server.ResolveDependency<ILogManager>();
+        var mapSys = server.System<SharedMapSystem>();
         var logger = logMan.RootSawmill;
 
         await server.WaitPost(() =>
@@ -29,7 +30,7 @@ await server.WaitPost(() =>
             var followerSystem = sysMan.GetEntitySystem<FollowerSystem>();
 
             // Create a map to spawn the observers on.
-            var map = mapMan.CreateMap();
+            mapSys.CreateMap(out var map);
 
             // Spawn an observer to be followed.
             var followed = entMan.SpawnEntity(GameTicker.ObserverPrototypeName, new MapCoordinates(0, 0, map));
@@ -41,7 +42,7 @@ await server.WaitPost(() =>
 
             followerSystem.StartFollowingEntity(follower, followed);
 
-            entMan.DeleteEntity(mapMan.GetMapEntityId(map));
+            entMan.DeleteEntity(mapSys.GetMap(map));
         });
         await pair.CleanReturnAsync();
     }
diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs
index c6a8e618cc1..02245c783e8 100644
--- a/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs
+++ b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs
@@ -1,5 +1,4 @@
 #nullable enable
-using System.Numerics;
 using Content.Server.Cuffs;
 using Content.Shared.Body.Components;
 using Content.Shared.Cuffs.Components;
@@ -7,7 +6,6 @@
 using Robust.Server.Console;
 using Robust.Shared.GameObjects;
 using Robust.Shared.Map;
-using Robust.Shared.Maths;
 
 namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking
 {
@@ -51,10 +49,11 @@ public async Task Test()
             var mapManager = server.ResolveDependency<IMapManager>();
             var host = server.ResolveDependency<IServerConsoleHost>();
 
+            var map = await pair.CreateTestMap();
+
             await server.WaitAssertion(() =>
             {
-                var mapId = mapManager.CreateMap();
-                var coordinates = new MapCoordinates(Vector2.Zero, mapId);
+                var coordinates = map.MapCoords;
 
                 var cuffableSys = entityManager.System<CuffableSystem>();
                 var xformSys = entityManager.System<SharedTransformSystem>();
diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs
index b0aceacc03d..9f04660008c 100644
--- a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs
+++ b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/AlertsComponentTests.cs
@@ -5,7 +5,6 @@
 using Robust.Client.UserInterface;
 using Robust.Server.Player;
 using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
 
 namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs
 {
@@ -45,8 +44,8 @@ await server.WaitAssertion(() =>
                 Assert.That(alerts, Is.Not.Null);
                 var alertCount = alerts.Count;
 
-                alertsSystem.ShowAlert(playerUid, AlertType.Debug1);
-                alertsSystem.ShowAlert(playerUid, AlertType.Debug2);
+                alertsSystem.ShowAlert(playerUid, "Debug1");
+                alertsSystem.ShowAlert(playerUid, "Debug2");
 
                 Assert.That(alerts, Has.Count.EqualTo(alertCount + 2));
             });
@@ -89,14 +88,14 @@ static AlertsUI FindAlertsUI(Control control)
                 // We should be seeing 2 alerts - the 2 debug alerts, in a specific order.
                 Assert.That(clientAlertsUI.AlertContainer.ChildCount, Is.GreaterThanOrEqualTo(2));
                 var alertControls = clientAlertsUI.AlertContainer.Children.Select(c => (AlertControl) c);
-                var alertIDs = alertControls.Select(ac => ac.Alert.AlertType).ToArray();
-                var expectedIDs = new[] { AlertType.Debug1, AlertType.Debug2 };
+                var alertIDs = alertControls.Select(ac => ac.Alert.ID).ToArray();
+                var expectedIDs = new[] { "HumanHealth", "Debug1", "Debug2" };
                 Assert.That(alertIDs, Is.SupersetOf(expectedIDs));
             });
 
             await server.WaitAssertion(() =>
             {
-                alertsSystem.ClearAlert(playerUid, AlertType.Debug1);
+                alertsSystem.ClearAlert(playerUid, "Debug1");
             });
 
             await pair.RunTicksSync(5);
@@ -106,8 +105,8 @@ await client.WaitAssertion(() =>
                 // We should be seeing 1 alert now because one was cleared
                 Assert.That(clientAlertsUI.AlertContainer.ChildCount, Is.GreaterThanOrEqualTo(1));
                 var alertControls = clientAlertsUI.AlertContainer.Children.Select(c => (AlertControl) c);
-                var alertIDs = alertControls.Select(ac => ac.Alert.AlertType).ToArray();
-                var expectedIDs = new[] { AlertType.Debug2 };
+                var alertIDs = alertControls.Select(ac => ac.Alert.ID).ToArray();
+                var expectedIDs = new[] { "HumanHealth", "Debug2" };
                 Assert.That(alertIDs, Is.SupersetOf(expectedIDs));
             });
 
diff --git a/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs b/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs
index 662ea3b9747..889c7868d7c 100644
--- a/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs
+++ b/Content.IntegrationTests/Tests/GameRules/AntagPreferenceTest.cs
@@ -47,7 +47,7 @@ public async Task TestLobbyPlayersValid()
         Assert.That(sys.IsEntityValid(client.AttachedEntity, def), Is.True);
 
         // By default, traitor/antag preferences are disabled, so the pool should be empty.
-        var sessions = new List<ICommonSession>{pair.Player!};
+        var sessions = new List<ICommonSession> { pair.Player! };
         var pool = sys.GetPlayerPool(rule, sessions, def);
         Assert.That(pool.Count, Is.EqualTo(0));
 
diff --git a/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs b/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs
index d0e0255ae77..b0039144c9c 100644
--- a/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs
+++ b/Content.IntegrationTests/Tests/GameRules/FailAndStartPresetTest.cs
@@ -1,6 +1,6 @@
-// #nullable enable
+// #nullable enable
 // using Content.Server.GameTicking;
-// using Content.Server.GameTicking.Components;
+// using Content.Shared.GameTicking.Components;
 // using Content.Server.GameTicking.Presets;
 // using Content.Shared.CCVar;
 // using Content.Shared.GameTicking;
@@ -36,7 +36,7 @@
 // - type: entity
 //   id: TestRule
 //   parent: BaseGameRule
-//   noSpawn: true
+//   categories: [ HideSpawnMenu ]
 //   components:
 //   - type: GameRule
 //     minPlayers: 0
@@ -45,7 +45,7 @@
 // - type: entity
 //   id: TestRuleTenPlayers
 //   parent: BaseGameRule
-//   noSpawn: true
+//   categories: [ HideSpawnMenu ]
 //   components:
 //   - type: GameRule
 //     minPlayers: 10
@@ -110,14 +110,14 @@
 //         player = pair.Player!.AttachedEntity!.Value;
 //         Assert.That(entMan.EntityExists(player));
 
-//         ticker.SetGamePreset((GamePresetPrototype?)null);
-//         server.CfgMan.SetCVar(CCVars.GridFill, false);
-//         server.CfgMan.SetCVar(CCVars.GameLobbyFallbackEnabled, true);
-//         server.CfgMan.SetCVar(CCVars.GameLobbyDefaultPreset, "secret");
-//         server.System<TestRuleSystem>().Run = false;
-//         await pair.CleanReturnAsync();
-//     }
-// }
+//        ticker.SetGamePreset((GamePresetPrototype?) null);
+//        server.CfgMan.SetCVar(CCVars.GridFill, false);
+//        server.CfgMan.SetCVar(CCVars.GameLobbyFallbackEnabled, true);
+//        server.CfgMan.SetCVar(CCVars.GameLobbyDefaultPreset, "secret");
+//        server.System<TestRuleSystem>().Run = false;
+//        await pair.CleanReturnAsync();
+//    }
+//}
 
 // public sealed class TestRuleSystem : EntitySystem
 // {
diff --git a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs
index 20a157e33e8..611b038309b 100644
--- a/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs
+++ b/Content.IntegrationTests/Tests/GameRules/RuleMaxTimeRestartTest.cs
@@ -1,9 +1,9 @@
 using Content.Server.GameTicking;
 using Content.Server.GameTicking.Commands;
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Shared.CCVar;
+using Content.Shared.GameTicking.Components;
 using Robust.Shared.Configuration;
 using Robust.Shared.GameObjects;
 using Robust.Shared.Timing;
@@ -27,8 +27,12 @@ public async Task RestartTest()
             var sGameTicker = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<GameTicker>();
             var sGameTiming = server.ResolveDependency<IGameTiming>();
 
-            sGameTicker.StartGameRule("MaxTimeRestart", out var ruleEntity);
-            Assert.That(entityManager.TryGetComponent<MaxTimeRestartRuleComponent>(ruleEntity, out var maxTime));
+            MaxTimeRestartRuleComponent maxTime = null;
+            await server.WaitPost(() =>
+            {
+                sGameTicker.StartGameRule("MaxTimeRestart", out var ruleEntity);
+                Assert.That(entityManager.TryGetComponent<MaxTimeRestartRuleComponent>(ruleEntity, out maxTime));
+            });
 
             Assert.That(server.EntMan.Count<GameRuleComponent>(), Is.EqualTo(1));
             Assert.That(server.EntMan.Count<ActiveGameRuleComponent>(), Is.EqualTo(1));
diff --git a/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs b/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs
index 0ad198d6ef2..74641126aee 100644
--- a/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs
+++ b/Content.IntegrationTests/Tests/Gravity/WeightlessStatusTests.cs
@@ -1,5 +1,6 @@
 using Content.Server.Gravity;
 using Content.Shared.Alert;
+using Content.Shared.Gravity;
 using Robust.Shared.GameObjects;
 
 namespace Content.IntegrationTests.Tests.Gravity
@@ -38,6 +39,7 @@ public async Task WeightlessStatusTest()
 
             var entityManager = server.ResolveDependency<IEntityManager>();
             var alertsSystem = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<AlertsSystem>();
+            var weightlessAlert = SharedGravitySystem.WeightlessAlert;
 
             EntityUid human = default;
 
@@ -56,7 +58,7 @@ await server.WaitAssertion(() =>
             await server.WaitAssertion(() =>
             {
                 // No gravity without a gravity generator
-                Assert.That(alertsSystem.IsShowingAlert(human, AlertType.Weightless));
+                Assert.That(alertsSystem.IsShowingAlert(human, weightlessAlert));
 
                 generatorUid = entityManager.SpawnEntity("WeightlessGravityGeneratorDummy", entityManager.GetComponent<TransformComponent>(human).Coordinates);
             });
@@ -66,7 +68,7 @@ await server.WaitAssertion(() =>
 
             await server.WaitAssertion(() =>
             {
-                Assert.That(alertsSystem.IsShowingAlert(human, AlertType.Weightless), Is.False);
+                Assert.That(alertsSystem.IsShowingAlert(human, weightlessAlert), Is.False);
 
                 // This should kill gravity
                 entityManager.DeleteEntity(generatorUid);
@@ -76,7 +78,7 @@ await server.WaitAssertion(() =>
 
             await server.WaitAssertion(() =>
             {
-                Assert.That(alertsSystem.IsShowingAlert(human, AlertType.Weightless));
+                Assert.That(alertsSystem.IsShowingAlert(human, weightlessAlert));
             });
 
             await pair.RunTicksSync(10);
diff --git a/Content.IntegrationTests/Tests/GravityGridTest.cs b/Content.IntegrationTests/Tests/GravityGridTest.cs
index 7f817e8a1e0..64f7a6d0820 100644
--- a/Content.IntegrationTests/Tests/GravityGridTest.cs
+++ b/Content.IntegrationTests/Tests/GravityGridTest.cs
@@ -34,29 +34,25 @@ public async Task Test()
 
             var testMap = await pair.CreateTestMap();
 
-            EntityUid generator = default;
-            var entityMan = server.ResolveDependency<IEntityManager>();
-            var mapMan = server.ResolveDependency<IMapManager>();
+            var entityMan = server.EntMan;
+            var mapMan = server.MapMan;
             var mapSys = entityMan.System<SharedMapSystem>();
 
-            MapGridComponent grid1 = null;
-            MapGridComponent grid2 = null;
-            EntityUid grid1Entity = default!;
-            EntityUid grid2Entity = default!;
+            EntityUid generator = default;
+            Entity<MapGridComponent> grid1 = default;
+            Entity<MapGridComponent> grid2 = default;
 
             // Create grids
             await server.WaitAssertion(() =>
             {
                 var mapId = testMap.MapId;
-                grid1 = mapMan.CreateGrid(mapId);
-                grid2 = mapMan.CreateGrid(mapId);
-                grid1Entity = grid1.Owner;
-                grid2Entity = grid2.Owner;
+                grid1 = mapMan.CreateGridEntity(mapId);
+                grid2 = mapMan.CreateGridEntity(mapId);
 
-                mapSys.SetTile(grid1Entity, grid1, Vector2i.Zero, new Tile(1));
-                mapSys.SetTile(grid2Entity, grid2, Vector2i.Zero, new Tile(1));
+                mapSys.SetTile(grid1, grid1, Vector2i.Zero, new Tile(1));
+                mapSys.SetTile(grid2, grid2, Vector2i.Zero, new Tile(1));
 
-                generator = entityMan.SpawnEntity("GridGravityGeneratorDummy", new EntityCoordinates(grid1Entity, 0.5f, 0.5f));
+                generator = entityMan.SpawnEntity("GridGravityGeneratorDummy", new EntityCoordinates(grid1, 0.5f, 0.5f));
                 Assert.Multiple(() =>
                 {
                     Assert.That(entityMan.HasComponent<GravityGeneratorComponent>(generator));
@@ -77,8 +73,8 @@ await server.WaitAssertion(() =>
                 Assert.Multiple(() =>
                 {
                     Assert.That(generatorComponent.GravityActive, Is.True);
-                    Assert.That(!entityMan.GetComponent<GravityComponent>(grid1Entity).EnabledVV);
-                    Assert.That(entityMan.GetComponent<GravityComponent>(grid2Entity).EnabledVV);
+                    Assert.That(!entityMan.GetComponent<GravityComponent>(grid1).EnabledVV);
+                    Assert.That(entityMan.GetComponent<GravityComponent>(grid2).EnabledVV);
                 });
 
                 // Re-enable needs power so it turns off again.
@@ -95,7 +91,7 @@ await server.WaitAssertion(() =>
                 Assert.Multiple(() =>
                 {
                     Assert.That(generatorComponent.GravityActive, Is.False);
-                    Assert.That(entityMan.GetComponent<GravityComponent>(grid2Entity).EnabledVV, Is.False);
+                    Assert.That(entityMan.GetComponent<GravityComponent>(grid2).EnabledVV, Is.False);
                 });
             });
 
diff --git a/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs b/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs
index 4415eddf376..456df3b2f31 100644
--- a/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs
+++ b/Content.IntegrationTests/Tests/Interaction/Click/InteractionSystemTests.cs
@@ -47,13 +47,9 @@ public async Task InteractionTest()
             var sysMan = server.ResolveDependency<IEntitySystemManager>();
             var handSys = sysMan.GetEntitySystem<SharedHandsSystem>();
 
-            var mapId = MapId.Nullspace;
-            var coords = MapCoordinates.Nullspace;
-            await server.WaitAssertion(() =>
-            {
-                mapId = mapManager.CreateMap();
-                coords = new MapCoordinates(Vector2.Zero, mapId);
-            });
+            var map = await pair.CreateTestMap();
+            var mapId = map.MapId;
+            var coords = map.MapCoords;
 
             await server.WaitIdleAsync();
             EntityUid user = default;
@@ -117,13 +113,9 @@ public async Task InteractionObstructionTest()
             var sysMan = server.ResolveDependency<IEntitySystemManager>();
             var handSys = sysMan.GetEntitySystem<SharedHandsSystem>();
 
-            var mapId = MapId.Nullspace;
-            var coords = MapCoordinates.Nullspace;
-            await server.WaitAssertion(() =>
-            {
-                mapId = mapManager.CreateMap();
-                coords = new MapCoordinates(Vector2.Zero, mapId);
-            });
+            var map = await pair.CreateTestMap();
+            var mapId = map.MapId;
+            var coords = map.MapCoords;
 
             await server.WaitIdleAsync();
             EntityUid user = default;
@@ -188,13 +180,9 @@ public async Task InteractionInRangeTest()
             var sysMan = server.ResolveDependency<IEntitySystemManager>();
             var handSys = sysMan.GetEntitySystem<SharedHandsSystem>();
 
-            var mapId = MapId.Nullspace;
-            var coords = MapCoordinates.Nullspace;
-            await server.WaitAssertion(() =>
-            {
-                mapId = mapManager.CreateMap();
-                coords = new MapCoordinates(Vector2.Zero, mapId);
-            });
+            var map = await pair.CreateTestMap();
+            var mapId = map.MapId;
+            var coords = map.MapCoords;
 
             await server.WaitIdleAsync();
             EntityUid user = default;
@@ -258,13 +246,9 @@ public async Task InteractionOutOfRangeTest()
             var sysMan = server.ResolveDependency<IEntitySystemManager>();
             var handSys = sysMan.GetEntitySystem<SharedHandsSystem>();
 
-            var mapId = MapId.Nullspace;
-            var coords = MapCoordinates.Nullspace;
-            await server.WaitAssertion(() =>
-            {
-                mapId = mapManager.CreateMap();
-                coords = new MapCoordinates(Vector2.Zero, mapId);
-            });
+            var map = await pair.CreateTestMap();
+            var mapId = map.MapId;
+            var coords = map.MapCoords;
 
             await server.WaitIdleAsync();
             EntityUid user = default;
@@ -328,13 +312,9 @@ public async Task InsideContainerInteractionBlockTest()
             var handSys = sysMan.GetEntitySystem<SharedHandsSystem>();
             var conSystem = sysMan.GetEntitySystem<SharedContainerSystem>();
 
-            var mapId = MapId.Nullspace;
-            var coords = MapCoordinates.Nullspace;
-            await server.WaitAssertion(() =>
-            {
-                mapId = mapManager.CreateMap();
-                coords = new MapCoordinates(Vector2.Zero, mapId);
-            });
+            var map = await pair.CreateTestMap();
+            var mapId = map.MapId;
+            var coords = map.MapCoords;
 
             await server.WaitIdleAsync();
             EntityUid user = default;
diff --git a/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs b/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs
index b8828763a23..e5ac0f785aa 100644
--- a/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InRangeUnobstructed.cs
@@ -37,10 +37,11 @@ public async Task EntityEntityTest()
             EntityUid other = default;
             MapCoordinates mapCoordinates = default;
 
+            var map = await pair.CreateTestMap();
+
             await server.WaitAssertion(() =>
             {
-                var mapId = mapManager.CreateMap();
-                var coordinates = new MapCoordinates(Vector2.Zero, mapId);
+                var coordinates = map.MapCoords;
 
                 origin = sEntities.SpawnEntity(HumanId, coordinates);
                 other = sEntities.SpawnEntity(HumanId, coordinates);
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.EntitySpecifier.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.EntitySpecifier.cs
index 37dca721373..194bc54fba6 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.EntitySpecifier.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.EntitySpecifier.cs
@@ -33,7 +33,7 @@ protected sealed class EntitySpecifier
         public int Quantity;
 
         /// <summary>
-        /// If true, a check has been performed to see if the prototype ia an entity prototype with a stack component,
+        /// If true, a check has been performed to see if the prototype is an entity prototype with a stack component,
         /// in which case the specifier was converted into a stack-specifier
         /// </summary>
         public bool Converted;
@@ -100,7 +100,7 @@ await Server.WaitPost(() =>
 
         if (!ProtoMan.TryIndex<EntityPrototype>(spec.Prototype, out var entProto))
         {
-            Assert.Fail($"Unkown prototype: {spec.Prototype}");
+            Assert.Fail($"Unknown prototype: {spec.Prototype}");
             return default;
         }
 
@@ -114,13 +114,13 @@ await Server.WaitPost(() =>
             return await SpawnEntity((stack.StackTypeId, spec.Quantity), coords);
 
         Assert.That(spec.Quantity, Is.EqualTo(1), "SpawnEntity only supports returning a singular entity");
-        await Server.WaitPost(() => uid = SEntMan.SpawnEntity(spec.Prototype, coords));
+        await Server.WaitPost(() => uid = SEntMan.SpawnAtPosition(spec.Prototype, coords));
         return uid;
     }
 
     /// <summary>
     /// Convert an entity-uid to a matching entity specifier. Useful when doing entity lookups & checking that the
-    /// right quantity of entities/materials werre produced. Returns null if passed an entity with a null prototype.
+    /// right quantity of entities/materials were produced. Returns null if passed an entity with a null prototype.
     /// </summary>
     protected EntitySpecifier? ToEntitySpecifier(EntityUid uid)
     {
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
index 19ca83a9715..a19b62cd70a 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.Helpers.cs
@@ -14,6 +14,7 @@
 using Content.Shared.Gravity;
 using Content.Shared.Item;
 using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
 using Robust.Client.UserInterface.CustomControls;
 using Robust.Shared.GameObjects;
 using Robust.Shared.Input;
@@ -44,8 +45,9 @@ await Client.WaitPost(() =>
                 return;
 
             var comp = CEntMan.GetComponent<ConstructionGhostComponent>(clientTarget!.Value);
-            ClientTarget = clientTarget;
-            ConstructionGhostId = comp.Owner.Id;
+            Target = CEntMan.GetNetEntity(clientTarget.Value);
+            Assert.That(Target.Value.IsClientSide());
+            ConstructionGhostId = clientTarget.Value.GetHashCode();
         });
 
         await RunTicks(1);
@@ -82,18 +84,21 @@ protected async Task CraftItem(string prototype, bool shouldSucceed = true)
     /// <summary>
     /// Spawn an entity entity and set it as the target.
     /// </summary>
-    [MemberNotNull(nameof(Target))]
-    protected async Task SpawnTarget(string prototype)
+    [MemberNotNull(nameof(Target), nameof(STarget), nameof(CTarget))]
+#pragma warning disable CS8774 // Member must have a non-null value when exiting.
+    protected async Task<NetEntity> SpawnTarget(string prototype)
     {
         Target = NetEntity.Invalid;
         await Server.WaitPost(() =>
         {
-            Target = SEntMan.GetNetEntity(SEntMan.SpawnEntity(prototype, SEntMan.GetCoordinates(TargetCoords)));
+            Target = SEntMan.GetNetEntity(SEntMan.SpawnAtPosition(prototype, SEntMan.GetCoordinates(TargetCoords)));
         });
 
         await RunTicks(5);
         AssertPrototype(prototype);
+        return Target!.Value;
     }
+#pragma warning restore CS8774 // Member must have a non-null value when exiting.
 
     /// <summary>
     /// Spawn an entity in preparation for deconstruction
@@ -129,21 +134,20 @@ await Server.WaitPost(() =>
     /// <summary>
     /// Place an entity prototype into the players hand. Deletes any currently held entity.
     /// </summary>
-    /// <remarks>
-    /// Automatically enables welders.
-    /// </remarks>
-    protected async Task<NetEntity> PlaceInHands(string id, int quantity = 1, bool enableWelder = true)
+    /// <param name="id">The entity or stack prototype to spawn and place into the users hand</param>
+    /// <param name="quantity">The number of entities to spawn. If the prototype is a stack, this sets the stack count.</param>
+    /// <param name="enableToggleable">Whether or not to automatically enable any toggleable items</param>
+    protected async Task<NetEntity> PlaceInHands(string id, int quantity = 1, bool enableToggleable = true)
     {
-        return await PlaceInHands((id, quantity), enableWelder);
+        return await PlaceInHands((id, quantity), enableToggleable);
     }
 
     /// <summary>
     /// Place an entity prototype into the players hand. Deletes any currently held entity.
     /// </summary>
-    /// <remarks>
-    /// Automatically enables welders.
-    /// </remarks>
-    protected async Task<NetEntity> PlaceInHands(EntitySpecifier entity, bool enableWelder = true)
+    /// <param name="entity">The entity type & quantity to spawn and place into the users hand</param>
+    /// <param name="enableToggleable">Whether or not to automatically enable any toggleable items</param>
+    protected async Task<NetEntity> PlaceInHands(EntitySpecifier entity, bool enableToggleable = true)
     {
         if (Hands.ActiveHand == null)
         {
@@ -165,7 +169,7 @@ await Server.WaitPost(() =>
             Assert.That(HandSys.TryPickup(playerEnt, item, Hands.ActiveHand, false, false, Hands));
 
             // turn on welders
-            if (enableWelder && SEntMan.TryGetComponent(item, out itemToggle) && !itemToggle.Activated)
+            if (enableToggleable && SEntMan.TryGetComponent(item, out itemToggle) && !itemToggle.Activated)
             {
                 Assert.That(ItemToggleSys.TryActivate(item, playerEnt, itemToggle: itemToggle));
             }
@@ -173,7 +177,7 @@ await Server.WaitPost(() =>
 
         await RunTicks(1);
         Assert.That(Hands.ActiveHandEntity, Is.EqualTo(item));
-        if (enableWelder && itemToggle != null)
+        if (enableToggleable && itemToggle != null)
             Assert.That(itemToggle.Activated);
 
         return SEntMan.GetNetEntity(item);
@@ -254,21 +258,20 @@ await Server.WaitPost(() =>
     /// <summary>
     /// Place an entity prototype into the players hand and interact with the given entity (or target position)
     /// </summary>
-    /// <remarks>
-    /// Empty strings imply empty hands.
-    /// </remarks>
-    protected async Task Interact(string id, int quantity = 1, bool shouldSucceed = true, bool awaitDoAfters = true)
+    /// <param name="id">The entity or stack prototype to spawn and place into the users hand</param>
+    /// <param name="quantity">The number of entities to spawn. If the prototype is a stack, this sets the stack count.</param>
+    /// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
+    protected async Task InteractUsing(string id, int quantity = 1, bool awaitDoAfters = true)
     {
-        await Interact((id, quantity), shouldSucceed, awaitDoAfters);
+        await InteractUsing((id, quantity), awaitDoAfters);
     }
 
     /// <summary>
-    /// Place an entity prototype into the players hand and interact with the given entity (or target position)
+    /// Place an entity prototype into the players hand and interact with the given entity (or target position).
     /// </summary>
-    /// <remarks>
-    /// Empty strings imply empty hands.
-    /// </remarks>
-    protected async Task Interact(EntitySpecifier entity, bool shouldSucceed = true, bool awaitDoAfters = true)
+    /// <param name="entity">The entity type & quantity to spawn and place into the users hand</param>
+    /// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
+    protected async Task InteractUsing(EntitySpecifier entity, bool awaitDoAfters = true)
     {
         // For every interaction, we will also examine the entity, just in case this breaks something, somehow.
         // (e.g., servers attempt to assemble construction examine hints).
@@ -278,38 +281,80 @@ protected async Task Interact(EntitySpecifier entity, bool shouldSucceed = true,
         }
 
         await PlaceInHands(entity);
-        await Interact(shouldSucceed, awaitDoAfters);
+        await Interact(awaitDoAfters);
     }
 
     /// <summary>
     /// Interact with an entity using the currently held entity.
     /// </summary>
-    protected async Task Interact(bool shouldSucceed = true, bool awaitDoAfters = true)
+    /// <param name="awaitDoAfters">Whether or not to wait for any do-afters to complete</param>
+    protected async Task Interact(bool awaitDoAfters = true)
     {
-        var clientTarget = ClientTarget;
-
-        if ((clientTarget?.IsValid() != true || CEntMan.Deleted(clientTarget)) && (Target == null || Target.Value.IsValid()))
+        if (Target == null || !Target.Value.IsClientSide())
         {
-            await Server.WaitPost(() => InteractSys.UserInteraction(SEntMan.GetEntity(Player), SEntMan.GetCoordinates(TargetCoords), SEntMan.GetEntity(Target)));
-            await RunTicks(1);
+            await Interact(Target, TargetCoords, awaitDoAfters);
+            return;
         }
-        else
-        {
-            // The entity is client-side, so attempt to start construction
-            var clientEnt = ClientTarget ?? CEntMan.GetEntity(Target);
 
-            await Client.WaitPost(() => CConSys.TryStartConstruction(clientEnt!.Value));
-            await RunTicks(5);
-        }
+        // The target is a client-side entity, so we will just attempt to start construction under the assumption that
+        // it is a construction ghost.
+
+        await Client.WaitPost(() => CConSys.TryStartConstruction(CTarget!.Value));
+        await RunTicks(5);
+
+        if (awaitDoAfters)
+            await AwaitDoAfters();
+
+        await CheckTargetChange();
+    }
+
+    /// <inheritdoc cref="Interact(EntityUid?,EntityCoordinates,bool)"/>
+    protected async Task Interact(NetEntity? target, NetCoordinates coordinates, bool awaitDoAfters = true)
+    {
+        Assert.That(SEntMan.TryGetEntity(target, out var sTarget) || target == null);
+        var coords = SEntMan.GetCoordinates(coordinates);
+        Assert.That(coords.IsValid(SEntMan));
+        await Interact(sTarget, coords, awaitDoAfters);
+    }
+
+    /// <summary>
+    /// Interact with an entity using the currently held entity.
+    /// </summary>
+    protected async Task Interact(EntityUid? target, EntityCoordinates coordinates, bool awaitDoAfters = true)
+    {
+        Assert.That(SEntMan.TryGetEntity(Player, out var player));
+
+        await Server.WaitPost(() => InteractSys.UserInteraction(player!.Value, coordinates, target));
+        await RunTicks(1);
 
         if (awaitDoAfters)
-            await AwaitDoAfters(shouldSucceed);
+            await AwaitDoAfters();
 
-        await CheckTargetChange(shouldSucceed && awaitDoAfters);
+        await CheckTargetChange();
     }
 
     /// <summary>
-    /// Variant of <see cref="InteractUsing"/> that performs several interactions using different entities.
+    /// Activate an entity.
+    /// </summary>
+    protected async Task Activate(NetEntity? target = null, bool awaitDoAfters = true)
+    {
+        target ??= Target;
+        Assert.That(target, Is.Not.Null);
+        Assert.That(SEntMan.TryGetEntity(target!.Value, out var sTarget));
+        Assert.That(SEntMan.TryGetEntity(Player, out var player));
+
+        await Server.WaitPost(() => InteractSys.InteractionActivate(player!.Value, sTarget!.Value));
+        await RunTicks(1);
+
+        if (awaitDoAfters)
+            await AwaitDoAfters();
+
+        await CheckTargetChange();
+    }
+
+    /// <summary>
+    /// Variant of <see cref="InteractUsing(string,int,bool)"/> that performs several interactions using different entities.
+    /// Useful for quickly finishing multiple construction steps.
     /// </summary>
     /// <remarks>
     /// Empty strings imply empty hands.
@@ -318,7 +363,7 @@ protected async Task Interact(params EntitySpecifier[] specifiers)
     {
         foreach (var spec in specifiers)
         {
-            await Interact(spec);
+            await InteractUsing(spec);
         }
     }
 
@@ -338,7 +383,7 @@ protected async Task<bool> ThrowItem(NetCoordinates? target = null, float minDis
     /// <summary>
     /// Wait for any currently active DoAfters to finish.
     /// </summary>
-    protected async Task AwaitDoAfters(bool shouldSucceed = true, int maxExpected = 1)
+    protected async Task AwaitDoAfters(int maxExpected = 1)
     {
         if (!ActiveDoAfters.Any())
             return;
@@ -353,13 +398,12 @@ protected async Task AwaitDoAfters(bool shouldSucceed = true, int maxExpected =
             await RunTicks(10);
         }
 
-        if (!shouldSucceed)
-            return;
-
         foreach (var doAfter in doAfters)
         {
             Assert.That(!doAfter.Cancelled);
         }
+
+        await RunTicks(5);
     }
 
     /// <summary>
@@ -398,39 +442,28 @@ await Server.WaitPost(() =>
     /// Check if the test's target entity has changed. E.g., construction interactions will swap out entities while
     /// a structure is being built.
     /// </summary>
-    protected async Task CheckTargetChange(bool shouldSucceed)
+    protected async Task CheckTargetChange()
     {
         if (Target == null)
             return;
 
-        var target = Target.Value;
+        var originalTarget = Target.Value;
         await RunTicks(5);
 
-        if (ClientTarget != null && CEntMan.IsClientSide(ClientTarget.Value))
+        if (Target.Value.IsClientSide() && CTestSystem.Ghosts.TryGetValue(ConstructionGhostId, out var newWeh))
         {
-            Assert.That(CEntMan.Deleted(ClientTarget.Value), Is.EqualTo(shouldSucceed),
-                $"Construction ghost was {(shouldSucceed ? "not deleted" : "deleted")}.");
-
-            if (shouldSucceed)
-            {
-                Assert.That(CTestSystem.Ghosts.TryGetValue(ConstructionGhostId, out var newWeh),
-                    $"Failed to get construction entity from ghost Id");
-
-                await Client.WaitPost(() => CLogger.Debug($"Construction ghost {ConstructionGhostId} became entity {newWeh}"));
-                Target = newWeh;
-            }
+            CLogger.Debug($"Construction ghost {ConstructionGhostId} became entity {newWeh}");
+            Target = newWeh;
         }
 
         if (STestSystem.EntChanges.TryGetValue(Target.Value, out var newServerWeh))
         {
-            await Server.WaitPost(
-                () => SLogger.Debug($"Construction entity {Target.Value} changed to {newServerWeh}"));
-
+            SLogger.Debug($"Construction entity {Target.Value} changed to {newServerWeh}");
             Target = newServerWeh;
         }
 
-        if (Target != target)
-            await CheckTargetChange(shouldSucceed);
+        if (Target != originalTarget)
+            await CheckTargetChange();
     }
 
     #region Asserts
@@ -444,16 +477,10 @@ protected void ClientAssertPrototype(string? prototype, NetEntity? target = null
             return;
         }
 
-        var meta = SEntMan.GetComponent<MetaDataComponent>(SEntMan.GetEntity(target.Value));
+        var meta = CEntMan.GetComponent<MetaDataComponent>(CEntMan.GetEntity(target.Value));
         Assert.That(meta.EntityPrototype?.ID, Is.EqualTo(prototype));
     }
 
-    protected void ClientAssertPrototype(string? prototype, EntityUid? target)
-    {
-        var netEnt = CTestSystem.Ghosts[target.GetHashCode()];
-        AssertPrototype(prototype, netEnt);
-    }
-
     protected void AssertPrototype(string? prototype, NetEntity? target = null)
     {
         target ??= Target;
@@ -544,11 +571,11 @@ protected async Task AssertTile(string? proto, NetCoordinates? coords = null)
 
         var tile = Tile.Empty;
         var serverCoords = SEntMan.GetCoordinates(coords ?? TargetCoords);
-        var pos = serverCoords.ToMap(SEntMan, Transform);
+        var pos = Transform.ToMapCoordinates(serverCoords);
         await Server.WaitPost(() =>
         {
-            if (MapMan.TryFindGridAt(pos, out _, out var grid))
-                tile = grid.GetTileRef(serverCoords).Tile;
+            if (MapMan.TryFindGridAt(pos, out var gridUid, out var grid))
+                tile = MapSystem.GetTileRef(gridUid, grid, serverCoords).Tile;
         });
 
         Assert.That(tile.TypeId, Is.EqualTo(targetTile.TypeId));
@@ -699,6 +726,8 @@ protected async Task<EntityUid> FindEntity(
     protected IEnumerable<Shared.DoAfter.DoAfter> ActiveDoAfters
         => DoAfters.DoAfters.Values.Where(x => !x.Cancelled && !x.Completed);
 
+    #region Component
+
     /// <summary>
     /// Convenience method to get components on the target. Returns SERVER-SIDE components.
     /// </summary>
@@ -708,39 +737,61 @@ protected T Comp<T>(NetEntity? target = null) where T : IComponent
         if (target == null)
             Assert.Fail("No target specified");
 
-        return SEntMan.GetComponent<T>(SEntMan.GetEntity(target!.Value));
+        return SEntMan.GetComponent<T>(ToServer(target!.Value));
     }
 
+    /// <inheritdoc cref="Comp{T}"/>
+    protected bool TryComp<T>(NetEntity? target, [NotNullWhen(true)] out T? comp) where T : IComponent
+    {
+        return SEntMan.TryGetComponent(ToServer(target), out comp);
+    }
+
+    /// <inheritdoc cref="Comp{T}"/>
+    protected bool TryComp<T>([NotNullWhen(true)] out T? comp) where T : IComponent
+    {
+        return SEntMan.TryGetComponent(STarget, out comp);
+    }
+
+    #endregion
+
     /// <summary>
     /// Set the tile at the target position to some prototype.
     /// </summary>
-    protected async Task SetTile(string? proto, NetCoordinates? coords = null, MapGridComponent? grid = null)
+    protected async Task SetTile(string? proto, NetCoordinates? coords = null, Entity<MapGridComponent>? grid = null)
     {
         var tile = proto == null
             ? Tile.Empty
             : new Tile(TileMan[proto].TileId);
 
-        var pos = SEntMan.GetCoordinates(coords ?? TargetCoords).ToMap(SEntMan, Transform);
+        var pos = Transform.ToMapCoordinates(SEntMan.GetCoordinates(coords ?? TargetCoords));
 
+        EntityUid gridUid;
+        MapGridComponent? gridComp;
         await Server.WaitPost(() =>
         {
-            if (grid != null || MapMan.TryFindGridAt(pos, out var gridUid, out grid))
+            if (grid is { } gridEnt)
+            {
+                MapSystem.SetTile(gridEnt, SEntMan.GetCoordinates(coords ?? TargetCoords), tile);
+                return;
+            }
+            else if (MapMan.TryFindGridAt(pos, out var gUid, out var gComp))
             {
-                grid.SetTile(SEntMan.GetCoordinates(coords ?? TargetCoords), tile);
+                MapSystem.SetTile(gUid, gComp, SEntMan.GetCoordinates(coords ?? TargetCoords), tile);
                 return;
             }
 
             if (proto == null)
                 return;
 
-            var gridEnt = MapMan.CreateGridEntity(MapData.MapId);
+            gridEnt = MapMan.CreateGridEntity(MapData.MapId);
             grid = gridEnt;
             gridUid = gridEnt;
+            gridComp = gridEnt.Comp;
             var gridXform = SEntMan.GetComponent<TransformComponent>(gridUid);
             Transform.SetWorldPosition(gridXform, pos.Position);
-            grid.SetTile(SEntMan.GetCoordinates(coords ?? TargetCoords), tile);
+            MapSystem.SetTile((gridUid, gridComp), SEntMan.GetCoordinates(coords ?? TargetCoords), tile);
 
-            if (!MapMan.TryFindGridAt(pos, out _, out grid))
+            if (!MapMan.TryFindGridAt(pos, out _, out _))
                 Assert.Fail("Failed to create grid?");
         });
         await AssertTile(proto, coords);
@@ -833,23 +884,70 @@ protected bool TryGetBui(Enum key, [NotNullWhen(true)] out BoundUserInterface? b
         return true;
     }
 
+    protected bool IsUiOpen(Enum key)
+    {
+        if (!TryComp(Player, out UserInterfaceUserComponent? user))
+            return false;
+
+        foreach (var keys in user.OpenInterfaces.Values)
+        {
+            if (keys.Contains(key))
+                return true;
+        }
+
+        return false;
+    }
+
     #endregion
 
     #region UI
 
     /// <summary>
-    ///     Presses and releases a button on some client-side window. Will fail if the button cannot be found.
+    /// Attempts to find, and then presses and releases a control on some client-side window.
+    /// Will fail if the control cannot be found.
     /// </summary>
-    protected async Task ClickControl<TWindow>(string name) where TWindow : BaseWindow
+    protected async Task ClickControl<TWindow, TControl>(string name, BoundKeyFunction? function = null)
+        where TWindow : BaseWindow
+        where TControl : Control
     {
-        await ClickControl(GetControl<TWindow, Control>(name));
+        var window = GetWindow<TWindow>();
+        var control = GetControlFromField<TControl>(name, window);
+        await ClickControl(control, function);
     }
 
     /// <summary>
-    ///     Simulates a click and release at the center of some UI Constrol.
+    /// Attempts to find, and then presses and releases a control on some client-side widget.
+    /// Will fail if the control cannot be found.
     /// </summary>
-    protected async Task ClickControl(Control control)
+    protected async Task ClickWidgetControl<TWidget, TControl>(string name, BoundKeyFunction? function = null)
+        where TWidget : UIWidget, new()
+        where TControl : Control
     {
+        var widget = GetWidget<TWidget>();
+        var control = GetControlFromField<TControl>(name, widget);
+        await ClickControl(control, function);
+    }
+
+    /// <inheritdoc cref="ClickControl{TWindow,TControl}"/>
+    protected async Task ClickControl<TWindow>(string name, BoundKeyFunction? function = null)
+        where TWindow : BaseWindow
+    {
+        await ClickControl<TWindow, Control>(name, function);
+    }
+
+    /// <inheritdoc cref="ClickWidgetControl{TWidget,TControl}"/>
+    protected async Task ClickWidgetControl<TWidget>(string name, BoundKeyFunction? function = null)
+        where TWidget : UIWidget, new()
+    {
+        await ClickWidgetControl<TWidget, Control>(name, function);
+    }
+
+    /// <summary>
+    ///     Simulates a click and release at the center of some UI control.
+    /// </summary>
+    protected async Task ClickControl(Control control, BoundKeyFunction? function = null)
+    {
+        function ??= EngineKeyFunctions.UIClick;
         var screenCoords = new ScreenCoordinates(
             control.GlobalPixelPosition + control.PixelSize / 2,
             control.Window?.Id ?? default);
@@ -858,7 +956,7 @@ protected async Task ClickControl(Control control)
         var relativePixelPos = screenCoords.Position - control.GlobalPixelPosition;
 
         var args = new GUIBoundKeyEventArgs(
-            EngineKeyFunctions.UIClick,
+            function.Value,
             BoundKeyState.Down,
             screenCoords,
             default,
@@ -869,7 +967,7 @@ protected async Task ClickControl(Control control)
         await RunTicks(1);
 
         args = new GUIBoundKeyEventArgs(
-            EngineKeyFunctions.UIClick,
+            function.Value,
             BoundKeyState.Up,
             screenCoords,
             default,
@@ -881,31 +979,26 @@ protected async Task ClickControl(Control control)
     }
 
     /// <summary>
-    ///     Attempts to find a control on some client-side window. Will fail if the control cannot be found.
+    /// Attempt to retrieve a control by looking for a field on some other control.
     /// </summary>
-    protected TControl GetControl<TWindow, TControl>(string name)
-        where TWindow : BaseWindow
+    /// <remarks>
+    /// Will fail if the control cannot be found.
+    /// </remarks>
+    protected TControl GetControlFromField<TControl>(string name, Control parent)
         where TControl : Control
-    {
-        var control = GetControl<TWindow>(name);
-        Assert.That(control.GetType().IsAssignableTo(typeof(TControl)));
-        return (TControl) control;
-    }
-
-    protected Control GetControl<TWindow>(string name) where TWindow : BaseWindow
     {
         const BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
-        var field = typeof(TWindow).GetField(name, flags);
-        var prop = typeof(TWindow).GetProperty(name, flags);
+        var parentType = parent.GetType();
+        var field = parentType.GetField(name, flags);
+        var prop = parentType.GetProperty(name, flags);
 
         if (field == null && prop == null)
         {
-            Assert.Fail($"Window {typeof(TWindow).Name} does not have a field or property named {name}");
+            Assert.Fail($"Window {parentType.Name} does not have a field or property named {name}");
             return default!;
         }
 
-        var window = GetWindow<TWindow>();
-        var fieldOrProp = field?.GetValue(window) ?? prop?.GetValue(window);
+        var fieldOrProp = field?.GetValue(parent) ?? prop?.GetValue(parent);
 
         if (fieldOrProp is not Control control)
         {
@@ -913,7 +1006,59 @@ protected Control GetControl<TWindow>(string name) where TWindow : BaseWindow
             return default!;
         }
 
-        return control;
+        Assert.That(control.GetType().IsAssignableTo(typeof(TControl)));
+        return (TControl) control;
+    }
+
+    /// <summary>
+    /// Attempt to retrieve a control that matches some predicate by iterating through a control's children.
+    /// </summary>
+    /// <remarks>
+    /// Will fail if the control cannot be found.
+    /// </remarks>
+    protected TControl GetControlFromChildren<TControl>(Func<TControl, bool> predicate, Control parent, bool recursive = true)
+        where TControl : Control
+    {
+        if (TryGetControlFromChildren(predicate, parent, out var control, recursive))
+            return control;
+
+        Assert.Fail($"Failed to find a {nameof(TControl)} that satisfies the predicate in {parent.Name}");
+        return default!;
+    }
+
+    /// <summary>
+    /// Attempt to retrieve a control of a given type by iterating through a control's children.
+    /// </summary>
+    protected TControl GetControlFromChildren<TControl>(Control parent, bool recursive = false)
+        where TControl : Control
+    {
+        return GetControlFromChildren<TControl>(static _ => true, parent, recursive);
+    }
+
+    /// <summary>
+    /// Attempt to retrieve a control that matches some predicate by iterating through a control's children.
+    /// </summary>
+    protected bool TryGetControlFromChildren<TControl>(
+        Func<TControl, bool> predicate,
+        Control parent,
+        [NotNullWhen(true)] out TControl? control,
+        bool recursive = true)
+        where TControl : Control
+    {
+        foreach (var ctrl in parent.Children)
+        {
+            if (ctrl is TControl cast && predicate(cast))
+            {
+                control = cast;
+                return true;
+            }
+
+            if (recursive && TryGetControlFromChildren(predicate, ctrl, out control))
+                return true;
+        }
+
+        control = null;
+        return false;
     }
 
     /// <summary>
@@ -944,7 +1089,6 @@ protected bool TryFindWindow<TWindow>([NotNullWhen(true)] out TWindow? window) w
         return window != null;
     }
 
-
     /// <summary>
     /// Attempts to find a currently open client-side window.
     /// </summary>
@@ -962,6 +1106,34 @@ protected bool TryFindWindow(Type type, [NotNullWhen(true)] out BaseWindow? wind
         return window != null;
     }
 
+
+    /// <summary>
+    /// Attempts to find client-side UI widget.
+    /// </summary>
+    protected UIWidget GetWidget<TWidget>()
+        where TWidget : UIWidget, new()
+    {
+        if (TryFindWidget(out TWidget? widget))
+            return widget;
+
+        Assert.Fail($"Could not find a {typeof(TWidget).Name} widget");
+        return default!;
+    }
+
+    /// <summary>
+    /// Attempts to find client-side UI widget.
+    /// </summary>
+    private bool TryFindWidget<TWidget>([NotNullWhen(true)] out TWidget? uiWidget)
+        where TWidget : UIWidget, new()
+    {
+        uiWidget = null;
+        var screen = UiMan.ActiveScreen;
+        if (screen == null)
+            return false;
+
+        return screen.TryGetWidget(out uiWidget);
+    }
+
     #endregion
 
     #region Power
@@ -1009,14 +1181,17 @@ await Server.WaitPost(() =>
 
     #region Inputs
 
+
+
     /// <summary>
     ///     Make the client press and then release a key. This assumes the key is currently released.
+    ///     This will default to using the <see cref="Target"/> entity and <see cref="TargetCoords"/> coordinates.
     /// </summary>
     protected async Task PressKey(
         BoundKeyFunction key,
         int ticks = 1,
         NetCoordinates? coordinates = null,
-        NetEntity cursorEntity = default)
+        NetEntity? cursorEntity = null)
     {
         await SetKey(key, BoundKeyState.Down, coordinates, cursorEntity);
         await RunTicks(ticks);
@@ -1025,15 +1200,17 @@ protected async Task PressKey(
     }
 
     /// <summary>
-    ///     Make the client press or release a key
+    ///     Make the client press or release a key.
+    ///     This will default to using the <see cref="Target"/> entity and <see cref="TargetCoords"/> coordinates.
     /// </summary>
     protected async Task SetKey(
         BoundKeyFunction key,
         BoundKeyState state,
         NetCoordinates? coordinates = null,
-        NetEntity cursorEntity = default)
+        NetEntity? cursorEntity = null)
     {
         var coords = coordinates ?? TargetCoords;
+        var target = cursorEntity ?? Target ?? default;
         ScreenCoordinates screen = default;
 
         var funcId = InputManager.NetworkBindMap.KeyFunctionID(key);
@@ -1042,7 +1219,7 @@ protected async Task SetKey(
             State = state,
             Coordinates = CEntMan.GetCoordinates(coords),
             ScreenCoordinates = screen,
-            Uid = CEntMan.GetEntity(cursorEntity),
+            Uid = CEntMan.GetEntity(target),
         };
 
         await Client.WaitPost(() => InputSystem.HandleInputCommand(ClientSession, key, message));
diff --git a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
index 42f64b344cd..f07f23d84c8 100644
--- a/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
+++ b/Content.IntegrationTests/Tests/Interaction/InteractionTest.cs
@@ -3,6 +3,7 @@
 using System.Numerics;
 using Content.Client.Construction;
 using Content.Client.Examine;
+using Content.Client.Gameplay;
 using Content.IntegrationTests.Pair;
 using Content.Server.Body.Systems;
 using Content.Server.Hands.Systems;
@@ -24,6 +25,7 @@
 using Robust.Shared.Timing;
 using Robust.UnitTesting;
 using Content.Shared.Item.ItemToggle;
+using Robust.Client.State;
 
 namespace Content.IntegrationTests.Tests.Interaction;
 
@@ -64,15 +66,12 @@ public abstract partial class InteractionTest
     /// The player entity that performs all these interactions. Defaults to an admin-observer with 1 hand.
     /// </summary>
     protected NetEntity Player;
-
-    protected EntityUid SPlayer => ToServer(Player);
-    protected EntityUid CPlayer => ToClient(Player);
+    protected EntityUid SPlayer;
+    protected EntityUid CPlayer;
 
     protected ICommonSession ClientSession = default!;
     protected ICommonSession ServerSession = default!;
 
-    public EntityUid? ClientTarget;
-
     /// <summary>
     /// The current target entity. This is the default entity for various helper functions.
     /// </summary>
@@ -84,6 +83,7 @@ public abstract partial class InteractionTest
     protected NetEntity? Target;
 
     protected EntityUid? STarget => ToServer(Target);
+
     protected EntityUid? CTarget => ToClient(Target);
 
     /// <summary>
@@ -107,7 +107,9 @@ public abstract partial class InteractionTest
     protected SharedItemToggleSystem ItemToggleSys = default!;
     protected InteractionTestSystem STestSystem = default!;
     protected SharedTransformSystem Transform = default!;
+    protected SharedMapSystem MapSystem = default!;
     protected ISawmill SLogger = default!;
+    protected SharedUserInterfaceSystem SUiSys = default!;
 
     // CLIENT dependencies
     protected IEntityManager CEntMan = default!;
@@ -119,6 +121,7 @@ public abstract partial class InteractionTest
     protected ExamineSystem ExamineSys = default!;
     protected InteractionTestSystem CTestSystem = default!;
     protected ISawmill CLogger = default!;
+    protected SharedUserInterfaceSystem CUiSys = default!;
 
     // player components
     protected HandsComponent Hands = default!;
@@ -126,7 +129,6 @@ public abstract partial class InteractionTest
 
     public float TickPeriod => (float) STiming.TickPeriod.TotalSeconds;
 
-
     // Simple mob that has one hand and can perform misc interactions.
     [TestPrototypes]
     private const string TestPrototypes = @"
@@ -139,6 +141,8 @@ public abstract partial class InteractionTest
   - type: Hands
   - type: MindContainer
   - type: Stripping
+  - type: Puller
+  - type: Physics
   - type: Tag
     tags:
     - CanPilot
@@ -148,7 +152,7 @@ public abstract partial class InteractionTest
     [SetUp]
     public virtual async Task Setup()
     {
-        Pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true, Dirty = true});
+        Pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true, Dirty = true });
 
         // server dependencies
         SEntMan = Server.ResolveDependency<IEntityManager>();
@@ -163,10 +167,12 @@ public virtual async Task Setup()
         ItemToggleSys = SEntMan.System<SharedItemToggleSystem>();
         DoAfterSys = SEntMan.System<SharedDoAfterSystem>();
         Transform = SEntMan.System<SharedTransformSystem>();
+        MapSystem = SEntMan.System<SharedMapSystem>();
         SConstruction = SEntMan.System<Server.Construction.ConstructionSystem>();
         STestSystem = SEntMan.System<InteractionTestSystem>();
         Stack = SEntMan.System<StackSystem>();
         SLogger = Server.ResolveDependency<ILogManager>().RootSawmill;
+        SUiSys = Client.System<SharedUserInterfaceSystem>();
 
         // client dependencies
         CEntMan = Client.ResolveDependency<IEntityManager>();
@@ -178,12 +184,14 @@ public virtual async Task Setup()
         CConSys = CEntMan.System<ConstructionSystem>();
         ExamineSys = CEntMan.System<ExamineSystem>();
         CLogger = Client.ResolveDependency<ILogManager>().RootSawmill;
+        CUiSys = Client.System<SharedUserInterfaceSystem>();
 
         // Setup map.
         await Pair.CreateTestMap();
-        PlayerCoords = SEntMan.GetNetCoordinates(MapData.GridCoords.Offset(new Vector2(0.5f, 0.5f)).WithEntityId(MapData.MapUid, Transform, SEntMan));
-        TargetCoords = SEntMan.GetNetCoordinates(MapData.GridCoords.Offset(new Vector2(1.5f, 0.5f)).WithEntityId(MapData.MapUid, Transform, SEntMan));
-        await SetTile(Plating, grid: MapData.Grid.Comp);
+
+        PlayerCoords = SEntMan.GetNetCoordinates(Transform.WithEntityId(MapData.GridCoords.Offset(new Vector2(0.5f, 0.5f)), MapData.MapUid));
+        TargetCoords = SEntMan.GetNetCoordinates(Transform.WithEntityId(MapData.GridCoords.Offset(new Vector2(1.5f, 0.5f)), MapData.MapUid));
+        await SetTile(Plating, grid: MapData.Grid);
 
         // Get player data
         var sPlayerMan = Server.ResolveDependency<Robust.Server.Player.IPlayerManager>();
@@ -202,16 +210,17 @@ await Server.WaitPost(() =>
             SEntMan.System<SharedMindSystem>().WipeMind(ServerSession.ContentData()?.Mind);
 
             old = cPlayerMan.LocalEntity;
-            Player = SEntMan.GetNetEntity(SEntMan.SpawnEntity(PlayerPrototype, SEntMan.GetCoordinates(PlayerCoords)));
-            var serverPlayerEnt = SEntMan.GetEntity(Player);
-            Server.PlayerMan.SetAttachedEntity(ServerSession, serverPlayerEnt);
-            Hands = SEntMan.GetComponent<HandsComponent>(serverPlayerEnt);
-            DoAfters = SEntMan.GetComponent<DoAfterComponent>(serverPlayerEnt);
+            SPlayer = SEntMan.SpawnEntity(PlayerPrototype, SEntMan.GetCoordinates(PlayerCoords));
+            Player = SEntMan.GetNetEntity(SPlayer);
+            Server.PlayerMan.SetAttachedEntity(ServerSession, SPlayer);
+            Hands = SEntMan.GetComponent<HandsComponent>(SPlayer);
+            DoAfters = SEntMan.GetComponent<DoAfterComponent>(SPlayer);
         });
 
         // Check player got attached.
         await RunTicks(5);
-        Assert.That(CEntMan.GetNetEntity(cPlayerMan.LocalEntity), Is.EqualTo(Player));
+        CPlayer = ToClient(Player);
+        Assert.That(cPlayerMan.LocalEntity, Is.EqualTo(CPlayer));
 
         // Delete old player entity.
         await Server.WaitPost(() =>
@@ -234,6 +243,10 @@ await Server.WaitPost(() =>
             }
         });
 
+        // Change UI state to in-game.
+        var state = Client.ResolveDependency<IStateManager>();
+        await Client.WaitPost(() => state.RequestStateChange<GameplayState>());
+
         // Final player asserts/checks.
         await Pair.ReallyBeIdle(5);
         Assert.Multiple(() =>
@@ -251,7 +264,8 @@ public async Task TearDownInternal()
         await TearDown();
     }
 
-    protected virtual async Task TearDown()
+    protected virtual Task TearDown()
     {
+        return Task.CompletedTask;
     }
 }
diff --git a/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs b/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs
index 30724b50a6d..0632fe1347c 100644
--- a/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs
+++ b/Content.IntegrationTests/Tests/Linter/StaticFieldValidationTest.cs
@@ -26,26 +26,26 @@ public async Task TestStaticFieldValidation()
             protos.Add(kind, ids);
         }
 
-        Assert.That(protoMan.ValidateStaticFields(typeof(StringValid), protos).Count, Is.Zero);
-        Assert.That(protoMan.ValidateStaticFields(typeof(StringArrayValid), protos).Count, Is.Zero);
-        Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdValid), protos).Count, Is.Zero);
-        Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdArrayValid), protos).Count, Is.Zero);
-        Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdTestValid), protos).Count, Is.Zero);
-        Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdArrayValid), protos).Count, Is.Zero);
-        Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdListValid), protos).Count, Is.Zero);
-        Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdSetValid), protos).Count, Is.Zero);
-        Assert.That(protoMan.ValidateStaticFields(typeof(PrivateProtoIdArrayValid), protos).Count, Is.Zero);
-        
-        Assert.That(protoMan.ValidateStaticFields(typeof(StringInvalid), protos).Count, Is.EqualTo(1));
-        Assert.That(protoMan.ValidateStaticFields(typeof(StringArrayInvalid), protos).Count, Is.EqualTo(2));
-        Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdInvalid), protos).Count, Is.EqualTo(1));
-        Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdArrayInvalid), protos).Count, Is.EqualTo(2));
-        Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdTestInvalid), protos).Count, Is.EqualTo(1));
-        Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdArrayInvalid), protos).Count, Is.EqualTo(2));
-        Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdListInvalid), protos).Count, Is.EqualTo(2));
-        Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdSetInvalid), protos).Count, Is.EqualTo(2));
-        Assert.That(protoMan.ValidateStaticFields(typeof(PrivateProtoIdArrayInvalid), protos).Count, Is.EqualTo(2));
-        
+        Assert.That(protoMan.ValidateStaticFields(typeof(StringValid), protos), Is.Empty);
+        Assert.That(protoMan.ValidateStaticFields(typeof(StringArrayValid), protos), Is.Empty);
+        Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdValid), protos), Is.Empty);
+        Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdArrayValid), protos), Is.Empty);
+        Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdTestValid), protos), Is.Empty);
+        Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdArrayValid), protos), Is.Empty);
+        Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdListValid), protos), Is.Empty);
+        Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdSetValid), protos), Is.Empty);
+        Assert.That(protoMan.ValidateStaticFields(typeof(PrivateProtoIdArrayValid), protos), Is.Empty);
+
+        Assert.That(protoMan.ValidateStaticFields(typeof(StringInvalid), protos), Has.Count.EqualTo(1));
+        Assert.That(protoMan.ValidateStaticFields(typeof(StringArrayInvalid), protos), Has.Count.EqualTo(2));
+        Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdInvalid), protos), Has.Count.EqualTo(1));
+        Assert.That(protoMan.ValidateStaticFields(typeof(EntProtoIdArrayInvalid), protos), Has.Count.EqualTo(2));
+        Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdTestInvalid), protos), Has.Count.EqualTo(1));
+        Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdArrayInvalid), protos), Has.Count.EqualTo(2));
+        Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdListInvalid), protos), Has.Count.EqualTo(2));
+        Assert.That(protoMan.ValidateStaticFields(typeof(ProtoIdSetInvalid), protos), Has.Count.EqualTo(2));
+        Assert.That(protoMan.ValidateStaticFields(typeof(PrivateProtoIdArrayInvalid), protos), Has.Count.EqualTo(2));
+
         await pair.CleanReturnAsync();
     }
 
@@ -58,93 +58,111 @@ public async Task TestStaticFieldValidation()
   id: StaticFieldTestTag
 ";
 
-    [Reflect(false)] private sealed class StringValid
+    [Reflect(false)]
+    private sealed class StringValid
     {
         [ValidatePrototypeId<TagPrototype>] public static string Tag = "StaticFieldTestTag";
     }
 
-    [Reflect(false)] private sealed class StringInvalid
+    [Reflect(false)]
+    private sealed class StringInvalid
     {
         [ValidatePrototypeId<TagPrototype>] public static string Tag = string.Empty;
     }
 
-    [Reflect(false)] private sealed class StringArrayValid
+    [Reflect(false)]
+    private sealed class StringArrayValid
     {
-        [ValidatePrototypeId<TagPrototype>] public static string[] Tag = {"StaticFieldTestTag", "StaticFieldTestTag"};
+        [ValidatePrototypeId<TagPrototype>] public static string[] Tag = ["StaticFieldTestTag", "StaticFieldTestTag"];
     }
 
-    [Reflect(false)] private sealed class StringArrayInvalid
+    [Reflect(false)]
+    private sealed class StringArrayInvalid
     {
-        [ValidatePrototypeId<TagPrototype>] public static string[] Tag = {string.Empty, "StaticFieldTestTag", string.Empty};
+        [ValidatePrototypeId<TagPrototype>] public static string[] Tag = [string.Empty, "StaticFieldTestTag", string.Empty];
     }
 
-    [Reflect(false)] private sealed class EntProtoIdValid
+    [Reflect(false)]
+    private sealed class EntProtoIdValid
     {
         public static EntProtoId Tag = "StaticFieldTestEnt";
     }
 
-    [Reflect(false)] private sealed class EntProtoIdInvalid
+    [Reflect(false)]
+    private sealed class EntProtoIdInvalid
     {
         public static EntProtoId Tag = string.Empty;
     }
 
-    [Reflect(false)] private sealed class EntProtoIdArrayValid
+    [Reflect(false)]
+    private sealed class EntProtoIdArrayValid
     {
-        public static EntProtoId[] Tag = {"StaticFieldTestEnt", "StaticFieldTestEnt"};
+        public static EntProtoId[] Tag = ["StaticFieldTestEnt", "StaticFieldTestEnt"];
     }
 
-    [Reflect(false)] private sealed class EntProtoIdArrayInvalid
+    [Reflect(false)]
+    private sealed class EntProtoIdArrayInvalid
     {
-        public static EntProtoId[] Tag = {string.Empty, "StaticFieldTestEnt", string.Empty};
+        public static EntProtoId[] Tag = [string.Empty, "StaticFieldTestEnt", string.Empty];
     }
 
-    [Reflect(false)] private sealed class ProtoIdTestValid
+    [Reflect(false)]
+    private sealed class ProtoIdTestValid
     {
         public static ProtoId<TagPrototype> Tag = "StaticFieldTestTag";
     }
 
-    [Reflect(false)] private sealed class ProtoIdTestInvalid
+    [Reflect(false)]
+    private sealed class ProtoIdTestInvalid
     {
         public static ProtoId<TagPrototype> Tag = string.Empty;
     }
 
-    [Reflect(false)] private sealed class ProtoIdArrayValid
+    [Reflect(false)]
+    private sealed class ProtoIdArrayValid
     {
-        public static ProtoId<TagPrototype>[] Tag = {"StaticFieldTestTag", "StaticFieldTestTag"};
+        public static ProtoId<TagPrototype>[] Tag = ["StaticFieldTestTag", "StaticFieldTestTag"];
     }
 
-    [Reflect(false)] private sealed class ProtoIdArrayInvalid
+    [Reflect(false)]
+    private sealed class ProtoIdArrayInvalid
     {
-        public static ProtoId<TagPrototype>[] Tag = {string.Empty, "StaticFieldTestTag", string.Empty};
+        public static ProtoId<TagPrototype>[] Tag = [string.Empty, "StaticFieldTestTag", string.Empty];
     }
 
-    [Reflect(false)] private sealed class ProtoIdListValid
+    [Reflect(false)]
+    private sealed class ProtoIdListValid
     {
-        public static List<ProtoId<TagPrototype>> Tag = new() {"StaticFieldTestTag", "StaticFieldTestTag"};
+        public static List<ProtoId<TagPrototype>> Tag = ["StaticFieldTestTag", "StaticFieldTestTag"];
     }
 
-    [Reflect(false)] private sealed class ProtoIdListInvalid
+    [Reflect(false)]
+    private sealed class ProtoIdListInvalid
     {
-        public static List<ProtoId<TagPrototype>> Tag = new() {string.Empty, "StaticFieldTestTag", string.Empty};
+        public static List<ProtoId<TagPrototype>> Tag = [string.Empty, "StaticFieldTestTag", string.Empty];
     }
 
-    [Reflect(false)] private sealed class ProtoIdSetValid
+    [Reflect(false)]
+    private sealed class ProtoIdSetValid
     {
-        public static HashSet<ProtoId<TagPrototype>> Tag = new() {"StaticFieldTestTag", "StaticFieldTestTag"};
+        public static HashSet<ProtoId<TagPrototype>> Tag = ["StaticFieldTestTag", "StaticFieldTestTag"];
     }
 
-    [Reflect(false)] private sealed class ProtoIdSetInvalid
+    [Reflect(false)]
+    private sealed class ProtoIdSetInvalid
     {
-        public static HashSet<ProtoId<TagPrototype>> Tag = new() {string.Empty, "StaticFieldTestTag", string.Empty, " "};
+        public static HashSet<ProtoId<TagPrototype>> Tag = [string.Empty, "StaticFieldTestTag", string.Empty, " "];
     }
 
-    [Reflect(false)] private sealed class PrivateProtoIdArrayValid
+    [Reflect(false)]
+    private sealed class PrivateProtoIdArrayValid
     {
-        private static ProtoId<TagPrototype>[] Tag = {"StaticFieldTestTag", "StaticFieldTestTag"};
+        private static readonly ProtoId<TagPrototype>[] Tag = ["StaticFieldTestTag", "StaticFieldTestTag"];
     }
 
-    [Reflect(false)] private sealed class PrivateProtoIdArrayInvalid
+    [Reflect(false)]
+    private sealed class PrivateProtoIdArrayInvalid
     {
-        private static ProtoId<TagPrototype>[] Tag = {string.Empty, "StaticFieldTestTag", string.Empty};
+        private static readonly ProtoId<TagPrototype>[] Tag = [string.Empty, "StaticFieldTestTag", string.Empty];
     }
 }
diff --git a/Content.IntegrationTests/Tests/MachineBoardTest.cs b/Content.IntegrationTests/Tests/MachineBoardTest.cs
index bd3a72f4c1d..097f38af420 100644
--- a/Content.IntegrationTests/Tests/MachineBoardTest.cs
+++ b/Content.IntegrationTests/Tests/MachineBoardTest.cs
@@ -2,6 +2,7 @@
 using System.Linq;
 using Content.Server.Construction.Components;
 using Content.Shared.Construction.Components;
+using Robust.Shared.GameObjects;
 using Robust.Shared.Prototypes;
 
 namespace Content.IntegrationTests.Tests;
@@ -35,6 +36,7 @@ public async Task TestMachineBoardHasValidMachine()
         var server = pair.Server;
 
         var protoMan = server.ResolveDependency<IPrototypeManager>();
+        var compFact = server.ResolveDependency<IComponentFactory>();
 
         await server.WaitAssertion(() =>
         {
@@ -43,7 +45,7 @@ await server.WaitAssertion(() =>
                          .Where(p => !pair.IsTestPrototype(p))
                          .Where(p => !_ignoredPrototypes.Contains(p.ID)))
             {
-                if (!p.TryGetComponent<MachineBoardComponent>(out var mbc))
+                if (!p.TryGetComponent<MachineBoardComponent>(out var mbc, compFact))
                     continue;
                 var mId = mbc.Prototype;
 
@@ -52,7 +54,7 @@ await server.WaitAssertion(() =>
                     Assert.That(mId, Is.Not.Null, $"Machine board {p.ID} does not have a corresponding machine.");
                     Assert.That(protoMan.TryIndex<EntityPrototype>(mId, out var mProto),
                         $"Machine board {p.ID}'s corresponding machine has an invalid prototype.");
-                    Assert.That(mProto.TryGetComponent<MachineComponent>(out var mComp),
+                    Assert.That(mProto.TryGetComponent<MachineComponent>(out var mComp, compFact),
                         $"Machine board {p.ID}'s corresponding machine {mId} does not have MachineComponent");
                     Assert.That(mComp.BoardPrototype, Is.EqualTo(p.ID),
                         $"Machine {mId}'s BoardPrototype is not equal to it's corresponding machine board, {p.ID}");
@@ -74,6 +76,7 @@ public async Task TestComputerBoardHasValidComputer()
         var server = pair.Server;
 
         var protoMan = server.ResolveDependency<IPrototypeManager>();
+        var compFact = server.ResolveDependency<IComponentFactory>();
 
         await server.WaitAssertion(() =>
         {
@@ -82,7 +85,7 @@ await server.WaitAssertion(() =>
                          .Where(p => !pair.IsTestPrototype(p))
                          .Where(p => !_ignoredPrototypes.Contains(p.ID)))
             {
-                if (!p.TryGetComponent<ComputerBoardComponent>(out var cbc))
+                if (!p.TryGetComponent<ComputerBoardComponent>(out var cbc, compFact))
                     continue;
                 var cId = cbc.Prototype;
 
@@ -91,7 +94,7 @@ await server.WaitAssertion(() =>
                     Assert.That(cId, Is.Not.Null, $"Computer board \"{p.ID}\" does not have a corresponding computer.");
                     Assert.That(protoMan.TryIndex<EntityPrototype>(cId, out var cProto),
                         $"Computer board \"{p.ID}\"'s corresponding computer has an invalid prototype.");
-                    Assert.That(cProto.TryGetComponent<ComputerComponent>(out var cComp),
+                    Assert.That(cProto.TryGetComponent<ComputerComponent>(out var cComp, compFact),
                         $"Computer board {p.ID}'s corresponding computer \"{cId}\" does not have ComputerComponent");
                     Assert.That(cComp.BoardPrototype, Is.EqualTo(p.ID),
                         $"Computer \"{cId}\"'s BoardPrototype is not equal to it's corresponding computer board, \"{p.ID}\"");
diff --git a/Content.IntegrationTests/Tests/Mapping/MappingTests.cs b/Content.IntegrationTests/Tests/Mapping/MappingTests.cs
index 287e30eb8b1..be8bad229b4 100644
--- a/Content.IntegrationTests/Tests/Mapping/MappingTests.cs
+++ b/Content.IntegrationTests/Tests/Mapping/MappingTests.cs
@@ -13,7 +13,7 @@ public sealed class MappingTests
     [Test]
     public async Task MappingTest()
     {
-        await using var pair = await PoolManager.GetServerClient(new PoolSettings {Dirty = true, Connected = true, DummyTicker = false});
+        await using var pair = await PoolManager.GetServerClient(new PoolSettings { Dirty = true, Connected = true, DummyTicker = false });
 
         var server = pair.Server;
         var entMan = server.EntMan;
diff --git a/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs b/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs
index 7f9c02fc13b..12b395f86d3 100644
--- a/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs
+++ b/Content.IntegrationTests/Tests/MaterialArbitrageTest.cs
@@ -38,15 +38,16 @@ public async Task NoMaterialArbitrage()
         await server.WaitIdleAsync();
 
         var entManager = server.ResolveDependency<IEntityManager>();
-        var sysManager = server.ResolveDependency<IEntitySystemManager>();
         var mapManager = server.ResolveDependency<IMapManager>();
-        Assert.That(mapManager.IsMapInitialized(testMap.MapId));
-
         var protoManager = server.ResolveDependency<IPrototypeManager>();
-        var pricing = sysManager.GetEntitySystem<PricingSystem>();
-        var stackSys = sysManager.GetEntitySystem<StackSystem>();
+
+        var pricing = entManager.System<PricingSystem>();
+        var stackSys = entManager.System<StackSystem>();
+        var mapSystem = server.System<SharedMapSystem>();
         var compFact = server.ResolveDependency<IComponentFactory>();
 
+        Assert.That(mapSystem.IsInitialized(testMap.MapId));
+
         var constructionName = compFact.GetComponentName(typeof(ConstructionComponent));
         var compositionName = compFact.GetComponentName(typeof(PhysicalCompositionComponent));
         var materialName = compFact.GetComponentName(typeof(MaterialComponent));
@@ -66,7 +67,7 @@ public async Task NoMaterialArbitrage()
         Dictionary<string, ConstructionComponent> constructionRecipes = new();
         foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
         {
-            if (proto.NoSpawn || proto.Abstract || pair.IsTestPrototype(proto))
+            if (proto.HideSpawnMenu || proto.Abstract || pair.IsTestPrototype(proto))
                 continue;
 
             if (!proto.Components.TryGetValue(constructionName, out var destructible))
@@ -126,7 +127,7 @@ public async Task NoMaterialArbitrage()
         // Here we get the set of entities/materials spawned when destroying an entity.
         foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
         {
-            if (proto.NoSpawn || proto.Abstract || pair.IsTestPrototype(proto))
+            if (proto.HideSpawnMenu || proto.Abstract || pair.IsTestPrototype(proto))
                 continue;
 
             if (!proto.Components.TryGetValue(destructibleName, out var destructible))
@@ -297,7 +298,7 @@ public async Task NoMaterialArbitrage()
         Dictionary<string, PhysicalCompositionComponent> physicalCompositions = new();
         foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
         {
-            if (proto.NoSpawn || proto.Abstract || pair.IsTestPrototype(proto))
+            if (proto.HideSpawnMenu || proto.Abstract || pair.IsTestPrototype(proto))
                 continue;
 
             if (!proto.Components.TryGetValue(compositionName, out var composition))
diff --git a/Content.IntegrationTests/Tests/Minds/GhostTests.cs b/Content.IntegrationTests/Tests/Minds/GhostTests.cs
index ad9d53a70db..3a860267e55 100644
--- a/Content.IntegrationTests/Tests/Minds/GhostTests.cs
+++ b/Content.IntegrationTests/Tests/Minds/GhostTests.cs
@@ -14,7 +14,7 @@ namespace Content.IntegrationTests.Tests.Minds;
 [TestFixture]
 public sealed class GhostTests
 {
-    struct GhostTestData
+    private struct GhostTestData
     {
         public IEntityManager SEntMan;
         public Robust.Server.Player.IPlayerManager SPlayerMan;
@@ -23,10 +23,10 @@ struct GhostTestData
 
         public TestPair Pair = default!;
 
-        public TestMapData MapData => Pair.TestMap!;
+        public readonly TestMapData MapData => Pair.TestMap!;
 
-        public RobustIntegrationTest.ServerIntegrationInstance Server => Pair.Server;
-        public RobustIntegrationTest.ClientIntegrationInstance Client => Pair.Client;
+        public readonly RobustIntegrationTest.ServerIntegrationInstance Server => Pair.Server;
+        public readonly RobustIntegrationTest.ClientIntegrationInstance Client => Pair.Client;
 
         /// <summary>
         /// Initial player coordinates. Note that this does not necessarily correspond to the position of the
@@ -47,15 +47,16 @@ public GhostTestData()
 
     private async Task<GhostTestData> SetupData()
     {
-        var data = new GhostTestData();
-
-        // Client is needed to create a session for the ghost system. Creating a dummy session was too difficult.
-        data.Pair = await PoolManager.GetServerClient(new PoolSettings
+        var data = new GhostTestData
         {
-            DummyTicker = false,
-            Connected = true,
-            Dirty = true
-        });
+            // Client is needed to create a session for the ghost system. Creating a dummy session was too difficult.
+            Pair = await PoolManager.GetServerClient(new PoolSettings
+            {
+                DummyTicker = false,
+                Connected = true,
+                Dirty = true
+            })
+        };
 
         data.SEntMan = data.Pair.Server.ResolveDependency<IServerEntityManager>();
         data.SPlayerMan = data.Pair.Server.ResolveDependency<Robust.Server.Player.IPlayerManager>();
@@ -64,7 +65,8 @@ private async Task<GhostTestData> SetupData()
 
         // Setup map.
         await data.Pair.CreateTestMap();
-        data.PlayerCoords = data.SEntMan.GetNetCoordinates(data.MapData.GridCoords.Offset(new Vector2(0.5f, 0.5f)).WithEntityId(data.MapData.MapUid, data.STransformSys, data.SEntMan));
+        var test = data.MapData.GridCoords.Offset(new Vector2(0.5f, 0.5f));
+        data.PlayerCoords = data.SEntMan.GetNetCoordinates(data.STransformSys.WithEntityId(data.MapData.GridCoords.Offset(new Vector2(0.5f, 0.5f)), data.MapData.MapUid));
 
         if (data.Client.Session == null)
             Assert.Fail("No player");
@@ -156,4 +158,20 @@ public async Task TestGridGhostOnQueueDelete()
         await data.Pair.CleanReturnAsync();
     }
 
+    [Test]
+    public async Task TestGhostGridNotTerminating()
+    {
+        var data = await SetupData();
+
+        Assert.DoesNotThrowAsync(async () =>
+        {
+            // Delete the grid
+            await data.Server.WaitPost(() => data.SEntMan.DeleteEntity(data.MapData.Grid.Owner));
+        });
+
+        await data.Pair.RunTicksSync(5);
+
+        await data.Pair.CleanReturnAsync();
+    }
+
 }
diff --git a/Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs b/Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs
index 428380631d7..b12c90e16e2 100644
--- a/Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs
+++ b/Content.IntegrationTests/Tests/Minds/MindTests.Helpers.cs
@@ -169,7 +169,7 @@ private static async Task Connect(Pair.TestPair pair, string username)
     {
         var netManager = pair.Client.ResolveDependency<IClientNetManager>();
         var playerMan = pair.Server.ResolveDependency<IPlayerManager>();
-        Assert.That(!playerMan.Sessions.Any());
+        Assert.That(playerMan.Sessions, Is.Empty);
 
         await Task.WhenAll(pair.Client.WaitIdleAsync(), pair.Client.WaitIdleAsync());
         pair.Client.SetConnectTarget(pair.Server);
diff --git a/Content.IntegrationTests/Tests/Movement/BuckleMovementTest.cs b/Content.IntegrationTests/Tests/Movement/BuckleMovementTest.cs
new file mode 100644
index 00000000000..3119ee55924
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Movement/BuckleMovementTest.cs
@@ -0,0 +1,63 @@
+using Content.Shared.Alert;
+using Content.Shared.Buckle.Components;
+using Robust.Shared.Maths;
+
+namespace Content.IntegrationTests.Tests.Movement;
+
+public sealed class BuckleMovementTest : MovementTest
+{
+    // Check that interacting with a chair straps you to it and prevents movement.
+    [Test]
+    public async Task ChairTest()
+    {
+        await SpawnTarget("Chair");
+
+        var cAlert = Client.System<AlertsSystem>();
+        var sAlert = Server.System<AlertsSystem>();
+        var buckle = Comp<BuckleComponent>(Player);
+        var strap = Comp<StrapComponent>(Target);
+
+#pragma warning disable RA0002
+        buckle.Delay = TimeSpan.Zero;
+#pragma warning restore RA0002
+
+        // Initially not buckled to the chair, and standing off to the side
+        Assert.That(Delta(), Is.InRange(0.9f, 1.1f));
+        Assert.That(buckle.Buckled, Is.False);
+        Assert.That(buckle.BuckledTo, Is.Null);
+        Assert.That(strap.BuckledEntities, Is.Empty);
+        Assert.That(cAlert.IsShowingAlert(CPlayer, strap.BuckledAlertType), Is.False);
+        Assert.That(sAlert.IsShowingAlert(SPlayer, strap.BuckledAlertType), Is.False);
+
+        // Interact results in being buckled to the chair
+        await Interact();
+        Assert.That(Delta(), Is.InRange(-0.01f, 0.01f));
+        Assert.That(buckle.Buckled, Is.True);
+        Assert.That(buckle.BuckledTo, Is.EqualTo(STarget));
+        Assert.That(strap.BuckledEntities, Is.EquivalentTo(new[] { SPlayer }));
+        Assert.That(cAlert.IsShowingAlert(CPlayer, strap.BuckledAlertType), Is.True);
+        Assert.That(sAlert.IsShowingAlert(SPlayer, strap.BuckledAlertType), Is.True);
+
+        // Attempting to walk away does nothing
+        await Move(DirectionFlag.East, 1);
+        Assert.That(Delta(), Is.InRange(-0.01f, 0.01f));
+        Assert.That(buckle.Buckled, Is.True);
+        Assert.That(buckle.BuckledTo, Is.EqualTo(STarget));
+        Assert.That(strap.BuckledEntities, Is.EquivalentTo(new[] { SPlayer }));
+        Assert.That(cAlert.IsShowingAlert(CPlayer, strap.BuckledAlertType), Is.True);
+        Assert.That(sAlert.IsShowingAlert(SPlayer, strap.BuckledAlertType), Is.True);
+
+        // Interacting again will unbuckle the player
+        await Interact();
+        Assert.That(Delta(), Is.InRange(-0.5f, 0.5f));
+        Assert.That(buckle.Buckled, Is.False);
+        Assert.That(buckle.BuckledTo, Is.Null);
+        Assert.That(strap.BuckledEntities, Is.Empty);
+        Assert.That(cAlert.IsShowingAlert(CPlayer, strap.BuckledAlertType), Is.False);
+        Assert.That(sAlert.IsShowingAlert(SPlayer, strap.BuckledAlertType), Is.False);
+
+        // And now they can move away
+        await Move(DirectionFlag.SouthEast, 1);
+        Assert.That(Delta(), Is.LessThan(-1));
+    }
+}
diff --git a/Content.IntegrationTests/Tests/Interaction/MovementTest.cs b/Content.IntegrationTests/Tests/Movement/MovementTest.cs
similarity index 93%
rename from Content.IntegrationTests/Tests/Interaction/MovementTest.cs
rename to Content.IntegrationTests/Tests/Movement/MovementTest.cs
index dc5aec92cfc..eba92530388 100644
--- a/Content.IntegrationTests/Tests/Interaction/MovementTest.cs
+++ b/Content.IntegrationTests/Tests/Movement/MovementTest.cs
@@ -1,8 +1,9 @@
 #nullable enable
 using System.Numerics;
+using Content.IntegrationTests.Tests.Interaction;
 using Robust.Shared.GameObjects;
 
-namespace Content.IntegrationTests.Tests.Interaction;
+namespace Content.IntegrationTests.Tests.Movement;
 
 /// <summary>
 /// This is a variation of <see cref="InteractionTest"/> that sets up the player with a normal human entity and a simple
@@ -31,7 +32,7 @@ public override async Task Setup()
 
         for (var i = -Tiles; i <= Tiles; i++)
         {
-            await SetTile(Plating, SEntMan.GetNetCoordinates(pCoords.Offset(new Vector2(i, 0))), MapData.Grid.Comp);
+            await SetTile(Plating, SEntMan.GetNetCoordinates(pCoords.Offset(new Vector2(i, 0))), MapData.Grid);
         }
         AssertGridCount(1);
 
diff --git a/Content.IntegrationTests/Tests/Movement/PullingTest.cs b/Content.IntegrationTests/Tests/Movement/PullingTest.cs
new file mode 100644
index 00000000000..d96c4ea0e56
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Movement/PullingTest.cs
@@ -0,0 +1,73 @@
+#nullable enable
+using Content.Shared.Alert;
+using Content.Shared.Input;
+using Content.Shared.Movement.Pulling.Components;
+using Robust.Shared.Maths;
+
+namespace Content.IntegrationTests.Tests.Movement;
+
+public sealed class PullingTest : MovementTest
+{
+    protected override int Tiles => 4;
+
+    [Test]
+    public async Task PullTest()
+    {
+        var cAlert = Client.System<AlertsSystem>();
+        var sAlert = Server.System<AlertsSystem>();
+        await SpawnTarget("MobHuman");
+
+        var puller = Comp<PullerComponent>(Player);
+        var pullable = Comp<PullableComponent>(Target);
+
+        // Player is initially to the left of the target and not pulling anything
+        Assert.That(Delta(), Is.InRange(0.9f, 1.1f));
+        Assert.That(puller.Pulling, Is.Null);
+        Assert.That(pullable.Puller, Is.Null);
+        Assert.That(pullable.BeingPulled, Is.False);
+        Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.False);
+        Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.False);
+
+        // Start pulling
+        await PressKey(ContentKeyFunctions.TryPullObject);
+        await RunTicks(5);
+        Assert.That(puller.Pulling, Is.EqualTo(STarget));
+        Assert.That(pullable.Puller, Is.EqualTo(SPlayer));
+        Assert.That(pullable.BeingPulled, Is.True);
+        Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.True);
+        Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.True);
+
+        // Move to the left and check that the target moves with the player and is still being pulled.
+        await Move(DirectionFlag.West, 1);
+        Assert.That(Delta(), Is.InRange(0.9f, 1.3f));
+        Assert.That(puller.Pulling, Is.EqualTo(STarget));
+        Assert.That(pullable.Puller, Is.EqualTo(SPlayer));
+        Assert.That(pullable.BeingPulled, Is.True);
+        Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.True);
+        Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.True);
+
+        // Move in the other direction
+        await Move(DirectionFlag.East, 2);
+        Assert.That(Delta(), Is.InRange(-1.3f, -0.9f));
+        Assert.That(puller.Pulling, Is.EqualTo(STarget));
+        Assert.That(pullable.Puller, Is.EqualTo(SPlayer));
+        Assert.That(pullable.BeingPulled, Is.True);
+        Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.True);
+        Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.True);
+
+        // Stop pulling
+        await PressKey(ContentKeyFunctions.ReleasePulledObject);
+        await RunTicks(5);
+        Assert.That(Delta(), Is.InRange(-1.3f, -0.9f));
+        Assert.That(puller.Pulling, Is.Null);
+        Assert.That(pullable.Puller, Is.Null);
+        Assert.That(pullable.BeingPulled, Is.False);
+        Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.False);
+        Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.False);
+
+        // Move back to the left and ensure the target is no longer following us.
+        await Move(DirectionFlag.West, 2);
+        Assert.That(Delta(), Is.GreaterThan(2f));
+    }
+}
+
diff --git a/Content.IntegrationTests/Tests/Slipping/SlippingTest.cs b/Content.IntegrationTests/Tests/Movement/SlippingTest.cs
similarity index 92%
rename from Content.IntegrationTests/Tests/Slipping/SlippingTest.cs
rename to Content.IntegrationTests/Tests/Movement/SlippingTest.cs
index 28da7a94658..9ac84a0a586 100644
--- a/Content.IntegrationTests/Tests/Slipping/SlippingTest.cs
+++ b/Content.IntegrationTests/Tests/Movement/SlippingTest.cs
@@ -11,7 +11,7 @@
 using Robust.Shared.IoC;
 using Robust.Shared.Maths;
 
-namespace Content.IntegrationTests.Tests.Slipping;
+namespace Content.IntegrationTests.Tests.Movement;
 
 public sealed class SlippingTest : MovementTest
 {
@@ -41,18 +41,14 @@ public async Task BananaSlipTest()
         // Assert.That(modifier, Is.EqualTo(1), "Player is not moving at full speed."); // Yeeting this pointless Assert because it's not actually important.
 
         // Player is to the left of the banana peel and has not slipped.
-#pragma warning disable NUnit2045
         Assert.That(Delta(), Is.GreaterThan(0.5f));
         Assert.That(sys.Slipped, Does.Not.Contain(SEntMan.GetEntity(Player)));
-#pragma warning restore NUnit2045
 
         // Walking over the banana slowly does not trigger a slip.
         await SetKey(EngineKeyFunctions.Walk, sprintWalks ? BoundKeyState.Up : BoundKeyState.Down);
         await Move(DirectionFlag.East, 1f);
-#pragma warning disable NUnit2045
         Assert.That(Delta(), Is.LessThan(0.5f));
         Assert.That(sys.Slipped, Does.Not.Contain(SEntMan.GetEntity(Player)));
-#pragma warning restore NUnit2045
         AssertComp<KnockedDownComponent>(false, Player);
 
         // Moving at normal speeds does trigger a slip.
diff --git a/Content.IntegrationTests/Tests/Networking/PvsCommandTest.cs b/Content.IntegrationTests/Tests/Networking/PvsCommandTest.cs
index 4783d21a053..b3955698489 100644
--- a/Content.IntegrationTests/Tests/Networking/PvsCommandTest.cs
+++ b/Content.IntegrationTests/Tests/Networking/PvsCommandTest.cs
@@ -7,12 +7,12 @@ namespace Content.IntegrationTests.Tests.Networking;
 [TestFixture]
 public sealed class PvsCommandTest
 {
-    public static EntProtoId TestEnt = "MobHuman";
+    private static readonly EntProtoId TestEnt = "MobHuman";
 
     [Test]
     public async Task TestPvsCommands()
     {
-        await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true, DummyTicker = false});
+        await using var pair = await PoolManager.GetServerClient(new PoolSettings { Connected = true, DummyTicker = false });
         var (server, client) = pair;
         await pair.RunTicksSync(5);
 
diff --git a/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs b/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs
index 52d464fa41e..29f2573c2d9 100644
--- a/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs
+++ b/Content.IntegrationTests/Tests/Networking/SimplePredictReconcileTest.cs
@@ -51,11 +51,12 @@ public async Task Test()
             PredictionTestComponent clientComponent = default!;
             var serverSystem = sEntityManager.System<PredictionTestEntitySystem>();
             var clientSystem = cEntityManager.System<PredictionTestEntitySystem>();
+            var sMapSys = sEntityManager.System<SharedMapSystem>();
 
             await server.WaitPost(() =>
             {
                 // Spawn dummy component entity.
-                var map = sMapManager.CreateMap();
+                sMapSys.CreateMap(out var map);
                 serverEnt = sEntityManager.SpawnEntity(null, new MapCoordinates(new Vector2(0, 0), map));
                 serverComponent = sEntityManager.AddComponent<PredictionTestComponent>(serverEnt);
             });
@@ -67,7 +68,7 @@ await server.WaitPost(() =>
             Assert.That(sGameTiming.TickTimingAdjustment, Is.EqualTo(0));
 
             // Check client buffer is full
-            Assert.That(cGameStateManager.CurrentBufferSize, Is.EqualTo(cGameStateManager.TargetBufferSize));
+            Assert.That(cGameStateManager.GetApplicableStateCount(), Is.EqualTo(cGameStateManager.TargetBufferSize));
             Assert.That(cGameStateManager.TargetBufferSize, Is.EqualTo(2));
 
             // This isn't required anymore, but the test had this for the sake of "technical things", and I cbf shifting
@@ -99,7 +100,7 @@ await client.WaitPost(() =>
 
                 // Client last ran tick 15 meaning it's ahead of the last server tick it processed (12)
                 Assert.That(cGameTiming.CurTick, Is.EqualTo(expected));
-                Assert.That(cGameTiming.LastProcessedTick, Is.EqualTo(new GameTick((uint)(baseTick - cGameStateManager.TargetBufferSize))));
+                Assert.That(cGameTiming.LastProcessedTick, Is.EqualTo(new GameTick((uint) (baseTick - cGameStateManager.TargetBufferSize))));
             });
 
             // *** I am using block scopes to visually distinguish these sections of the test to make it more readable.
@@ -264,7 +265,7 @@ await client.WaitPost(() =>
                 // Assert timing is still correct.
                 Assert.That(sGameTiming.CurTick, Is.EqualTo(new GameTick(baseTick + 8)));
                 Assert.That(cGameTiming.CurTick, Is.EqualTo(new GameTick(baseTick + 8 + delta)));
-                Assert.That(cGameTiming.LastProcessedTick, Is.EqualTo(new GameTick((uint)(baseTick + 8 - cGameStateManager.TargetBufferSize))));
+                Assert.That(cGameTiming.LastProcessedTick, Is.EqualTo(new GameTick((uint) (baseTick + 8 - cGameStateManager.TargetBufferSize))));
             });
 
             {
diff --git a/Content.IntegrationTests/Tests/Payload/ModularGrenadeTests.cs b/Content.IntegrationTests/Tests/Payload/ModularGrenadeTests.cs
index 70179fdec1a..4db79373d38 100644
--- a/Content.IntegrationTests/Tests/Payload/ModularGrenadeTests.cs
+++ b/Content.IntegrationTests/Tests/Payload/ModularGrenadeTests.cs
@@ -22,32 +22,32 @@ public async Task AssembleAndDetonateGrenade()
         Target = SEntMan.GetNetEntity(await FindEntity("ModularGrenade"));
 
         await Drop();
-        await Interact(Cable);
+        await InteractUsing(Cable);
 
         // Insert & remove trigger
         AssertComp<OnUseTimerTriggerComponent>(false);
-        await Interact(Trigger);
+        await InteractUsing(Trigger);
         AssertComp<OnUseTimerTriggerComponent>();
         await FindEntity(Trigger, LookupFlags.Uncontained, shouldSucceed: false);
-        await Interact(Pry);
+        await InteractUsing(Pry);
         AssertComp<OnUseTimerTriggerComponent>(false);
 
         // Trigger was dropped to floor, not deleted.
         await FindEntity(Trigger, LookupFlags.Uncontained);
 
         // Re-insert
-        await Interact(Trigger);
+        await InteractUsing(Trigger);
         AssertComp<OnUseTimerTriggerComponent>();
 
         // Insert & remove payload.
-        await Interact(Payload);
+        await InteractUsing(Payload);
         await FindEntity(Payload, LookupFlags.Uncontained, shouldSucceed: false);
-        await Interact(Pry);
+        await InteractUsing(Pry);
         var ent = await FindEntity(Payload, LookupFlags.Uncontained);
         await Delete(ent);
 
         // successfully insert a second time
-        await Interact(Payload);
+        await InteractUsing(Payload);
         ent = await FindEntity(Payload);
         var sys = SEntMan.System<SharedContainerSystem>();
         Assert.That(sys.IsEntityInContainer(ent));
diff --git a/Content.IntegrationTests/Tests/PostMapInitTest.cs b/Content.IntegrationTests/Tests/PostMapInitTest.cs
index 05f603408b3..aebe3a1c0eb 100644
--- a/Content.IntegrationTests/Tests/PostMapInitTest.cs
+++ b/Content.IntegrationTests/Tests/PostMapInitTest.cs
@@ -16,6 +16,7 @@
 using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
 using Robust.Shared.Prototypes;
+using FastAccessors;
 using Robust.Shared.Utility;
 using YamlDotNet.RepresentationModel;
 
@@ -77,13 +78,14 @@ public async Task GridsLoadableTest(string mapFile)
 
             var entManager = server.ResolveDependency<IEntityManager>();
             var mapLoader = entManager.System<MapLoaderSystem>();
+            var mapSystem = entManager.System<SharedMapSystem>();
             var mapManager = server.ResolveDependency<IMapManager>();
             var cfg = server.ResolveDependency<IConfigurationManager>();
             Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False);
 
             await server.WaitPost(() =>
             {
-                var mapId = mapManager.CreateMap();
+                mapSystem.CreateMap(out var mapId);
                 try
                 {
 #pragma warning disable NUnit2045
@@ -164,6 +166,7 @@ public async Task GameMapsLoadableTest(string mapProto)
             var mapManager = server.ResolveDependency<IMapManager>();
             var entManager = server.ResolveDependency<IEntityManager>();
             var mapLoader = entManager.System<MapLoaderSystem>();
+            var mapSystem = entManager.System<SharedMapSystem>();
             var protoManager = server.ResolveDependency<IPrototypeManager>();
             var ticker = entManager.EntitySysManager.GetEntitySystem<GameTicker>();
             var shuttleSystem = entManager.EntitySysManager.GetEntitySystem<ShuttleSystem>();
@@ -173,7 +176,7 @@ public async Task GameMapsLoadableTest(string mapProto)
 
             await server.WaitPost(() =>
             {
-                var mapId = mapManager.CreateMap();
+                mapSystem.CreateMap(out var mapId);
                 try
                 {
                     ticker.LoadGameMap(protoManager.Index<GameMapPrototype>(mapProto), mapId, null);
@@ -183,7 +186,7 @@ await server.WaitPost(() =>
                     throw new Exception($"Failed to load map {mapProto}", ex);
                 }
 
-                var shuttleMap = mapManager.CreateMap();
+                mapSystem.CreateMap(out var shuttleMap);
                 var largest = 0f;
                 EntityUid? targetGrid = null;
                 var memberQuery = entManager.GetEntityQuery<StationMemberComponent>();
@@ -242,24 +245,17 @@ await server.WaitPost(() =>
                         Assert.That(lateSpawns, Is.GreaterThan(0), $"Found no latejoin spawn points on {mapProto}");
                     }
 
+                    var comp = entManager.GetComponent<StationJobsComponent>(station);
+                    var jobs = new HashSet<string>(comp.SetupAvailableJobs.Keys);
+
                     // Test all availableJobs have spawnPoints
                     // This is done inside gamemap test because loading the map takes ages and we already have it.
-                    var jobList = entManager.GetComponent<StationJobsComponent>(station).RoundStartJobList
-                        .Where(x => x.Value != 0)
-                        .Select(x => x.Key);
                     var spawnPoints = entManager.EntityQuery<SpawnPointComponent>()
-                        .Where(spawnpoint => spawnpoint.SpawnType == SpawnPointType.Job)
-                        .Select(spawnpoint => spawnpoint.Job.ID)
-                        .Distinct();
-                    List<string> missingSpawnPoints = new();
-                    foreach (var spawnpoint in jobList.Except(spawnPoints))
-                    {
-                        if (protoManager.Index<JobPrototype>(spawnpoint).SetPreference)
-                            missingSpawnPoints.Add(spawnpoint);
-                    }
+                        .Where(x => x.SpawnType == SpawnPointType.Job)
+                        .Select(x => x.Job!.ID);
 
-                    Assert.That(missingSpawnPoints, Has.Count.EqualTo(0),
-                        $"There is no spawnpoint for {string.Join(", ", missingSpawnPoints)} on {mapProto}.");
+                    jobs.ExceptWith(spawnPoints);
+                    Assert.That(jobs, Is.Empty, $"There is no spawnpoints for {string.Join(", ", jobs)} on {mapProto}.");
                 }
 
                 try
@@ -332,6 +328,7 @@ public async Task NonGameMapsLoadableTest()
             var resourceManager = server.ResolveDependency<IResourceManager>();
             var protoManager = server.ResolveDependency<IPrototypeManager>();
             var cfg = server.ResolveDependency<IConfigurationManager>();
+            var mapSystem = server.System<SharedMapSystem>();
             Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False);
 
             var gameMaps = protoManager.EnumeratePrototypes<GameMapPrototype>().Select(o => o.MapPath).ToHashSet();
@@ -362,7 +359,7 @@ await server.WaitPost(() =>
                 {
                     foreach (var mapName in mapNames)
                     {
-                        var mapId = mapManager.CreateMap();
+                        mapSystem.CreateMap(out var mapId);
                         try
                         {
                             Assert.That(mapLoader.TryLoad(mapId, mapName, out _));
diff --git a/Content.IntegrationTests/Tests/Power/PowerTest.cs b/Content.IntegrationTests/Tests/Power/PowerTest.cs
index a94e94489c0..55bb42f8ced 100644
--- a/Content.IntegrationTests/Tests/Power/PowerTest.cs
+++ b/Content.IntegrationTests/Tests/Power/PowerTest.cs
@@ -166,6 +166,7 @@ public async Task TestSimpleSurplus()
             var server = pair.Server;
             var mapManager = server.ResolveDependency<IMapManager>();
             var entityManager = server.ResolveDependency<IEntityManager>();
+            var mapSys = entityManager.System<SharedMapSystem>();
             const float loadPower = 200;
             PowerSupplierComponent supplier = default!;
             PowerConsumerComponent consumer1 = default!;
@@ -173,21 +174,19 @@ public async Task TestSimpleSurplus()
 
             await server.WaitAssertion(() =>
             {
-                var map = mapManager.CreateMap();
-                var grid = mapManager.CreateGrid(map);
-
-                var gridOwner = grid.Owner;
+                var map = mapSys.CreateMap(out var mapId);
+                var grid = mapManager.CreateGridEntity(mapId);
 
                 // Power only works when anchored
                 for (var i = 0; i < 3; i++)
                 {
-                    grid.SetTile(new Vector2i(0, i), new Tile(1));
-                    entityManager.SpawnEntity("CableHV", gridOwner.ToCoordinates(0, i));
+                    mapSys.SetTile(grid, new Vector2i(0, i), new Tile(1));
+                    entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i));
                 }
 
-                var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", gridOwner.ToCoordinates());
-                var consumerEnt1 = entityManager.SpawnEntity("ConsumerDummy", gridOwner.ToCoordinates(0, 1));
-                var consumerEnt2 = entityManager.SpawnEntity("ConsumerDummy", gridOwner.ToCoordinates(0, 2));
+                var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates());
+                var consumerEnt1 = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 1));
+                var consumerEnt2 = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 2));
 
                 supplier = entityManager.GetComponent<PowerSupplierComponent>(generatorEnt);
                 consumer1 = entityManager.GetComponent<PowerConsumerComponent>(consumerEnt1);
@@ -229,6 +228,7 @@ public async Task TestSimpleDeficit()
             var server = pair.Server;
             var mapManager = server.ResolveDependency<IMapManager>();
             var entityManager = server.ResolveDependency<IEntityManager>();
+            var mapSys = entityManager.System<SharedMapSystem>();
             const float loadPower = 200;
             PowerSupplierComponent supplier = default!;
             PowerConsumerComponent consumer1 = default!;
@@ -236,21 +236,19 @@ public async Task TestSimpleDeficit()
 
             await server.WaitAssertion(() =>
             {
-                var map = mapManager.CreateMap();
-                var grid = mapManager.CreateGrid(map);
-
-                var gridOwner = grid.Owner;
+                var map = mapSys.CreateMap(out var mapId);
+                var grid = mapManager.CreateGridEntity(mapId);
 
                 // Power only works when anchored
                 for (var i = 0; i < 3; i++)
                 {
-                    grid.SetTile(new Vector2i(0, i), new Tile(1));
-                    entityManager.SpawnEntity("CableHV", gridOwner.ToCoordinates(0, i));
+                    mapSys.SetTile(grid, new Vector2i(0, i), new Tile(1));
+                    entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i));
                 }
 
-                var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", gridOwner.ToCoordinates());
-                var consumerEnt1 = entityManager.SpawnEntity("ConsumerDummy", gridOwner.ToCoordinates(0, 1));
-                var consumerEnt2 = entityManager.SpawnEntity("ConsumerDummy", gridOwner.ToCoordinates(0, 2));
+                var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates());
+                var consumerEnt1 = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 1));
+                var consumerEnt2 = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 2));
 
                 supplier = entityManager.GetComponent<PowerSupplierComponent>(generatorEnt);
                 consumer1 = entityManager.GetComponent<PowerConsumerComponent>(consumerEnt1);
@@ -288,25 +286,25 @@ public async Task TestSupplyRamp()
             var server = pair.Server;
             var mapManager = server.ResolveDependency<IMapManager>();
             var entityManager = server.ResolveDependency<IEntityManager>();
+            var mapSys = entityManager.System<SharedMapSystem>();
             var gameTiming = server.ResolveDependency<IGameTiming>();
             PowerSupplierComponent supplier = default!;
             PowerConsumerComponent consumer = default!;
 
             await server.WaitAssertion(() =>
             {
-                var map = mapManager.CreateMap();
-                var grid = mapManager.CreateGrid(map);
-                var gridOwner = grid.Owner;
+                var map = mapSys.CreateMap(out var mapId);
+                var grid = mapManager.CreateGridEntity(mapId);
 
                 // Power only works when anchored
                 for (var i = 0; i < 3; i++)
                 {
-                    grid.SetTile(new Vector2i(0, i), new Tile(1));
-                    entityManager.SpawnEntity("CableHV", gridOwner.ToCoordinates(0, i));
+                    mapSys.SetTile(grid, new Vector2i(0, i), new Tile(1));
+                    entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i));
                 }
 
-                var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", gridOwner.ToCoordinates());
-                var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", gridOwner.ToCoordinates(0, 2));
+                var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates());
+                var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 2));
 
                 supplier = entityManager.GetComponent<PowerSupplierComponent>(generatorEnt);
                 consumer = entityManager.GetComponent<PowerConsumerComponent>(consumerEnt);
@@ -378,6 +376,7 @@ public async Task TestBatteryRamp()
             var entityManager = server.ResolveDependency<IEntityManager>();
             var gameTiming = server.ResolveDependency<IGameTiming>();
             var batterySys = entityManager.System<BatterySystem>();
+            var mapSys = entityManager.System<SharedMapSystem>();
             const float startingCharge = 100_000;
 
             PowerNetworkBatteryComponent netBattery = default!;
@@ -386,19 +385,18 @@ public async Task TestBatteryRamp()
 
             await server.WaitAssertion(() =>
             {
-                var map = mapManager.CreateMap();
-                var grid = mapManager.CreateGrid(map);
-                var gridOwner = grid.Owner;
+                var map = mapSys.CreateMap(out var mapId);
+                var grid = mapManager.CreateGridEntity(mapId);
 
                 // Power only works when anchored
                 for (var i = 0; i < 3; i++)
                 {
-                    grid.SetTile(new Vector2i(0, i), new Tile(1));
-                    entityManager.SpawnEntity("CableHV", gridOwner.ToCoordinates(0, i));
+                    mapSys.SetTile(grid, new Vector2i(0, i), new Tile(1));
+                    entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i));
                 }
 
-                var generatorEnt = entityManager.SpawnEntity("DischargingBatteryDummy", gridOwner.ToCoordinates());
-                var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", gridOwner.ToCoordinates(0, 2));
+                var generatorEnt = entityManager.SpawnEntity("DischargingBatteryDummy", grid.Owner.ToCoordinates());
+                var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 2));
 
                 netBattery = entityManager.GetComponent<PowerNetworkBatteryComponent>(generatorEnt);
                 battery = entityManager.GetComponent<BatteryComponent>(generatorEnt);
@@ -479,6 +477,7 @@ public async Task TestNoDemandRampdown()
             var mapManager = server.ResolveDependency<IMapManager>();
             var entityManager = server.ResolveDependency<IEntityManager>();
             var batterySys = entityManager.System<BatterySystem>();
+            var mapSys = entityManager.System<SharedMapSystem>();
             PowerSupplierComponent supplier = default!;
             PowerNetworkBatteryComponent netBattery = default!;
             BatteryComponent battery = default!;
@@ -490,20 +489,19 @@ public async Task TestNoDemandRampdown()
 
             await server.WaitAssertion(() =>
             {
-                var map = mapManager.CreateMap();
-                var grid = mapManager.CreateGrid(map);
-                var gridOwner = grid.Owner;
+                var map = mapSys.CreateMap(out var mapId);
+                var grid = mapManager.CreateGridEntity(mapId);
 
                 // Power only works when anchored
                 for (var i = 0; i < 3; i++)
                 {
-                    grid.SetTile(new Vector2i(0, i), new Tile(1));
-                    entityManager.SpawnEntity("CableHV", gridOwner.ToCoordinates(0, i));
+                    mapSys.SetTile(grid, new Vector2i(0, i), new Tile(1));
+                    entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i));
                 }
 
-                var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", gridOwner.ToCoordinates());
-                var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", gridOwner.ToCoordinates(0, 1));
-                var batteryEnt = entityManager.SpawnEntity("DischargingBatteryDummy", gridOwner.ToCoordinates(0, 2));
+                var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates());
+                var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 1));
+                var batteryEnt = entityManager.SpawnEntity("DischargingBatteryDummy", grid.Owner.ToCoordinates(0, 2));
                 netBattery = entityManager.GetComponent<PowerNetworkBatteryComponent>(batteryEnt);
                 battery = entityManager.GetComponent<BatteryComponent>(batteryEnt);
                 supplier = entityManager.GetComponent<PowerSupplierComponent>(generatorEnt);
@@ -577,24 +575,24 @@ public async Task TestSimpleBatteryChargeDeficit()
             var gameTiming = server.ResolveDependency<IGameTiming>();
             var entityManager = server.ResolveDependency<IEntityManager>();
             var batterySys = entityManager.System<BatterySystem>();
+            var mapSys = entityManager.System<SharedMapSystem>();
             PowerSupplierComponent supplier = default!;
             BatteryComponent battery = default!;
 
             await server.WaitAssertion(() =>
             {
-                var map = mapManager.CreateMap();
-                var grid = mapManager.CreateGrid(map);
-                var gridOwner = grid.Owner;
+                var map = mapSys.CreateMap(out var mapId);
+                var grid = mapManager.CreateGridEntity(mapId);
 
                 // Power only works when anchored
                 for (var i = 0; i < 3; i++)
                 {
-                    grid.SetTile(new Vector2i(0, i), new Tile(1));
-                    entityManager.SpawnEntity("CableHV", gridOwner.ToCoordinates(0, i));
+                    mapSys.SetTile(grid, new Vector2i(0, i), new Tile(1));
+                    entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i));
                 }
 
-                var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", gridOwner.ToCoordinates());
-                var batteryEnt = entityManager.SpawnEntity("ChargingBatteryDummy", gridOwner.ToCoordinates(0, 2));
+                var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates());
+                var batteryEnt = entityManager.SpawnEntity("ChargingBatteryDummy", grid.Owner.ToCoordinates(0, 2));
 
                 supplier = entityManager.GetComponent<PowerSupplierComponent>(generatorEnt);
                 var netBattery = entityManager.GetComponent<PowerNetworkBatteryComponent>(batteryEnt);
@@ -634,6 +632,7 @@ public async Task TestFullBattery()
             var entityManager = server.ResolveDependency<IEntityManager>();
             var gameTiming = server.ResolveDependency<IGameTiming>();
             var batterySys = entityManager.System<BatterySystem>();
+            var mapSys = entityManager.System<SharedMapSystem>();
             PowerConsumerComponent consumer = default!;
             PowerSupplierComponent supplier = default!;
             PowerNetworkBatteryComponent netBattery = default!;
@@ -641,23 +640,22 @@ public async Task TestFullBattery()
 
             await server.WaitAssertion(() =>
             {
-                var map = mapManager.CreateMap();
-                var grid = mapManager.CreateGrid(map);
-                var gridOwner = grid.Owner;
+                var map = mapSys.CreateMap(out var mapId);
+                var grid = mapManager.CreateGridEntity(mapId);
 
                 // Power only works when anchored
                 for (var i = 0; i < 4; i++)
                 {
-                    grid.SetTile(new Vector2i(0, i), new Tile(1));
-                    entityManager.SpawnEntity("CableHV", gridOwner.ToCoordinates(0, i));
+                    mapSys.SetTile(grid, new Vector2i(0, i), new Tile(1));
+                    entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i));
                 }
 
-                var terminal = entityManager.SpawnEntity("CableTerminal", gridOwner.ToCoordinates(0, 1));
+                var terminal = entityManager.SpawnEntity("CableTerminal", grid.Owner.ToCoordinates(0, 1));
                 entityManager.GetComponent<TransformComponent>(terminal).LocalRotation = Angle.FromDegrees(180);
 
-                var batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", gridOwner.ToCoordinates(0, 2));
-                var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", gridOwner.ToCoordinates(0, 0));
-                var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", gridOwner.ToCoordinates(0, 3));
+                var batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
+                var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
+                var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3));
 
                 consumer = entityManager.GetComponent<PowerConsumerComponent>(consumerEnt);
                 supplier = entityManager.GetComponent<PowerSupplierComponent>(supplyEnt);
@@ -712,6 +710,7 @@ public async Task TestFullBatteryEfficiencyPassThrough()
             var entityManager = server.ResolveDependency<IEntityManager>();
             var gameTiming = server.ResolveDependency<IGameTiming>();
             var batterySys = entityManager.System<BatterySystem>();
+            var mapSys = entityManager.System<SharedMapSystem>();
             PowerConsumerComponent consumer = default!;
             PowerSupplierComponent supplier = default!;
             PowerNetworkBatteryComponent netBattery = default!;
@@ -719,23 +718,22 @@ public async Task TestFullBatteryEfficiencyPassThrough()
 
             await server.WaitAssertion(() =>
             {
-                var map = mapManager.CreateMap();
-                var grid = mapManager.CreateGrid(map);
-                var gridOwner = grid.Owner;
+                var map = mapSys.CreateMap(out var mapId);
+                var grid = mapManager.CreateGridEntity(mapId);
 
                 // Power only works when anchored
                 for (var i = 0; i < 4; i++)
                 {
-                    grid.SetTile(new Vector2i(0, i), new Tile(1));
-                    entityManager.SpawnEntity("CableHV", gridOwner.ToCoordinates(0, i));
+                    mapSys.SetTile(grid, new Vector2i(0, i), new Tile(1));
+                    entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i));
                 }
 
-                var terminal = entityManager.SpawnEntity("CableTerminal", gridOwner.ToCoordinates(0, 1));
+                var terminal = entityManager.SpawnEntity("CableTerminal", grid.Owner.ToCoordinates(0, 1));
                 entityManager.GetComponent<TransformComponent>(terminal).LocalRotation = Angle.FromDegrees(180);
 
-                var batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", gridOwner.ToCoordinates(0, 2));
-                var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", gridOwner.ToCoordinates(0, 0));
-                var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", gridOwner.ToCoordinates(0, 3));
+                var batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
+                var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
+                var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3));
 
                 consumer = entityManager.GetComponent<PowerConsumerComponent>(consumerEnt);
                 supplier = entityManager.GetComponent<PowerSupplierComponent>(supplyEnt);
@@ -789,15 +787,15 @@ public async Task TestFullBatteryEfficiencyDemandPassThrough()
             var mapManager = server.ResolveDependency<IMapManager>();
             var entityManager = server.ResolveDependency<IEntityManager>();
             var batterySys = entityManager.System<BatterySystem>();
+            var mapSys = entityManager.System<SharedMapSystem>();
             PowerConsumerComponent consumer1 = default!;
             PowerConsumerComponent consumer2 = default!;
             PowerSupplierComponent supplier = default!;
 
             await server.WaitAssertion(() =>
             {
-                var map = mapManager.CreateMap();
-                var grid = mapManager.CreateGrid(map);
-                var gridOwner = grid.Owner;
+                var map = mapSys.CreateMap(out var mapId);
+                var grid = mapManager.CreateGridEntity(mapId);
 
                 // Map layout here is
                 // C - consumer
@@ -810,19 +808,19 @@ await server.WaitAssertion(() =>
                 // Power only works when anchored
                 for (var i = 0; i < 5; i++)
                 {
-                    grid.SetTile(new Vector2i(0, i), new Tile(1));
-                    entityManager.SpawnEntity("CableHV", gridOwner.ToCoordinates(0, i));
+                    mapSys.SetTile(grid, new Vector2i(0, i), new Tile(1));
+                    entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i));
                 }
 
-                entityManager.SpawnEntity("CableTerminal", gridOwner.ToCoordinates(0, 2));
-                var terminal = entityManager.SpawnEntity("CableTerminal", gridOwner.ToCoordinates(0, 2));
+                entityManager.SpawnEntity("CableTerminal", grid.Owner.ToCoordinates(0, 2));
+                var terminal = entityManager.SpawnEntity("CableTerminal", grid.Owner.ToCoordinates(0, 2));
                 entityManager.GetComponent<TransformComponent>(terminal).LocalRotation = Angle.FromDegrees(180);
 
-                var batteryEnt1 = entityManager.SpawnEntity("FullBatteryDummy", gridOwner.ToCoordinates(0, 1));
-                var batteryEnt2 = entityManager.SpawnEntity("FullBatteryDummy", gridOwner.ToCoordinates(0, 3));
-                var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", gridOwner.ToCoordinates(0, 2));
-                var consumerEnt1 = entityManager.SpawnEntity("ConsumerDummy", gridOwner.ToCoordinates(0, 0));
-                var consumerEnt2 = entityManager.SpawnEntity("ConsumerDummy", gridOwner.ToCoordinates(0, 4));
+                var batteryEnt1 = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 1));
+                var batteryEnt2 = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 3));
+                var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 2));
+                var consumerEnt1 = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 0));
+                var consumerEnt2 = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 4));
 
                 consumer1 = entityManager.GetComponent<PowerConsumerComponent>(consumerEnt1);
                 consumer2 = entityManager.GetComponent<PowerConsumerComponent>(consumerEnt2);
@@ -887,6 +885,7 @@ public async Task TestSupplyPrioritized()
             var entityManager = server.ResolveDependency<IEntityManager>();
             var gameTiming = server.ResolveDependency<IGameTiming>();
             var batterySys = entityManager.System<BatterySystem>();
+            var mapSys = entityManager.System<SharedMapSystem>();
             PowerConsumerComponent consumer = default!;
             PowerSupplierComponent supplier1 = default!;
             PowerSupplierComponent supplier2 = default!;
@@ -897,9 +896,8 @@ public async Task TestSupplyPrioritized()
 
             await server.WaitAssertion(() =>
             {
-                var map = mapManager.CreateMap();
-                var grid = mapManager.CreateGrid(map);
-                var gridOwner = grid.Owner;
+                var map = mapSys.CreateMap(out var mapId);
+                var grid = mapManager.CreateGridEntity(mapId);
 
                 // Layout is two generators, two batteries, and one load. As to why two: because previously this test
                 // would fail ONLY if there were more than two batteries present, because each of them tries to supply
@@ -911,17 +909,17 @@ await server.WaitAssertion(() =>
                 // Place cables
                 for (var i = -2; i <= 2; i++)
                 {
-                    grid.SetTile(new Vector2i(0, i), new Tile(1));
-                    entityManager.SpawnEntity("CableHV", gridOwner.ToCoordinates(0, i));
+                    mapSys.SetTile(grid, new Vector2i(0, i), new Tile(1));
+                    entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i));
                 }
 
-                var batteryEnt1 = entityManager.SpawnEntity("FullBatteryDummy", gridOwner.ToCoordinates(0, 2));
-                var batteryEnt2 = entityManager.SpawnEntity("FullBatteryDummy", gridOwner.ToCoordinates(0, -2));
+                var batteryEnt1 = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
+                var batteryEnt2 = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, -2));
 
-                var supplyEnt1 = entityManager.SpawnEntity("GeneratorDummy", gridOwner.ToCoordinates(0, 1));
-                var supplyEnt2 = entityManager.SpawnEntity("GeneratorDummy", gridOwner.ToCoordinates(0, -1));
+                var supplyEnt1 = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 1));
+                var supplyEnt2 = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, -1));
 
-                var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", gridOwner.ToCoordinates(0, 0));
+                var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 0));
 
                 consumer = entityManager.GetComponent<PowerConsumerComponent>(consumerEnt);
                 supplier1 = entityManager.GetComponent<PowerSupplierComponent>(supplyEnt1);
@@ -985,15 +983,15 @@ public async Task TestBatteriesProportional()
             var mapManager = server.ResolveDependency<IMapManager>();
             var entityManager = server.ResolveDependency<IEntityManager>();
             var batterySys = entityManager.System<BatterySystem>();
+            var mapSys = entityManager.System<SharedMapSystem>();
             PowerConsumerComponent consumer1 = default!;
             PowerConsumerComponent consumer2 = default!;
             PowerSupplierComponent supplier = default!;
 
             await server.WaitAssertion(() =>
             {
-                var map = mapManager.CreateMap();
-                var grid = mapManager.CreateGrid(map);
-                var gridOwner = grid.Owner;
+                var map = mapSys.CreateMap(out var mapId);
+                var grid = mapManager.CreateGridEntity(mapId);
 
                 // Map layout here is
                 // C - consumer
@@ -1006,19 +1004,19 @@ await server.WaitAssertion(() =>
                 // Power only works when anchored
                 for (var i = 0; i < 5; i++)
                 {
-                    grid.SetTile(new Vector2i(0, i), new Tile(1));
-                    entityManager.SpawnEntity("CableHV", gridOwner.ToCoordinates(0, i));
+                    mapSys.SetTile(grid, new Vector2i(0, i), new Tile(1));
+                    entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i));
                 }
 
-                entityManager.SpawnEntity("CableTerminal", gridOwner.ToCoordinates(0, 2));
-                var terminal = entityManager.SpawnEntity("CableTerminal", gridOwner.ToCoordinates(0, 2));
+                entityManager.SpawnEntity("CableTerminal", grid.Owner.ToCoordinates(0, 2));
+                var terminal = entityManager.SpawnEntity("CableTerminal", grid.Owner.ToCoordinates(0, 2));
                 entityManager.GetComponent<TransformComponent>(terminal).LocalRotation = Angle.FromDegrees(180);
 
-                var batteryEnt1 = entityManager.SpawnEntity("FullBatteryDummy", gridOwner.ToCoordinates(0, 1));
-                var batteryEnt2 = entityManager.SpawnEntity("FullBatteryDummy", gridOwner.ToCoordinates(0, 3));
-                var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", gridOwner.ToCoordinates(0, 2));
-                var consumerEnt1 = entityManager.SpawnEntity("ConsumerDummy", gridOwner.ToCoordinates(0, 0));
-                var consumerEnt2 = entityManager.SpawnEntity("ConsumerDummy", gridOwner.ToCoordinates(0, 4));
+                var batteryEnt1 = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 1));
+                var batteryEnt2 = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 3));
+                var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 2));
+                var consumerEnt1 = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 0));
+                var consumerEnt2 = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 4));
 
                 consumer1 = entityManager.GetComponent<PowerConsumerComponent>(consumerEnt1);
                 consumer2 = entityManager.GetComponent<PowerConsumerComponent>(consumerEnt2);
@@ -1073,29 +1071,29 @@ public async Task TestBatteryEngineCut()
             var mapManager = server.ResolveDependency<IMapManager>();
             var entityManager = server.ResolveDependency<IEntityManager>();
             var batterySys = entityManager.System<BatterySystem>();
+            var mapSys = entityManager.System<SharedMapSystem>();
             PowerConsumerComponent consumer = default!;
             PowerSupplierComponent supplier = default!;
             PowerNetworkBatteryComponent netBattery = default!;
 
             await server.WaitPost(() =>
             {
-                var map = mapManager.CreateMap();
-                var grid = mapManager.CreateGrid(map);
-                var gridOwner = grid.Owner;
+                var map = mapSys.CreateMap(out var mapId);
+                var grid = mapManager.CreateGridEntity(mapId);
 
                 // Power only works when anchored
                 for (var i = 0; i < 4; i++)
                 {
-                    grid.SetTile(new Vector2i(0, i), new Tile(1));
-                    entityManager.SpawnEntity("CableHV", gridOwner.ToCoordinates(0, i));
+                    mapSys.SetTile(grid, new Vector2i(0, i), new Tile(1));
+                    entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, i));
                 }
 
-                var terminal = entityManager.SpawnEntity("CableTerminal", gridOwner.ToCoordinates(0, 1));
+                var terminal = entityManager.SpawnEntity("CableTerminal", grid.Owner.ToCoordinates(0, 1));
                 entityManager.GetComponent<TransformComponent>(terminal).LocalRotation = Angle.FromDegrees(180);
 
-                var batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", gridOwner.ToCoordinates(0, 2));
-                var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", gridOwner.ToCoordinates(0, 0));
-                var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", gridOwner.ToCoordinates(0, 3));
+                var batteryEnt = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
+                var supplyEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
+                var consumerEnt = entityManager.SpawnEntity("ConsumerDummy", grid.Owner.ToCoordinates(0, 3));
 
                 consumer = entityManager.GetComponent<PowerConsumerComponent>(consumerEnt);
                 supplier = entityManager.GetComponent<PowerSupplierComponent>(supplyEnt);
@@ -1158,6 +1156,7 @@ public async Task TestTerminalNodeGroups()
             var mapManager = server.ResolveDependency<IMapManager>();
             var entityManager = server.ResolveDependency<IEntityManager>();
             var nodeContainer = entityManager.System<NodeContainerSystem>();
+            var mapSys = entityManager.System<SharedMapSystem>();
             CableNode leftNode = default!;
             CableNode rightNode = default!;
             Node batteryInput = default!;
@@ -1165,25 +1164,24 @@ public async Task TestTerminalNodeGroups()
 
             await server.WaitAssertion(() =>
             {
-                var map = mapManager.CreateMap();
-                var grid = mapManager.CreateGrid(map);
-                var gridOwner = grid.Owner;
+                var map = mapSys.CreateMap(out var mapId);
+                var grid = mapManager.CreateGridEntity(mapId);
 
                 // Power only works when anchored
                 for (var i = 0; i < 4; i++)
                 {
-                    grid.SetTile(new Vector2i(0, i), new Tile(1));
+                    mapSys.SetTile(grid, new Vector2i(0, i), new Tile(1));
                 }
 
-                var leftEnt = entityManager.SpawnEntity("CableHV", gridOwner.ToCoordinates(0, 0));
-                entityManager.SpawnEntity("CableHV", gridOwner.ToCoordinates(0, 1));
-                entityManager.SpawnEntity("CableHV", gridOwner.ToCoordinates(0, 2));
-                var rightEnt = entityManager.SpawnEntity("CableHV", gridOwner.ToCoordinates(0, 3));
+                var leftEnt = entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, 0));
+                entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, 1));
+                entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, 2));
+                var rightEnt = entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, 3));
 
-                var terminal = entityManager.SpawnEntity("CableTerminal", gridOwner.ToCoordinates(0, 1));
+                var terminal = entityManager.SpawnEntity("CableTerminal", grid.Owner.ToCoordinates(0, 1));
                 entityManager.GetComponent<TransformComponent>(terminal).LocalRotation = Angle.FromDegrees(180);
 
-                var battery = entityManager.SpawnEntity("FullBatteryDummy", gridOwner.ToCoordinates(0, 2));
+                var battery = entityManager.SpawnEntity("FullBatteryDummy", grid.Owner.ToCoordinates(0, 2));
                 var batteryNodeContainer = entityManager.GetComponent<NodeContainerComponent>(battery);
 
                 if (nodeContainer.TryGetNode<CableNode>(entityManager.GetComponent<NodeContainerComponent>(leftEnt),
@@ -1224,29 +1222,29 @@ public async Task ApcChargingTest()
             var mapManager = server.ResolveDependency<IMapManager>();
             var entityManager = server.ResolveDependency<IEntityManager>();
             var batterySys = entityManager.System<BatterySystem>();
+            var mapSys = entityManager.System<SharedMapSystem>();
             PowerNetworkBatteryComponent substationNetBattery = default!;
             BatteryComponent apcBattery = default!;
 
             await server.WaitAssertion(() =>
             {
-                var map = mapManager.CreateMap();
-                var grid = mapManager.CreateGrid(map);
-                var gridOwner = grid.Owner;
+                var map = mapSys.CreateMap(out var mapId);
+                var grid = mapManager.CreateGridEntity(mapId);
 
                 // Power only works when anchored
                 for (var i = 0; i < 3; i++)
                 {
-                    grid.SetTile(new Vector2i(0, i), new Tile(1));
+                    mapSys.SetTile(grid, new Vector2i(0, i), new Tile(1));
                 }
 
-                entityManager.SpawnEntity("CableHV", gridOwner.ToCoordinates(0, 0));
-                entityManager.SpawnEntity("CableHV", gridOwner.ToCoordinates(0, 1));
-                entityManager.SpawnEntity("CableMV", gridOwner.ToCoordinates(0, 1));
-                entityManager.SpawnEntity("CableMV", gridOwner.ToCoordinates(0, 2));
+                entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, 0));
+                entityManager.SpawnEntity("CableHV", grid.Owner.ToCoordinates(0, 1));
+                entityManager.SpawnEntity("CableMV", grid.Owner.ToCoordinates(0, 1));
+                entityManager.SpawnEntity("CableMV", grid.Owner.ToCoordinates(0, 2));
 
-                var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", gridOwner.ToCoordinates(0, 0));
-                var substationEnt = entityManager.SpawnEntity("SubstationDummy", gridOwner.ToCoordinates(0, 1));
-                var apcEnt = entityManager.SpawnEntity("ApcDummy", gridOwner.ToCoordinates(0, 2));
+                var generatorEnt = entityManager.SpawnEntity("GeneratorDummy", grid.Owner.ToCoordinates(0, 0));
+                var substationEnt = entityManager.SpawnEntity("SubstationDummy", grid.Owner.ToCoordinates(0, 1));
+                var apcEnt = entityManager.SpawnEntity("ApcDummy", grid.Owner.ToCoordinates(0, 2));
 
                 var generatorSupplier = entityManager.GetComponent<PowerSupplierComponent>(generatorEnt);
                 substationNetBattery = entityManager.GetComponent<PowerNetworkBatteryComponent>(substationEnt);
@@ -1281,33 +1279,33 @@ public async Task ApcNetTest()
             var entityManager = server.ResolveDependency<IEntityManager>();
             var batterySys = entityManager.System<BatterySystem>();
             var extensionCableSystem = entityManager.System<ExtensionCableSystem>();
+            var mapSys = entityManager.System<SharedMapSystem>();
             PowerNetworkBatteryComponent apcNetBattery = default!;
             ApcPowerReceiverComponent receiver = default!;
             ApcPowerReceiverComponent unpoweredReceiver = default!;
 
             await server.WaitAssertion(() =>
             {
-                var map = mapManager.CreateMap();
-                var grid = mapManager.CreateGrid(map);
-                var gridOwner = grid.Owner;
+                var map = mapSys.CreateMap(out var mapId);
+                var grid = mapManager.CreateGridEntity(mapId);
 
                 const int range = 5;
 
                 // Power only works when anchored
                 for (var i = 0; i < range; i++)
                 {
-                    grid.SetTile(new Vector2i(0, i), new Tile(1));
+                    mapSys.SetTile(grid, new Vector2i(0, i), new Tile(1));
                 }
 
-                var apcEnt = entityManager.SpawnEntity("ApcDummy", gridOwner.ToCoordinates(0, 0));
-                var apcExtensionEnt = entityManager.SpawnEntity("CableApcExtension", gridOwner.ToCoordinates(0, 0));
+                var apcEnt = entityManager.SpawnEntity("ApcDummy", grid.Owner.ToCoordinates(0, 0));
+                var apcExtensionEnt = entityManager.SpawnEntity("CableApcExtension", grid.Owner.ToCoordinates(0, 0));
 
                 // Create a powered receiver in range (range is 0 indexed)
-                var powerReceiverEnt = entityManager.SpawnEntity("ApcPowerReceiverDummy", gridOwner.ToCoordinates(0, range - 1));
+                var powerReceiverEnt = entityManager.SpawnEntity("ApcPowerReceiverDummy", grid.Owner.ToCoordinates(0, range - 1));
                 receiver = entityManager.GetComponent<ApcPowerReceiverComponent>(powerReceiverEnt);
 
                 // Create an unpowered receiver outside range
-                var unpoweredReceiverEnt = entityManager.SpawnEntity("ApcPowerReceiverDummy", gridOwner.ToCoordinates(0, range));
+                var unpoweredReceiverEnt = entityManager.SpawnEntity("ApcPowerReceiverDummy", grid.Owner.ToCoordinates(0, range));
                 unpoweredReceiver = entityManager.GetComponent<ApcPowerReceiverComponent>(unpoweredReceiverEnt);
 
                 var battery = entityManager.GetComponent<BatteryComponent>(apcEnt);
diff --git a/Content.IntegrationTests/Tests/PrototypeSaveTest.cs b/Content.IntegrationTests/Tests/PrototypeSaveTest.cs
index 9e26fa5eaa2..1ef34365ea3 100644
--- a/Content.IntegrationTests/Tests/PrototypeSaveTest.cs
+++ b/Content.IntegrationTests/Tests/PrototypeSaveTest.cs
@@ -40,6 +40,7 @@ public async Task UninitializedSaveTest()
         var prototypeMan = server.ResolveDependency<IPrototypeManager>();
         var seriMan = server.ResolveDependency<ISerializationManager>();
         var compFact = server.ResolveDependency<IComponentFactory>();
+        var mapSystem = server.System<SharedMapSystem>();
 
         var prototypes = new List<EntityPrototype>();
         EntityUid uid;
@@ -77,7 +78,7 @@ public async Task UninitializedSaveTest()
 
         await server.WaitAssertion(() =>
         {
-            Assert.That(!mapManager.IsMapInitialized(mapId));
+            Assert.That(!mapSystem.IsInitialized(mapId));
             var testLocation = grid.Owner.ToCoordinates();
 
             Assert.Multiple(() =>
@@ -184,7 +185,7 @@ public DataNode Write(ISerializationManager serializationManager, EntityUid valu
             IDependencyCollection dependencies, bool alwaysWrite = false,
             ISerializationContext? context = null)
         {
-            if (WritingComponent != "Transform" && (Prototype?.NoSpawn == false))
+            if (WritingComponent != "Transform" && Prototype?.HideSpawnMenu == false)
             {
                 // Maybe this will be necessary in the future, but at the moment it just indicates that there is some
                 // issue, like a non-nullable entityUid data-field. If a component MUST have an entity uid to work with,
diff --git a/Content.IntegrationTests/Tests/Puller/PullerTest.cs b/Content.IntegrationTests/Tests/Puller/PullerTest.cs
index 87d174f7272..a4fde86dbfb 100644
--- a/Content.IntegrationTests/Tests/Puller/PullerTest.cs
+++ b/Content.IntegrationTests/Tests/Puller/PullerTest.cs
@@ -29,7 +29,7 @@ await server.WaitAssertion(() =>
             {
                 foreach (var proto in protoManager.EnumeratePrototypes<EntityPrototype>())
                 {
-                    if (!proto.TryGetComponent(out PullerComponent? puller))
+                    if (!proto.TryGetComponent(out PullerComponent? puller, compFactory))
                         continue;
 
                     if (!puller.NeedsHands)
diff --git a/Content.IntegrationTests/Tests/ResearchTest.cs b/Content.IntegrationTests/Tests/ResearchTest.cs
index ee319daa436..7ae29a79ffd 100644
--- a/Content.IntegrationTests/Tests/ResearchTest.cs
+++ b/Content.IntegrationTests/Tests/ResearchTest.cs
@@ -2,6 +2,7 @@
 using System.Linq;
 using Content.Shared.Lathe;
 using Content.Shared.Research.Prototypes;
+using Robust.Shared.GameObjects;
 using Robust.Shared.Prototypes;
 
 namespace Content.IntegrationTests.Tests;
@@ -52,6 +53,7 @@ public async Task AllTechPrintableTest()
         var server = pair.Server;
 
         var protoManager = server.ResolveDependency<IPrototypeManager>();
+        var compFact = server.ResolveDependency<IComponentFactory>();
 
         await server.WaitAssertion(() =>
         {
@@ -65,7 +67,7 @@ await server.WaitAssertion(() =>
                 if (pair.IsTestPrototype(proto))
                     continue;
 
-                if (!proto.TryGetComponent<LatheComponent>(out var lathe))
+                if (!proto.TryGetComponent<LatheComponent>(out var lathe, compFact))
                     continue;
                 allLathes.Add(lathe);
             }
diff --git a/Content.IntegrationTests/Tests/SalvageTest.cs b/Content.IntegrationTests/Tests/SalvageTest.cs
index 9d75428beb7..5dfba82308f 100644
--- a/Content.IntegrationTests/Tests/SalvageTest.cs
+++ b/Content.IntegrationTests/Tests/SalvageTest.cs
@@ -1,5 +1,4 @@
 using System.Linq;
-using Content.Server.Salvage;
 using Content.Shared.CCVar;
 using Content.Shared.Salvage;
 using Robust.Server.GameObjects;
@@ -28,6 +27,7 @@ public async Task AllSalvageMapsLoadableTest()
         var mapManager = server.ResolveDependency<IMapManager>();
         var prototypeManager = server.ResolveDependency<IPrototypeManager>();
         var cfg = server.ResolveDependency<IConfigurationManager>();
+        var mapSystem = entManager.System<SharedMapSystem>();
         Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False);
 
         await server.WaitPost(() =>
@@ -36,7 +36,7 @@ await server.WaitPost(() =>
             {
                 var mapFile = salvage.MapPath;
 
-                var mapId = mapManager.CreateMap();
+                mapSystem.CreateMap(out var mapId);
                 try
                 {
                     Assert.That(mapLoader.TryLoad(mapId, mapFile.ToString(), out var roots));
diff --git a/Content.IntegrationTests/Tests/SaveLoadMapTest.cs b/Content.IntegrationTests/Tests/SaveLoadMapTest.cs
index db2109ca599..213da5d7862 100644
--- a/Content.IntegrationTests/Tests/SaveLoadMapTest.cs
+++ b/Content.IntegrationTests/Tests/SaveLoadMapTest.cs
@@ -23,6 +23,7 @@ public async Task SaveLoadMultiGridMap()
             var mapManager = server.ResolveDependency<IMapManager>();
             var sEntities = server.ResolveDependency<IEntityManager>();
             var mapLoader = sEntities.System<MapLoaderSystem>();
+            var mapSystem = sEntities.System<SharedMapSystem>();
             var xformSystem = sEntities.EntitySysManager.GetEntitySystem<SharedTransformSystem>();
             var resManager = server.ResolveDependency<IResourceManager>();
             var cfg = server.ResolveDependency<IConfigurationManager>();
@@ -33,19 +34,17 @@ await server.WaitAssertion(() =>
                 var dir = new ResPath(mapPath).Directory;
                 resManager.UserData.CreateDir(dir);
 
-                var mapId = mapManager.CreateMap();
+                mapSystem.CreateMap(out var mapId);
 
                 {
-                    var mapGrid = mapManager.CreateGrid(mapId);
-                    var mapGridEnt = mapGrid.Owner;
-                    xformSystem.SetWorldPosition(mapGridEnt, new Vector2(10, 10));
-                    mapGrid.SetTile(new Vector2i(0, 0), new Tile(1, (TileRenderFlag) 1, 255));
+                    var mapGrid = mapManager.CreateGridEntity(mapId);
+                    xformSystem.SetWorldPosition(mapGrid, new Vector2(10, 10));
+                    mapSystem.SetTile(mapGrid, new Vector2i(0, 0), new Tile(1, (TileRenderFlag) 1, 255));
                 }
                 {
-                    var mapGrid = mapManager.CreateGrid(mapId);
-                    var mapGridEnt = mapGrid.Owner;
-                    xformSystem.SetWorldPosition(mapGridEnt, new Vector2(-8, -8));
-                    mapGrid.SetTile(new Vector2i(0, 0), new Tile(2, (TileRenderFlag) 1, 254));
+                    var mapGrid = mapManager.CreateGridEntity(mapId);
+                    xformSystem.SetWorldPosition(mapGrid, new Vector2(-8, -8));
+                    mapSystem.SetTile(mapGrid, new Vector2i(0, 0), new Tile(2, (TileRenderFlag) 1, 254));
                 }
 
                 Assert.Multiple(() => mapLoader.SaveMap(mapId, mapPath));
@@ -74,7 +73,7 @@ await server.WaitAssertion(() =>
                     Assert.Multiple(() =>
                     {
                         Assert.That(xformSystem.GetWorldPosition(gridXform), Is.EqualTo(new Vector2(10, 10)));
-                        Assert.That(mapGrid.GetTileRef(new Vector2i(0, 0)).Tile, Is.EqualTo(new Tile(1, (TileRenderFlag) 1, 255)));
+                        Assert.That(mapSystem.GetTileRef(gridUid, mapGrid, new Vector2i(0, 0)).Tile, Is.EqualTo(new Tile(1, (TileRenderFlag) 1, 255)));
                     });
                 }
                 {
@@ -88,7 +87,7 @@ await server.WaitAssertion(() =>
                     Assert.Multiple(() =>
                     {
                         Assert.That(xformSystem.GetWorldPosition(gridXform), Is.EqualTo(new Vector2(-8, -8)));
-                        Assert.That(mapGrid.GetTileRef(new Vector2i(0, 0)).Tile, Is.EqualTo(new Tile(2, (TileRenderFlag) 1, 254)));
+                        Assert.That(mapSystem.GetTileRef(gridUid, mapGrid, new Vector2i(0, 0)).Tile, Is.EqualTo(new Tile(2, (TileRenderFlag) 1, 254)));
                     });
                 }
             });
diff --git a/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs b/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs
index 01c03aace71..af60db55322 100644
--- a/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs
+++ b/Content.IntegrationTests/Tests/SaveLoadSaveTest.cs
@@ -25,17 +25,18 @@ public async Task SaveLoadSave()
             var server = pair.Server;
             var entManager = server.ResolveDependency<IEntityManager>();
             var mapLoader = entManager.System<MapLoaderSystem>();
+            var mapSystem = entManager.System<SharedMapSystem>();
             var mapManager = server.ResolveDependency<IMapManager>();
             var cfg = server.ResolveDependency<IConfigurationManager>();
             Assert.That(cfg.GetCVar(CCVars.GridFill), Is.False);
 
             await server.WaitPost(() =>
             {
-                var mapId0 = mapManager.CreateMap();
+                mapSystem.CreateMap(out var mapId0);
                 // TODO: Properly find the "main" station grid.
-                var grid0 = mapManager.CreateGrid(mapId0);
+                var grid0 = mapManager.CreateGridEntity(mapId0);
                 mapLoader.Save(grid0.Owner, "save load save 1.yml");
-                var mapId1 = mapManager.CreateMap();
+                mapSystem.CreateMap(out var mapId1);
                 EntityUid grid1 = default!;
 #pragma warning disable NUnit2045
                 Assert.That(mapLoader.TryLoad(mapId1, "save load save 1.yml", out var roots, new MapLoadOptions() { LoadMap = false }), $"Failed to load test map {TestMap}");
@@ -101,6 +102,7 @@ public async Task LoadSaveTicksSavePebble()
             var server = pair.Server;
             var mapLoader = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<MapLoaderSystem>();
             var mapManager = server.ResolveDependency<IMapManager>();
+            var mapSystem = server.System<SharedMapSystem>();
 
             MapId mapId = default;
             var cfg = server.ResolveDependency<IConfigurationManager>();
@@ -109,8 +111,7 @@ public async Task LoadSaveTicksSavePebble()
             // Load pebble.yml as uninitialized map, and save it to ensure it's up to date.
             server.Post(() =>
             {
-                mapId = mapManager.CreateMap();
-                mapManager.AddUninitializedMap(mapId);
+                mapSystem.CreateMap(out mapId, runMapInit: false);
                 mapManager.SetMapPaused(mapId, true);
                 Assert.That(mapLoader.TryLoad(mapId, TestMap, out _), $"Failed to load test map {TestMap}");
                 mapLoader.SaveMap(mapId, "load save ticks save 1.yml");
@@ -182,7 +183,8 @@ public async Task LoadTickLoadPebble()
             await using var pair = await PoolManager.GetServerClient();
             var server = pair.Server;
 
-            var mapLoader = server.ResolveDependency<IEntitySystemManager>().GetEntitySystem<MapLoaderSystem>();
+            var mapLoader = server.System<MapLoaderSystem>();
+            var mapSystem = server.System<SharedMapSystem>();
             var mapManager = server.ResolveDependency<IMapManager>();
             var userData = server.ResolveDependency<IResourceManager>().UserData;
             var cfg = server.ResolveDependency<IConfigurationManager>();
@@ -197,8 +199,7 @@ public async Task LoadTickLoadPebble()
             // Load & save the first map
             server.Post(() =>
             {
-                mapId = mapManager.CreateMap();
-                mapManager.AddUninitializedMap(mapId);
+                mapSystem.CreateMap(out mapId, runMapInit: false);
                 mapManager.SetMapPaused(mapId, true);
                 Assert.That(mapLoader.TryLoad(mapId, TestMap, out _), $"Failed to load test map {TestMap}");
                 mapLoader.SaveMap(mapId, fileA);
@@ -217,8 +218,7 @@ public async Task LoadTickLoadPebble()
             server.Post(() =>
             {
                 mapManager.DeleteMap(mapId);
-                mapManager.CreateMap(mapId);
-                mapManager.AddUninitializedMap(mapId);
+                mapSystem.CreateMap(out mapId, runMapInit: false);
                 mapManager.SetMapPaused(mapId, true);
                 Assert.That(mapLoader.TryLoad(mapId, TestMap, out _), $"Failed to load test map {TestMap}");
                 mapLoader.SaveMap(mapId, fileB);
diff --git a/Content.IntegrationTests/Tests/Serialization/SerializationTest.cs b/Content.IntegrationTests/Tests/Serialization/SerializationTest.cs
index 052ea997c0d..339420362c1 100644
--- a/Content.IntegrationTests/Tests/Serialization/SerializationTest.cs
+++ b/Content.IntegrationTests/Tests/Serialization/SerializationTest.cs
@@ -24,7 +24,7 @@ public async Task SerializeGenericEnums()
 
         Enum value = TestEnum.Bb;
 
-        var node = seriMan.WriteValue(value, notNullableOverride:true);
+        var node = seriMan.WriteValue(value, notNullableOverride: true);
         var valueNode = node as ValueDataNode;
         Assert.That(valueNode, Is.Not.Null);
 
@@ -34,22 +34,22 @@ public async Task SerializeGenericEnums()
         var errors = seriMan.ValidateNode<Enum>(valueNode).GetErrors();
         Assert.That(errors.Any(), Is.False);
 
-        var deserialized = seriMan.Read<Enum>(node, notNullableOverride:true);
+        var deserialized = seriMan.Read<Enum>(node, notNullableOverride: true);
         Assert.That(deserialized, Is.EqualTo(value));
 
         // Repeat test with enums in a data definitions.
         var data = new TestData
         {
             Value = TestEnum.Cc,
-            Sequence = new() {TestEnum.Dd, TestEnum.Aa}
+            Sequence = [TestEnum.Dd, TestEnum.Aa]
         };
 
-        node = seriMan.WriteValue(data, notNullableOverride:true);
+        node = seriMan.WriteValue(data, notNullableOverride: true);
 
         errors = seriMan.ValidateNode<TestData>(node).GetErrors();
         Assert.That(errors.Any(), Is.False);
 
-        var deserializedData = seriMan.Read<TestData>(node, notNullableOverride:false);
+        var deserializedData = seriMan.Read<TestData>(node, notNullableOverride: false);
 
         Assert.That(deserializedData.Value, Is.EqualTo(data.Value));
         Assert.That(deserializedData.Sequence.Count, Is.EqualTo(data.Sequence.Count));
@@ -60,7 +60,7 @@ public async Task SerializeGenericEnums()
         Enum genericValue = TestEnum.Bb;
         TestEnum typedValue = TestEnum.Bb;
 
-        var genericNode = seriMan.WriteValue(genericValue, notNullableOverride:true);
+        var genericNode = seriMan.WriteValue(genericValue, notNullableOverride: true);
         var typedNode = seriMan.WriteValue(typedValue);
 
         Assert.That(seriMan.ValidateNode<Enum>(genericNode).GetErrors().Any(), Is.False);
@@ -76,7 +76,7 @@ private enum TestEnum : byte { Aa, Bb, Cc, Dd }
     [DataDefinition]
     private sealed partial class TestData
     {
-        [DataField("value")] public Enum Value = default!;
-        [DataField("sequence")] public List<Enum> Sequence = default!;
+        [DataField] public Enum Value = default!;
+        [DataField] public List<Enum> Sequence = default!;
     }
 }
diff --git a/Content.IntegrationTests/Tests/Shuttle/DockTest.cs b/Content.IntegrationTests/Tests/Shuttle/DockTest.cs
index a1aa462a697..d91d18793e5 100644
--- a/Content.IntegrationTests/Tests/Shuttle/DockTest.cs
+++ b/Content.IntegrationTests/Tests/Shuttle/DockTest.cs
@@ -97,13 +97,14 @@ public async Task TestPlanetDock()
         var entManager = server.ResolveDependency<IEntityManager>();
         var dockingSystem = entManager.System<DockingSystem>();
         var mapSystem = entManager.System<SharedMapSystem>();
+        MapGridComponent mapGrid = default!;
 
-        var mapGrid = entManager.AddComponent<MapGridComponent>(map.MapUid);
         var shuttle = EntityUid.Invalid;
 
         // Spawn shuttle and affirm no valid docks.
         await server.WaitAssertion(() =>
         {
+            mapGrid = entManager.AddComponent<MapGridComponent>(map.MapUid);
             entManager.DeleteEntity(map.Grid);
             Assert.That(entManager.System<MapLoaderSystem>().TryLoad(otherMap.MapId, "/Maps/Shuttles/emergency.yml", out var rootUids));
             shuttle = rootUids[0];
@@ -125,4 +126,4 @@ await server.WaitAssertion(() =>
 
         await pair.CleanReturnAsync();
     }
-}
+}
\ No newline at end of file
diff --git a/Content.IntegrationTests/Tests/ShuttleTest.cs b/Content.IntegrationTests/Tests/ShuttleTest.cs
index fb786373a5a..da5b82d91e7 100644
--- a/Content.IntegrationTests/Tests/ShuttleTest.cs
+++ b/Content.IntegrationTests/Tests/ShuttleTest.cs
@@ -2,7 +2,6 @@
 using Content.Server.Shuttles.Components;
 using Robust.Shared.GameObjects;
 using Robust.Shared.Map;
-using Robust.Shared.Maths;
 using Robust.Shared.Physics;
 using Robust.Shared.Physics.Components;
 using Robust.Shared.Physics.Systems;
@@ -23,33 +22,33 @@ public async Task Test()
             var entManager = server.ResolveDependency<IEntityManager>();
             var physicsSystem = entManager.System<SharedPhysicsSystem>();
 
-            EntityUid gridEnt = default;
             PhysicsComponent gridPhys = null;
 
+            var map = await pair.CreateTestMap();
+
             await server.WaitAssertion(() =>
             {
-                var mapId = mapMan.CreateMap();
-                var grid = mapMan.CreateGridEntity(mapId);
-                gridEnt = grid.Owner;
+                var mapId = map.MapId;
+                var grid = map.Grid;
 
                 Assert.Multiple(() =>
                 {
-                    Assert.That(entManager.HasComponent<ShuttleComponent>(gridEnt));
-                    Assert.That(entManager.TryGetComponent(gridEnt, out gridPhys));
+                    Assert.That(entManager.HasComponent<ShuttleComponent>(grid));
+                    Assert.That(entManager.TryGetComponent(grid, out gridPhys));
                 });
                 Assert.Multiple(() =>
                 {
                     Assert.That(gridPhys.BodyType, Is.EqualTo(BodyType.Dynamic));
-                    Assert.That(entManager.GetComponent<TransformComponent>(gridEnt).LocalPosition, Is.EqualTo(Vector2.Zero));
+                    Assert.That(entManager.GetComponent<TransformComponent>(grid).LocalPosition, Is.EqualTo(Vector2.Zero));
                 });
-                physicsSystem.ApplyLinearImpulse(gridEnt, Vector2.One, body: gridPhys);
+                physicsSystem.ApplyLinearImpulse(grid, Vector2.One, body: gridPhys);
             });
 
             await server.WaitRunTicks(1);
 
             await server.WaitAssertion(() =>
             {
-                Assert.That(entManager.GetComponent<TransformComponent>(gridEnt).LocalPosition, Is.Not.EqualTo(Vector2.Zero));
+                Assert.That(entManager.GetComponent<TransformComponent>(map.Grid).LocalPosition, Is.Not.EqualTo(Vector2.Zero));
             });
             await pair.CleanReturnAsync();
         }
diff --git a/Content.IntegrationTests/Tests/Sprite/ItemSpriteTest.cs b/Content.IntegrationTests/Tests/Sprite/ItemSpriteTest.cs
index 1762c4213c4..bf75188f029 100644
--- a/Content.IntegrationTests/Tests/Sprite/ItemSpriteTest.cs
+++ b/Content.IntegrationTests/Tests/Sprite/ItemSpriteTest.cs
@@ -19,12 +19,12 @@ namespace Content.IntegrationTests.Tests.Sprite;
 /// - Shouldn't have an item component
 /// - Is missing the required sprite information.
 /// If none of the abveo are true, it might need to be added to the list of ignored components, see
-/// <see cref="_ignored"/>
+/// <see cref="Ignored"/>
 /// </remarks>
 [TestFixture]
 public sealed class PrototypeSaveTest
 {
-    private static HashSet<string> _ignored = new()
+    private static readonly HashSet<string> Ignored = new()
     {
         // The only prototypes that should get ignored are those that REQUIRE setup to get a sprite. At that point it is
         // the responsibility of the spawner to ensure that a valid sprite is set.
@@ -34,13 +34,13 @@ public sealed class PrototypeSaveTest
     [Test]
     public async Task AllItemsHaveSpritesTest()
     {
-        var settings = new PoolSettings() {Connected = true}; // client needs to be in-game
+        var settings = new PoolSettings() { Connected = true }; // client needs to be in-game
         await using var pair = await PoolManager.GetServerClient(settings);
-        List<EntityPrototype> badPrototypes = new();
+        List<EntityPrototype> badPrototypes = [];
 
         await pair.Client.WaitPost(() =>
         {
-            foreach (var proto in pair.GetPrototypesWithComponent<ItemComponent>(_ignored))
+            foreach (var proto in pair.GetPrototypesWithComponent<ItemComponent>(Ignored))
             {
                 var dummy = pair.Client.EntMan.Spawn(proto.ID);
                 pair.Client.EntMan.RunMapInit(dummy, pair.Client.MetaData(dummy));
diff --git a/Content.IntegrationTests/Tests/Minds/JobTests.cs b/Content.IntegrationTests/Tests/Station/JobTests.cs
similarity index 100%
rename from Content.IntegrationTests/Tests/Minds/JobTests.cs
rename to Content.IntegrationTests/Tests/Station/JobTests.cs
diff --git a/Content.IntegrationTests/Tests/Storage/StorageInteractionTest.cs b/Content.IntegrationTests/Tests/Storage/StorageInteractionTest.cs
new file mode 100644
index 00000000000..34402dd5e62
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Storage/StorageInteractionTest.cs
@@ -0,0 +1,75 @@
+using Content.Client.UserInterface.Systems.Hotbar.Widgets;
+using Content.Client.UserInterface.Systems.Storage.Controls;
+using Content.IntegrationTests.Tests.Interaction;
+using Content.Shared.Input;
+using Content.Shared.PDA;
+using Content.Shared.Storage;
+using Robust.Client.UserInterface;
+using Robust.Shared.Containers;
+using Robust.Shared.GameObjects;
+
+namespace Content.IntegrationTests.Tests.Storage;
+
+public sealed class StorageInteractionTest : InteractionTest
+{
+    /// <summary>
+    /// Check that players can interact with items in storage if the storage UI is open
+    /// </summary>
+    [Test]
+    public async Task UiInteractTest()
+    {
+        var sys = Server.System<SharedContainerSystem>();
+
+        await SpawnTarget("ClothingBackpack");
+        var backpack = ToServer(Target);
+
+        // Initially no BUI is open.
+        Assert.That(IsUiOpen(StorageComponent.StorageUiKey.Key), Is.False);
+        Assert.That(IsUiOpen(PdaUiKey.Key), Is.False);
+
+        // Activating the backpack opens the UI
+        await Activate();
+        Assert.That(IsUiOpen(StorageComponent.StorageUiKey.Key), Is.True);
+        Assert.That(IsUiOpen(PdaUiKey.Key), Is.False);
+
+        // Pick up a PDA
+        var pda = await PlaceInHands("PassengerPDA");
+        var sPda = ToServer(pda);
+        Assert.That(sys.IsEntityInContainer(sPda), Is.True);
+        Assert.That(sys.TryGetContainingContainer((sPda, null), out var container));
+        Assert.That(container!.Owner, Is.EqualTo(SPlayer));
+
+        // Insert the PDA into the backpack
+        await Interact();
+        Assert.That(sys.TryGetContainingContainer((sPda, null), out container));
+        Assert.That(container!.Owner, Is.EqualTo(backpack));
+
+        // Use "e" / ActivateInWorld to open the PDA UI while it is still in the backpack.
+        var ctrl = GetStorageControl(pda);
+        await ClickControl(ctrl, ContentKeyFunctions.ActivateItemInWorld);
+        await RunTicks(10);
+        Assert.That(IsUiOpen(StorageComponent.StorageUiKey.Key), Is.True);
+        Assert.That(IsUiOpen(PdaUiKey.Key), Is.True);
+
+        // Click on the pda to pick it up and remove it from the backpack.
+        await ClickControl(ctrl, ContentKeyFunctions.MoveStoredItem);
+        await RunTicks(10);
+        Assert.That(sys.TryGetContainingContainer((sPda, null), out container));
+        Assert.That(container!.Owner, Is.EqualTo(SPlayer));
+
+        // UIs should still be open
+        Assert.That(IsUiOpen(StorageComponent.StorageUiKey.Key), Is.True);
+        Assert.That(IsUiOpen(PdaUiKey.Key), Is.True);
+    }
+
+    /// <summary>
+    /// Retrieve the control that corresponds to the given entity in the currently open storage UI.
+    /// </summary>
+    private ItemGridPiece GetStorageControl(NetEntity target)
+    {
+        var uid = ToClient(target);
+        var hotbar = GetWidget<HotbarGui>();
+        var storageContainer  = GetControlFromField<Control>(nameof(HotbarGui.StorageContainer), hotbar);
+        return GetControlFromChildren<ItemGridPiece>(c => c.Entity == uid, storageContainer);
+    }
+}
diff --git a/Content.IntegrationTests/Tests/Tag/TagTest.cs b/Content.IntegrationTests/Tests/Tag/TagTest.cs
index ed3c484b435..cbcdd1c6c62 100644
--- a/Content.IntegrationTests/Tests/Tag/TagTest.cs
+++ b/Content.IntegrationTests/Tests/Tag/TagTest.cs
@@ -130,9 +130,9 @@ await server.WaitAssertion(() =>
                 Assert.Multiple(() =>
                 {
                     // Cannot add the starting tag again
-                    Assert.That(tagSystem.AddTag(sTagComponent, StartingTag), Is.False);
-                    Assert.That(tagSystem.AddTags(sTagComponent, StartingTag, StartingTag), Is.False);
-                    Assert.That(tagSystem.AddTags(sTagComponent, new List<string> { StartingTag, StartingTag }), Is.False);
+                    Assert.That(tagSystem.AddTag(sTagDummy, sTagComponent, StartingTag), Is.False);
+                    Assert.That(tagSystem.AddTags(sTagDummy, sTagComponent, StartingTag, StartingTag), Is.False);
+                    Assert.That(tagSystem.AddTags(sTagDummy, sTagComponent, new List<string> { StartingTag, StartingTag }), Is.False);
 
                     // Has the starting tag
                     Assert.That(tagSystem.HasTag(sTagComponent, StartingTag), Is.True);
@@ -157,22 +157,22 @@ await server.WaitAssertion(() =>
                     Assert.That(tagSystem.HasAllTags(sTagComponent, new List<string> { StartingTag, AddedTag }), Is.False);
 
                     // Cannot remove a tag that does not exist
-                    Assert.That(tagSystem.RemoveTag(sTagComponent, AddedTag), Is.False);
-                    Assert.That(tagSystem.RemoveTags(sTagComponent, AddedTag, AddedTag), Is.False);
-                    Assert.That(tagSystem.RemoveTags(sTagComponent, new List<string> { AddedTag, AddedTag }), Is.False);
+                    Assert.That(tagSystem.RemoveTag(sTagDummy, sTagComponent, AddedTag), Is.False);
+                    Assert.That(tagSystem.RemoveTags(sTagDummy, sTagComponent, AddedTag, AddedTag), Is.False);
+                    Assert.That(tagSystem.RemoveTags(sTagDummy, sTagComponent, new List<string> { AddedTag, AddedTag }), Is.False);
                 });
 
                 // Can add the new tag
-                Assert.That(tagSystem.AddTag(sTagComponent, AddedTag), Is.True);
+                Assert.That(tagSystem.AddTag(sTagDummy, sTagComponent, AddedTag), Is.True);
 
                 Assert.Multiple(() =>
                 {
                     // Cannot add it twice
-                    Assert.That(tagSystem.AddTag(sTagComponent, AddedTag), Is.False);
+                    Assert.That(tagSystem.AddTag(sTagDummy, sTagComponent, AddedTag), Is.False);
 
                     // Cannot add existing tags
-                    Assert.That(tagSystem.AddTags(sTagComponent, StartingTag, AddedTag), Is.False);
-                    Assert.That(tagSystem.AddTags(sTagComponent, new List<string> { StartingTag, AddedTag }), Is.False);
+                    Assert.That(tagSystem.AddTags(sTagDummy, sTagComponent, StartingTag, AddedTag), Is.False);
+                    Assert.That(tagSystem.AddTags(sTagDummy, sTagComponent, new List<string> { StartingTag, AddedTag }), Is.False);
 
                     // Now has two tags
                     Assert.That(sTagComponent.Tags, Has.Count.EqualTo(2));
@@ -191,16 +191,16 @@ await server.WaitAssertion(() =>
                 Assert.Multiple(() =>
                 {
                     // Remove the existing starting tag
-                    Assert.That(tagSystem.RemoveTag(sTagComponent, StartingTag), Is.True);
+                    Assert.That(tagSystem.RemoveTag(sTagDummy, sTagComponent, StartingTag), Is.True);
 
                     // Remove the existing added tag
-                    Assert.That(tagSystem.RemoveTags(sTagComponent, AddedTag, AddedTag), Is.True);
+                    Assert.That(tagSystem.RemoveTags(sTagDummy, sTagComponent, AddedTag, AddedTag), Is.True);
                 });
 
                 Assert.Multiple(() =>
                 {
                     // No tags left to remove
-                    Assert.That(tagSystem.RemoveTags(sTagComponent, new List<string> { StartingTag, AddedTag }), Is.False);
+                    Assert.That(tagSystem.RemoveTags(sTagDummy, sTagComponent, new List<string> { StartingTag, AddedTag }), Is.False);
 
                     // No tags left in the component
                     Assert.That(sTagComponent.Tags, Is.Empty);
diff --git a/Content.IntegrationTests/Tests/Tiles/TileConstructionTests.cs b/Content.IntegrationTests/Tests/Tiles/TileConstructionTests.cs
index 083e817d697..6ea8b6882ad 100644
--- a/Content.IntegrationTests/Tests/Tiles/TileConstructionTests.cs
+++ b/Content.IntegrationTests/Tests/Tiles/TileConstructionTests.cs
@@ -15,10 +15,10 @@ public async Task PlaceThenCutLattice()
         await AssertTile(Plating, PlayerCoords);
         AssertGridCount(1);
         await SetTile(null);
-        await Interact(Rod);
+        await InteractUsing(Rod);
         await AssertTile(Lattice);
         Assert.That(Hands.ActiveHandEntity, Is.Null);
-        await Interact(Cut);
+        await InteractUsing(Cut);
         await AssertTile(null);
         await AssertEntityLookup((Rod, 1));
         AssertGridCount(1);
@@ -43,14 +43,14 @@ public async Task CutThenPlaceLatticeNewGrid()
         // Place Lattice
         var oldPos = TargetCoords;
         TargetCoords = SEntMan.GetNetCoordinates(new EntityCoordinates(MapData.MapUid, 1, 0));
-        await Interact(Rod);
+        await InteractUsing(Rod);
         TargetCoords = oldPos;
         await AssertTile(Lattice);
         AssertGridCount(1);
 
         // Cut lattice
         Assert.That(Hands.ActiveHandEntity, Is.Null);
-        await Interact(Cut);
+        await InteractUsing(Cut);
         await AssertTile(null);
         AssertGridCount(0);
 
@@ -76,25 +76,25 @@ public async Task FloorConstructDeconstruct()
         // Space -> Lattice
         var oldPos = TargetCoords;
         TargetCoords = SEntMan.GetNetCoordinates(new EntityCoordinates(MapData.MapUid, 1, 0));
-        await Interact(Rod);
+        await InteractUsing(Rod);
         TargetCoords = oldPos;
         await AssertTile(Lattice);
         AssertGridCount(1);
 
         // Lattice -> Plating
-        await Interact(Steel);
+        await InteractUsing(Steel);
         Assert.That(Hands.ActiveHandEntity, Is.Null);
         await AssertTile(Plating);
         AssertGridCount(1);
 
         // Plating -> Tile
-        await Interact(FloorItem);
+        await InteractUsing(FloorItem);
         Assert.That(Hands.ActiveHandEntity, Is.Null);
         await AssertTile(Floor);
         AssertGridCount(1);
 
         // Tile -> Plating
-        await Interact(Pry);
+        await InteractUsing(Pry);
         await AssertTile(Plating);
         AssertGridCount(1);
 
diff --git a/Content.IntegrationTests/Tests/Toolshed/ToolshedTest.cs b/Content.IntegrationTests/Tests/Toolshed/ToolshedTest.cs
index dd68ff1ccf1..7de81fb3dc2 100644
--- a/Content.IntegrationTests/Tests/Toolshed/ToolshedTest.cs
+++ b/Content.IntegrationTests/Tests/Toolshed/ToolshedTest.cs
@@ -36,16 +36,18 @@ public async Task TearDownInternal()
         await TearDown();
     }
 
-    protected virtual async Task TearDown()
+    protected virtual Task TearDown()
     {
         Assert.That(_expectedErrors, Is.Empty);
         ClearErrors();
+
+        return Task.CompletedTask;
     }
 
     [SetUp]
     public virtual async Task Setup()
     {
-        Pair = await PoolManager.GetServerClient(new PoolSettings {Connected = Connected});
+        Pair = await PoolManager.GetServerClient(new PoolSettings { Connected = Connected });
         Server = Pair.Server;
 
         if (Connected)
@@ -142,7 +144,7 @@ public void ReportError(IConError err)
                 );
         }
 
-        done:
+    done:
         _errors.Add(err);
     }
 
diff --git a/Content.IntegrationTests/Tests/VendingMachineRestockTest.cs b/Content.IntegrationTests/Tests/VendingMachineRestockTest.cs
index 99481db70e7..5bfebfbd530 100644
--- a/Content.IntegrationTests/Tests/VendingMachineRestockTest.cs
+++ b/Content.IntegrationTests/Tests/VendingMachineRestockTest.cs
@@ -110,6 +110,7 @@ public async Task TestAllRestocksAreAvailableToBuy()
             await server.WaitIdleAsync();
 
             var prototypeManager = server.ResolveDependency<IPrototypeManager>();
+            var compFact = server.ResolveDependency<IComponentFactory>();
 
             await server.WaitAssertion(() =>
             {
@@ -132,7 +133,7 @@ await server.WaitAssertion(() =>
                 // Collect all the prototypes with StorageFills referencing those entities.
                 foreach (var proto in prototypeManager.EnumeratePrototypes<EntityPrototype>())
                 {
-                    if (!proto.TryGetComponent<StorageFillComponent>(out var storage))
+                    if (!proto.TryGetComponent<StorageFillComponent>(out var storage, compFact))
                         continue;
 
                     List<string> restockStore = new();
diff --git a/Content.IntegrationTests/Tests/Weldable/WeldableTests.cs b/Content.IntegrationTests/Tests/Weldable/WeldableTests.cs
index 6227f3dee1b..e7eadeda0a4 100644
--- a/Content.IntegrationTests/Tests/Weldable/WeldableTests.cs
+++ b/Content.IntegrationTests/Tests/Weldable/WeldableTests.cs
@@ -18,7 +18,7 @@ public async Task WeldLocker()
 
         Assert.That(comp.IsWelded, Is.False);
 
-        await Interact(Weld);
+        await InteractUsing(Weld);
         Assert.That(comp.IsWelded, Is.True);
         AssertPrototype(Locker); // Prototype did not change.
     }
diff --git a/Content.Server.Database/Migrations/Postgres/20240606065731_RemoveLastReadRules.Designer.cs b/Content.Server.Database/Migrations/Postgres/20240606065731_RemoveLastReadRules.Designer.cs
new file mode 100644
index 00000000000..222fe126d99
--- /dev/null
+++ b/Content.Server.Database/Migrations/Postgres/20240606065731_RemoveLastReadRules.Designer.cs
@@ -0,0 +1,1909 @@
+// <auto-generated />
+using System;
+using System.Net;
+using System.Text.Json;
+using Content.Server.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using NpgsqlTypes;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Postgres
+{
+    [DbContext(typeof(PostgresServerDbContext))]
+    [Migration("20240606065731_RemoveLastReadRules")]
+    partial class RemoveLastReadRules
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasAnnotation("ProductVersion", "8.0.0")
+                .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.Property<int?>("AdminRankId")
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<string>("Title")
+                        .HasColumnType("text")
+                        .HasColumnName("title");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_admin");
+
+                    b.HasIndex("AdminRankId")
+                        .HasDatabaseName("IX_admin_admin_rank_id");
+
+                    b.ToTable("admin", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_flag_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<Guid>("AdminId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("admin_id");
+
+                    b.Property<string>("Flag")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("flag");
+
+                    b.Property<bool>("Negative")
+                        .HasColumnType("boolean")
+                        .HasColumnName("negative");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_flag");
+
+                    b.HasIndex("AdminId")
+                        .HasDatabaseName("IX_admin_flag_admin_id");
+
+                    b.HasIndex("Flag", "AdminId")
+                        .IsUnique();
+
+                    b.ToTable("admin_flag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.Property<int>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Id")
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_log_id");
+
+                    b.Property<DateTime>("Date")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("date");
+
+                    b.Property<short>("Impact")
+                        .HasColumnType("smallint")
+                        .HasColumnName("impact");
+
+                    b.Property<JsonDocument>("Json")
+                        .IsRequired()
+                        .HasColumnType("jsonb")
+                        .HasColumnName("json");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("message");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("integer")
+                        .HasColumnName("type");
+
+                    b.HasKey("RoundId", "Id")
+                        .HasName("PK_admin_log");
+
+                    b.HasIndex("Date");
+
+                    b.HasIndex("Message")
+                        .HasAnnotation("Npgsql:TsVectorConfig", "english");
+
+                    NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Message"), "GIN");
+
+                    b.HasIndex("Type")
+                        .HasDatabaseName("IX_admin_log_type");
+
+                    b.ToTable("admin_log", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+                {
+                    b.Property<int>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("LogId")
+                        .HasColumnType("integer")
+                        .HasColumnName("log_id");
+
+                    b.Property<Guid>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.HasKey("RoundId", "LogId", "PlayerUserId")
+                        .HasName("PK_admin_log_player");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_log_player_player_user_id");
+
+                    b.ToTable("admin_log_player", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_messages_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("boolean")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<bool>("Dismissed")
+                        .HasColumnType("boolean")
+                        .HasColumnName("dismissed");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("character varying(4096)")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<bool>("Seen")
+                        .HasColumnType("boolean")
+                        .HasColumnName("seen");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_messages");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_messages_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_messages_round_id");
+
+                    b.ToTable("admin_messages", null, t =>
+                        {
+                            t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_notes_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("boolean")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .IsRequired()
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("character varying(4096)")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<bool>("Secret")
+                        .HasColumnType("boolean")
+                        .HasColumnName("secret");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("integer")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_notes");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_notes_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_notes_round_id");
+
+                    b.ToTable("admin_notes", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_rank_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_rank");
+
+                    b.ToTable("admin_rank", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_rank_flag_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("AdminRankId")
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<string>("Flag")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("flag");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_rank_flag");
+
+                    b.HasIndex("AdminRankId");
+
+                    b.HasIndex("Flag", "AdminRankId")
+                        .IsUnique();
+
+                    b.ToTable("admin_rank_flag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_watchlists_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("boolean")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .IsRequired()
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("character varying(4096)")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_watchlists");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_watchlists_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_watchlists_round_id");
+
+                    b.ToTable("admin_watchlists", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Antag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("antag_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("AntagName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("antag_name");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_antag");
+
+                    b.HasIndex("ProfileId", "AntagName")
+                        .IsUnique();
+
+                    b.ToTable("antag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AssignedUserId", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("assigned_user_id_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.Property<string>("UserName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("user_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_assigned_user_id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.HasIndex("UserName")
+                        .IsUnique();
+
+                    b.ToTable("assigned_user_id", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("connection_log_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<IPAddress>("Address")
+                        .IsRequired()
+                        .HasColumnType("inet")
+                        .HasColumnName("address");
+
+                    b.Property<byte?>("Denied")
+                        .HasColumnType("smallint")
+                        .HasColumnName("denied");
+
+                    b.Property<byte[]>("HWId")
+                        .HasColumnType("bytea")
+                        .HasColumnName("hwid");
+
+                    b.Property<int>("ServerId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasDefaultValue(0)
+                        .HasColumnName("server_id");
+
+                    b.Property<DateTime>("Time")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("time");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.Property<string>("UserName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("user_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_connection_log");
+
+                    b.HasIndex("ServerId")
+                        .HasDatabaseName("IX_connection_log_server_id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("connection_log", null, t =>
+                        {
+                            t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Job", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("job_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("JobName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("job_name");
+
+                    b.Property<int>("Priority")
+                        .HasColumnType("integer")
+                        .HasColumnName("priority");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_job");
+
+                    b.HasIndex("ProfileId");
+
+                    b.HasIndex("ProfileId", "JobName")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority")
+                        .IsUnique()
+                        .HasFilter("priority = 3");
+
+                    b.ToTable("job", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.PlayTime", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("play_time_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<Guid>("PlayerId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_id");
+
+                    b.Property<TimeSpan>("TimeSpent")
+                        .HasColumnType("interval")
+                        .HasColumnName("time_spent");
+
+                    b.Property<string>("Tracker")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("tracker");
+
+                    b.HasKey("Id")
+                        .HasName("PK_play_time");
+
+                    b.HasIndex("PlayerId", "Tracker")
+                        .IsUnique();
+
+                    b.ToTable("play_time", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Player", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("player_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<DateTime>("FirstSeenTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("first_seen_time");
+
+                    b.Property<IPAddress>("LastSeenAddress")
+                        .IsRequired()
+                        .HasColumnType("inet")
+                        .HasColumnName("last_seen_address");
+
+                    b.Property<byte[]>("LastSeenHWId")
+                        .HasColumnType("bytea")
+                        .HasColumnName("last_seen_hwid");
+
+                    b.Property<DateTime>("LastSeenTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_seen_time");
+
+                    b.Property<string>("LastSeenUserName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("last_seen_user_name");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_player");
+
+                    b.HasAlternateKey("UserId")
+                        .HasName("ak_player_user_id");
+
+                    b.HasIndex("LastSeenUserName");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("player", null, t =>
+                        {
+                            t.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Preference", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("preference_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("AdminOOCColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("admin_ooc_color");
+
+                    b.Property<int>("SelectedCharacterSlot")
+                        .HasColumnType("integer")
+                        .HasColumnName("selected_character_slot");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_preference");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("preference", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("Age")
+                        .HasColumnType("integer")
+                        .HasColumnName("age");
+
+                    b.Property<string>("CharacterName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("char_name");
+
+                    b.Property<string>("EyeColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("eye_color");
+
+                    b.Property<string>("FacialHairColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("facial_hair_color");
+
+                    b.Property<string>("FacialHairName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("facial_hair_name");
+
+                    b.Property<string>("FlavorText")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("flavor_text");
+
+                    b.Property<string>("Gender")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("gender");
+
+                    b.Property<string>("HairColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("hair_color");
+
+                    b.Property<string>("HairName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("hair_name");
+
+                    b.Property<JsonDocument>("Markings")
+                        .HasColumnType("jsonb")
+                        .HasColumnName("markings");
+
+                    b.Property<int>("PreferenceId")
+                        .HasColumnType("integer")
+                        .HasColumnName("preference_id");
+
+                    b.Property<int>("PreferenceUnavailable")
+                        .HasColumnType("integer")
+                        .HasColumnName("pref_unavailable");
+
+                    b.Property<string>("Sex")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("sex");
+
+                    b.Property<string>("SkinColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("skin_color");
+
+                    b.Property<int>("Slot")
+                        .HasColumnType("integer")
+                        .HasColumnName("slot");
+
+                    b.Property<int>("SpawnPriority")
+                        .HasColumnType("integer")
+                        .HasColumnName("spawn_priority");
+
+                    b.Property<string>("Species")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("species");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile");
+
+                    b.HasIndex("PreferenceId")
+                        .HasDatabaseName("IX_profile_preference_id");
+
+                    b.HasIndex("Slot", "PreferenceId")
+                        .IsUnique();
+
+                    b.ToTable("profile", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_loadout_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("LoadoutName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("loadout_name");
+
+                    b.Property<int>("ProfileLoadoutGroupId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_loadout_group_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_loadout");
+
+                    b.HasIndex("ProfileLoadoutGroupId");
+
+                    b.ToTable("profile_loadout", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_loadout_group_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("GroupName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("group_name");
+
+                    b.Property<int>("ProfileRoleLoadoutId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_role_loadout_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_loadout_group");
+
+                    b.HasIndex("ProfileRoleLoadoutId");
+
+                    b.ToTable("profile_loadout_group", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_role_loadout_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    b.Property<string>("RoleName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("role_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_role_loadout");
+
+                    b.HasIndex("ProfileId");
+
+                    b.ToTable("profile_role_loadout", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
+                {
+                    b.Property<Guid>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<string>("RoleId")
+                        .HasColumnType("text")
+                        .HasColumnName("role_id");
+
+                    b.HasKey("PlayerUserId", "RoleId")
+                        .HasName("PK_role_whitelists");
+
+                    b.ToTable("role_whitelists", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("ServerId")
+                        .HasColumnType("integer")
+                        .HasColumnName("server_id");
+
+                    b.Property<DateTime?>("StartDate")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("start_date");
+
+                    b.HasKey("Id")
+                        .HasName("PK_round");
+
+                    b.HasIndex("ServerId")
+                        .HasDatabaseName("IX_round_server_id");
+
+                    b.HasIndex("StartDate");
+
+                    b.ToTable("round", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Server", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("server_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server");
+
+                    b.ToTable("server", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("server_ban_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<NpgsqlInet?>("Address")
+                        .HasColumnType("inet")
+                        .HasColumnName("address");
+
+                    b.Property<bool>("AutoDelete")
+                        .HasColumnType("boolean")
+                        .HasColumnName("auto_delete");
+
+                    b.Property<DateTime>("BanTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("ban_time");
+
+                    b.Property<Guid?>("BanningAdmin")
+                        .HasColumnType("uuid")
+                        .HasColumnName("banning_admin");
+
+                    b.Property<int>("ExemptFlags")
+                        .HasColumnType("integer")
+                        .HasColumnName("exempt_flags");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<byte[]>("HWId")
+                        .HasColumnType("bytea")
+                        .HasColumnName("hwid");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("boolean")
+                        .HasColumnName("hidden");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("reason");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("integer")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_ban");
+
+                    b.HasIndex("Address");
+
+                    b.HasIndex("BanningAdmin");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_server_ban_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_server_ban_round_id");
+
+                    b.ToTable("server_ban", null, t =>
+                        {
+                            t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+
+                            t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.Property<int>("Flags")
+                        .HasColumnType("integer")
+                        .HasColumnName("flags");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_server_ban_exemption");
+
+                    b.ToTable("server_ban_exemption", null, t =>
+                        {
+                            t.HasCheckConstraint("FlagsNotZero", "flags != 0");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("server_ban_hit_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_id");
+
+                    b.Property<int>("ConnectionId")
+                        .HasColumnType("integer")
+                        .HasColumnName("connection_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_ban_hit");
+
+                    b.HasIndex("BanId")
+                        .HasDatabaseName("IX_server_ban_hit_ban_id");
+
+                    b.HasIndex("ConnectionId")
+                        .HasDatabaseName("IX_server_ban_hit_connection_id");
+
+                    b.ToTable("server_ban_hit", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("server_role_ban_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<NpgsqlInet?>("Address")
+                        .HasColumnType("inet")
+                        .HasColumnName("address");
+
+                    b.Property<DateTime>("BanTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("ban_time");
+
+                    b.Property<Guid?>("BanningAdmin")
+                        .HasColumnType("uuid")
+                        .HasColumnName("banning_admin");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<byte[]>("HWId")
+                        .HasColumnType("bytea")
+                        .HasColumnName("hwid");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("boolean")
+                        .HasColumnName("hidden");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("reason");
+
+                    b.Property<string>("RoleId")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("role_id");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("integer")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_role_ban");
+
+                    b.HasIndex("Address");
+
+                    b.HasIndex("BanningAdmin");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_server_role_ban_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_server_role_ban_round_id");
+
+                    b.ToTable("server_role_ban", null, t =>
+                        {
+                            t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+
+                            t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("role_unban_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_id");
+
+                    b.Property<DateTime>("UnbanTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("unban_time");
+
+                    b.Property<Guid?>("UnbanningAdmin")
+                        .HasColumnType("uuid")
+                        .HasColumnName("unbanning_admin");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_role_unban");
+
+                    b.HasIndex("BanId")
+                        .IsUnique();
+
+                    b.ToTable("server_role_unban", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("unban_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_id");
+
+                    b.Property<DateTime>("UnbanTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("unban_time");
+
+                    b.Property<Guid?>("UnbanningAdmin")
+                        .HasColumnType("uuid")
+                        .HasColumnName("unbanning_admin");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_unban");
+
+                    b.HasIndex("BanId")
+                        .IsUnique();
+
+                    b.ToTable("server_unban", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Trait", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("trait_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    b.Property<string>("TraitName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("trait_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_trait");
+
+                    b.HasIndex("ProfileId", "TraitName")
+                        .IsUnique();
+
+                    b.ToTable("trait", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("uploaded_resource_log_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<byte[]>("Data")
+                        .IsRequired()
+                        .HasColumnType("bytea")
+                        .HasColumnName("data");
+
+                    b.Property<DateTime>("Date")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("date");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("path");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_uploaded_resource_log");
+
+                    b.ToTable("uploaded_resource_log", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Whitelist", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_whitelist");
+
+                    b.ToTable("whitelist", (string)null);
+                });
+
+            modelBuilder.Entity("PlayerRound", b =>
+                {
+                    b.Property<int>("PlayersId")
+                        .HasColumnType("integer")
+                        .HasColumnName("players_id");
+
+                    b.Property<int>("RoundsId")
+                        .HasColumnType("integer")
+                        .HasColumnName("rounds_id");
+
+                    b.HasKey("PlayersId", "RoundsId")
+                        .HasName("PK_player_round");
+
+                    b.HasIndex("RoundsId")
+                        .HasDatabaseName("IX_player_round_rounds_id");
+
+                    b.ToTable("player_round", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.HasOne("Content.Server.Database.AdminRank", "AdminRank")
+                        .WithMany("Admins")
+                        .HasForeignKey("AdminRankId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_admin_rank_admin_rank_id");
+
+                    b.Navigation("AdminRank");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+                {
+                    b.HasOne("Content.Server.Database.Admin", "Admin")
+                        .WithMany("Flags")
+                        .HasForeignKey("AdminId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_flag_admin_admin_id");
+
+                    b.Navigation("Admin");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany("AdminLogs")
+                        .HasForeignKey("RoundId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_round_round_id");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminLogs")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_player_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.AdminLog", "Log")
+                        .WithMany("Players")
+                        .HasForeignKey("RoundId", "LogId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id");
+
+                    b.Navigation("Log");
+
+                    b.Navigation("Player");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminMessagesCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminMessagesDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminMessagesLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminMessagesReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_messages_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_messages_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminNotesCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminNotesDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminNotesLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminNotesReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_notes_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_notes_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+                {
+                    b.HasOne("Content.Server.Database.AdminRank", "Rank")
+                        .WithMany("Flags")
+                        .HasForeignKey("AdminRankId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id");
+
+                    b.Navigation("Rank");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminWatchlistsCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminWatchlistsDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminWatchlistsLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminWatchlistsReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_watchlists_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_watchlists_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Antag", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Antags")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_antag_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.HasOne("Content.Server.Database.Server", "Server")
+                        .WithMany("ConnectionLogs")
+                        .HasForeignKey("ServerId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .IsRequired()
+                        .HasConstraintName("FK_connection_log_server_server_id");
+
+                    b.Navigation("Server");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Job", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Jobs")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_job_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.HasOne("Content.Server.Database.Preference", "Preference")
+                        .WithMany("Profiles")
+                        .HasForeignKey("PreferenceId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_preference_preference_id");
+
+                    b.Navigation("Preference");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
+                {
+                    b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup")
+                        .WithMany("Loadouts")
+                        .HasForeignKey("ProfileLoadoutGroupId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group~");
+
+                    b.Navigation("ProfileLoadoutGroup");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout")
+                        .WithMany("Groups")
+                        .HasForeignKey("ProfileRoleLoadoutId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loa~");
+
+                    b.Navigation("ProfileRoleLoadout");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Loadouts")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_role_loadout_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("JobWhitelists")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_role_whitelists_player_player_user_id");
+
+                    b.Navigation("Player");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.HasOne("Content.Server.Database.Server", "Server")
+                        .WithMany("Rounds")
+                        .HasForeignKey("ServerId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_round_server_server_id");
+
+                    b.Navigation("Server");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminServerBansCreated")
+                        .HasForeignKey("BanningAdmin")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_ban_player_banning_admin");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminServerBansLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_ban_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_server_ban_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerBan", "Ban")
+                        .WithMany("BanHits")
+                        .HasForeignKey("BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_ban_hit_server_ban_ban_id");
+
+                    b.HasOne("Content.Server.Database.ConnectionLog", "Connection")
+                        .WithMany("BanHits")
+                        .HasForeignKey("ConnectionId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_ban_hit_connection_log_connection_id");
+
+                    b.Navigation("Ban");
+
+                    b.Navigation("Connection");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminServerRoleBansCreated")
+                        .HasForeignKey("BanningAdmin")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_role_ban_player_banning_admin");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminServerRoleBansLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_role_ban_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_server_role_ban_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerRoleBan", "Ban")
+                        .WithOne("Unban")
+                        .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id");
+
+                    b.Navigation("Ban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerBan", "Ban")
+                        .WithOne("Unban")
+                        .HasForeignKey("Content.Server.Database.ServerUnban", "BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_unban_server_ban_ban_id");
+
+                    b.Navigation("Ban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Trait", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Traits")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_trait_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("PlayerRound", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", null)
+                        .WithMany()
+                        .HasForeignKey("PlayersId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_player_round_player_players_id");
+
+                    b.HasOne("Content.Server.Database.Round", null)
+                        .WithMany()
+                        .HasForeignKey("RoundsId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_player_round_round_rounds_id");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.Navigation("Flags");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.Navigation("Players");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+                {
+                    b.Navigation("Admins");
+
+                    b.Navigation("Flags");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.Navigation("BanHits");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Player", b =>
+                {
+                    b.Navigation("AdminLogs");
+
+                    b.Navigation("AdminMessagesCreated");
+
+                    b.Navigation("AdminMessagesDeleted");
+
+                    b.Navigation("AdminMessagesLastEdited");
+
+                    b.Navigation("AdminMessagesReceived");
+
+                    b.Navigation("AdminNotesCreated");
+
+                    b.Navigation("AdminNotesDeleted");
+
+                    b.Navigation("AdminNotesLastEdited");
+
+                    b.Navigation("AdminNotesReceived");
+
+                    b.Navigation("AdminServerBansCreated");
+
+                    b.Navigation("AdminServerBansLastEdited");
+
+                    b.Navigation("AdminServerRoleBansCreated");
+
+                    b.Navigation("AdminServerRoleBansLastEdited");
+
+                    b.Navigation("AdminWatchlistsCreated");
+
+                    b.Navigation("AdminWatchlistsDeleted");
+
+                    b.Navigation("AdminWatchlistsLastEdited");
+
+                    b.Navigation("AdminWatchlistsReceived");
+
+                    b.Navigation("JobWhitelists");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Preference", b =>
+                {
+                    b.Navigation("Profiles");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.Navigation("Antags");
+
+                    b.Navigation("Jobs");
+
+                    b.Navigation("Loadouts");
+
+                    b.Navigation("Traits");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.Navigation("Loadouts");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.Navigation("Groups");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.Navigation("AdminLogs");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Server", b =>
+                {
+                    b.Navigation("ConnectionLogs");
+
+                    b.Navigation("Rounds");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.Navigation("BanHits");
+
+                    b.Navigation("Unban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.Navigation("Unban");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/Content.Server.Database/Migrations/Postgres/20240606065731_RemoveLastReadRules.cs b/Content.Server.Database/Migrations/Postgres/20240606065731_RemoveLastReadRules.cs
new file mode 100644
index 00000000000..1982197f62e
--- /dev/null
+++ b/Content.Server.Database/Migrations/Postgres/20240606065731_RemoveLastReadRules.cs
@@ -0,0 +1,29 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Postgres
+{
+    /// <inheritdoc />
+    public partial class RemoveLastReadRules : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropColumn(
+                name: "last_read_rules",
+                table: "player");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AddColumn<DateTime>(
+                name: "last_read_rules",
+                table: "player",
+                type: "timestamp with time zone",
+                nullable: true);
+        }
+    }
+}
diff --git a/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs
index 0282063649b..e06793fee11 100644
--- a/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs
+++ b/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs
@@ -720,10 +720,6 @@ protected override void BuildModel(ModelBuilder modelBuilder)
                         .HasColumnType("timestamp with time zone")
                         .HasColumnName("first_seen_time");
 
-                    b.Property<DateTime?>("LastReadRules")
-                        .HasColumnType("timestamp with time zone")
-                        .HasColumnName("last_read_rules");
-
                     b.Property<IPAddress>("LastSeenAddress")
                         .IsRequired()
                         .HasColumnType("inet")
diff --git a/Content.Server.Database/Migrations/Sqlite/20240606065717_RemoveLastReadRules.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20240606065717_RemoveLastReadRules.Designer.cs
new file mode 100644
index 00000000000..fceb831f00e
--- /dev/null
+++ b/Content.Server.Database/Migrations/Sqlite/20240606065717_RemoveLastReadRules.Designer.cs
@@ -0,0 +1,1834 @@
+// <auto-generated />
+using System;
+using Content.Server.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Sqlite
+{
+    [DbContext(typeof(SqliteServerDbContext))]
+    [Migration("20240606065717_RemoveLastReadRules")]
+    partial class RemoveLastReadRules
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.Property<int?>("AdminRankId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<string>("Title")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("title");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_admin");
+
+                    b.HasIndex("AdminRankId")
+                        .HasDatabaseName("IX_admin_admin_rank_id");
+
+                    b.ToTable("admin", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_flag_id");
+
+                    b.Property<Guid>("AdminId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("admin_id");
+
+                    b.Property<string>("Flag")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("flag");
+
+                    b.Property<bool>("Negative")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("negative");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_flag");
+
+                    b.HasIndex("AdminId")
+                        .HasDatabaseName("IX_admin_flag_admin_id");
+
+                    b.HasIndex("Flag", "AdminId")
+                        .IsUnique();
+
+                    b.ToTable("admin_flag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.Property<int>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Id")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_log_id");
+
+                    b.Property<DateTime>("Date")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("date");
+
+                    b.Property<sbyte>("Impact")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("impact");
+
+                    b.Property<string>("Json")
+                        .IsRequired()
+                        .HasColumnType("jsonb")
+                        .HasColumnName("json");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("message");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("type");
+
+                    b.HasKey("RoundId", "Id")
+                        .HasName("PK_admin_log");
+
+                    b.HasIndex("Date");
+
+                    b.HasIndex("Type")
+                        .HasDatabaseName("IX_admin_log_type");
+
+                    b.ToTable("admin_log", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+                {
+                    b.Property<int>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("LogId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("log_id");
+
+                    b.Property<Guid>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.HasKey("RoundId", "LogId", "PlayerUserId")
+                        .HasName("PK_admin_log_player");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_log_player_player_user_id");
+
+                    b.ToTable("admin_log_player", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_messages_id");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<bool>("Dismissed")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("dismissed");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("TEXT")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<bool>("Seen")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("seen");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_messages");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_messages_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_messages_round_id");
+
+                    b.ToTable("admin_messages", null, t =>
+                        {
+                            t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_notes_id");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("TEXT")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<bool>("Secret")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("secret");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_notes");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_notes_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_notes_round_id");
+
+                    b.ToTable("admin_notes", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_rank");
+
+                    b.ToTable("admin_rank", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_rank_flag_id");
+
+                    b.Property<int>("AdminRankId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<string>("Flag")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("flag");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_rank_flag");
+
+                    b.HasIndex("AdminRankId");
+
+                    b.HasIndex("Flag", "AdminRankId")
+                        .IsUnique();
+
+                    b.ToTable("admin_rank_flag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_watchlists_id");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("TEXT")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_watchlists");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_watchlists_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_watchlists_round_id");
+
+                    b.ToTable("admin_watchlists", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Antag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("antag_id");
+
+                    b.Property<string>("AntagName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("antag_name");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_antag");
+
+                    b.HasIndex("ProfileId", "AntagName")
+                        .IsUnique();
+
+                    b.ToTable("antag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AssignedUserId", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("assigned_user_id_id");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.Property<string>("UserName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_assigned_user_id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.HasIndex("UserName")
+                        .IsUnique();
+
+                    b.ToTable("assigned_user_id", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("connection_log_id");
+
+                    b.Property<string>("Address")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("address");
+
+                    b.Property<byte?>("Denied")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("denied");
+
+                    b.Property<byte[]>("HWId")
+                        .HasColumnType("BLOB")
+                        .HasColumnName("hwid");
+
+                    b.Property<int>("ServerId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasDefaultValue(0)
+                        .HasColumnName("server_id");
+
+                    b.Property<DateTime>("Time")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("time");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.Property<string>("UserName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_connection_log");
+
+                    b.HasIndex("ServerId")
+                        .HasDatabaseName("IX_connection_log_server_id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("connection_log", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Job", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("job_id");
+
+                    b.Property<string>("JobName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("job_name");
+
+                    b.Property<int>("Priority")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("priority");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_job");
+
+                    b.HasIndex("ProfileId");
+
+                    b.HasIndex("ProfileId", "JobName")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority")
+                        .IsUnique()
+                        .HasFilter("priority = 3");
+
+                    b.ToTable("job", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.PlayTime", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("play_time_id");
+
+                    b.Property<Guid>("PlayerId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_id");
+
+                    b.Property<TimeSpan>("TimeSpent")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("time_spent");
+
+                    b.Property<string>("Tracker")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("tracker");
+
+                    b.HasKey("Id")
+                        .HasName("PK_play_time");
+
+                    b.HasIndex("PlayerId", "Tracker")
+                        .IsUnique();
+
+                    b.ToTable("play_time", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Player", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("player_id");
+
+                    b.Property<DateTime>("FirstSeenTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("first_seen_time");
+
+                    b.Property<string>("LastSeenAddress")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_seen_address");
+
+                    b.Property<byte[]>("LastSeenHWId")
+                        .HasColumnType("BLOB")
+                        .HasColumnName("last_seen_hwid");
+
+                    b.Property<DateTime>("LastSeenTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_seen_time");
+
+                    b.Property<string>("LastSeenUserName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_seen_user_name");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_player");
+
+                    b.HasAlternateKey("UserId")
+                        .HasName("ak_player_user_id");
+
+                    b.HasIndex("LastSeenUserName");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("player", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Preference", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("preference_id");
+
+                    b.Property<string>("AdminOOCColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("admin_ooc_color");
+
+                    b.Property<int>("SelectedCharacterSlot")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("selected_character_slot");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_preference");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("preference", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.Property<int>("Age")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("age");
+
+                    b.Property<string>("CharacterName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("char_name");
+
+                    b.Property<string>("EyeColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("eye_color");
+
+                    b.Property<string>("FacialHairColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("facial_hair_color");
+
+                    b.Property<string>("FacialHairName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("facial_hair_name");
+
+                    b.Property<string>("FlavorText")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("flavor_text");
+
+                    b.Property<string>("Gender")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("gender");
+
+                    b.Property<string>("HairColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("hair_color");
+
+                    b.Property<string>("HairName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("hair_name");
+
+                    b.Property<byte[]>("Markings")
+                        .HasColumnType("jsonb")
+                        .HasColumnName("markings");
+
+                    b.Property<int>("PreferenceId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("preference_id");
+
+                    b.Property<int>("PreferenceUnavailable")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("pref_unavailable");
+
+                    b.Property<string>("Sex")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("sex");
+
+                    b.Property<string>("SkinColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("skin_color");
+
+                    b.Property<int>("Slot")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("slot");
+
+                    b.Property<int>("SpawnPriority")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("spawn_priority");
+
+                    b.Property<string>("Species")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("species");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile");
+
+                    b.HasIndex("PreferenceId")
+                        .HasDatabaseName("IX_profile_preference_id");
+
+                    b.HasIndex("Slot", "PreferenceId")
+                        .IsUnique();
+
+                    b.ToTable("profile", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_loadout_id");
+
+                    b.Property<string>("LoadoutName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("loadout_name");
+
+                    b.Property<int>("ProfileLoadoutGroupId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_loadout_group_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_loadout");
+
+                    b.HasIndex("ProfileLoadoutGroupId");
+
+                    b.ToTable("profile_loadout", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_loadout_group_id");
+
+                    b.Property<string>("GroupName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("group_name");
+
+                    b.Property<int>("ProfileRoleLoadoutId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_role_loadout_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_loadout_group");
+
+                    b.HasIndex("ProfileRoleLoadoutId");
+
+                    b.ToTable("profile_loadout_group", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_role_loadout_id");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.Property<string>("RoleName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("role_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_role_loadout");
+
+                    b.HasIndex("ProfileId");
+
+                    b.ToTable("profile_role_loadout", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
+                {
+                    b.Property<Guid>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<string>("RoleId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("role_id");
+
+                    b.HasKey("PlayerUserId", "RoleId")
+                        .HasName("PK_role_whitelists");
+
+                    b.ToTable("role_whitelists", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("ServerId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("server_id");
+
+                    b.Property<DateTime?>("StartDate")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("start_date");
+
+                    b.HasKey("Id")
+                        .HasName("PK_round");
+
+                    b.HasIndex("ServerId")
+                        .HasDatabaseName("IX_round_server_id");
+
+                    b.HasIndex("StartDate");
+
+                    b.ToTable("round", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Server", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("server_id");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server");
+
+                    b.ToTable("server", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("server_ban_id");
+
+                    b.Property<string>("Address")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("address");
+
+                    b.Property<bool>("AutoDelete")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("auto_delete");
+
+                    b.Property<DateTime>("BanTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("ban_time");
+
+                    b.Property<Guid?>("BanningAdmin")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("banning_admin");
+
+                    b.Property<int>("ExemptFlags")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("exempt_flags");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<byte[]>("HWId")
+                        .HasColumnType("BLOB")
+                        .HasColumnName("hwid");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("hidden");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("reason");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_ban");
+
+                    b.HasIndex("Address");
+
+                    b.HasIndex("BanningAdmin");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_server_ban_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_server_ban_round_id");
+
+                    b.ToTable("server_ban", null, t =>
+                        {
+                            t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.Property<int>("Flags")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("flags");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_server_ban_exemption");
+
+                    b.ToTable("server_ban_exemption", null, t =>
+                        {
+                            t.HasCheckConstraint("FlagsNotZero", "flags != 0");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("server_ban_hit_id");
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_id");
+
+                    b.Property<int>("ConnectionId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("connection_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_ban_hit");
+
+                    b.HasIndex("BanId")
+                        .HasDatabaseName("IX_server_ban_hit_ban_id");
+
+                    b.HasIndex("ConnectionId")
+                        .HasDatabaseName("IX_server_ban_hit_connection_id");
+
+                    b.ToTable("server_ban_hit", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("server_role_ban_id");
+
+                    b.Property<string>("Address")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("address");
+
+                    b.Property<DateTime>("BanTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("ban_time");
+
+                    b.Property<Guid?>("BanningAdmin")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("banning_admin");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<byte[]>("HWId")
+                        .HasColumnType("BLOB")
+                        .HasColumnName("hwid");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("hidden");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("reason");
+
+                    b.Property<string>("RoleId")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("role_id");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_role_ban");
+
+                    b.HasIndex("Address");
+
+                    b.HasIndex("BanningAdmin");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_server_role_ban_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_server_role_ban_round_id");
+
+                    b.ToTable("server_role_ban", null, t =>
+                        {
+                            t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("role_unban_id");
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_id");
+
+                    b.Property<DateTime>("UnbanTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("unban_time");
+
+                    b.Property<Guid?>("UnbanningAdmin")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("unbanning_admin");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_role_unban");
+
+                    b.HasIndex("BanId")
+                        .IsUnique();
+
+                    b.ToTable("server_role_unban", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("unban_id");
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_id");
+
+                    b.Property<DateTime>("UnbanTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("unban_time");
+
+                    b.Property<Guid?>("UnbanningAdmin")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("unbanning_admin");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_unban");
+
+                    b.HasIndex("BanId")
+                        .IsUnique();
+
+                    b.ToTable("server_unban", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Trait", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("trait_id");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.Property<string>("TraitName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("trait_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_trait");
+
+                    b.HasIndex("ProfileId", "TraitName")
+                        .IsUnique();
+
+                    b.ToTable("trait", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("uploaded_resource_log_id");
+
+                    b.Property<byte[]>("Data")
+                        .IsRequired()
+                        .HasColumnType("BLOB")
+                        .HasColumnName("data");
+
+                    b.Property<DateTime>("Date")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("date");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("path");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_uploaded_resource_log");
+
+                    b.ToTable("uploaded_resource_log", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Whitelist", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_whitelist");
+
+                    b.ToTable("whitelist", (string)null);
+                });
+
+            modelBuilder.Entity("PlayerRound", b =>
+                {
+                    b.Property<int>("PlayersId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("players_id");
+
+                    b.Property<int>("RoundsId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("rounds_id");
+
+                    b.HasKey("PlayersId", "RoundsId")
+                        .HasName("PK_player_round");
+
+                    b.HasIndex("RoundsId")
+                        .HasDatabaseName("IX_player_round_rounds_id");
+
+                    b.ToTable("player_round", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.HasOne("Content.Server.Database.AdminRank", "AdminRank")
+                        .WithMany("Admins")
+                        .HasForeignKey("AdminRankId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_admin_rank_admin_rank_id");
+
+                    b.Navigation("AdminRank");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+                {
+                    b.HasOne("Content.Server.Database.Admin", "Admin")
+                        .WithMany("Flags")
+                        .HasForeignKey("AdminId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_flag_admin_admin_id");
+
+                    b.Navigation("Admin");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany("AdminLogs")
+                        .HasForeignKey("RoundId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_round_round_id");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminLogs")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_player_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.AdminLog", "Log")
+                        .WithMany("Players")
+                        .HasForeignKey("RoundId", "LogId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id");
+
+                    b.Navigation("Log");
+
+                    b.Navigation("Player");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminMessagesCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminMessagesDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminMessagesLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminMessagesReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_messages_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_messages_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminNotesCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminNotesDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminNotesLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminNotesReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_notes_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_notes_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+                {
+                    b.HasOne("Content.Server.Database.AdminRank", "Rank")
+                        .WithMany("Flags")
+                        .HasForeignKey("AdminRankId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id");
+
+                    b.Navigation("Rank");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminWatchlistsCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminWatchlistsDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminWatchlistsLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminWatchlistsReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_watchlists_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_watchlists_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Antag", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Antags")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_antag_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.HasOne("Content.Server.Database.Server", "Server")
+                        .WithMany("ConnectionLogs")
+                        .HasForeignKey("ServerId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .IsRequired()
+                        .HasConstraintName("FK_connection_log_server_server_id");
+
+                    b.Navigation("Server");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Job", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Jobs")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_job_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.HasOne("Content.Server.Database.Preference", "Preference")
+                        .WithMany("Profiles")
+                        .HasForeignKey("PreferenceId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_preference_preference_id");
+
+                    b.Navigation("Preference");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
+                {
+                    b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup")
+                        .WithMany("Loadouts")
+                        .HasForeignKey("ProfileLoadoutGroupId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group_id");
+
+                    b.Navigation("ProfileLoadoutGroup");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout")
+                        .WithMany("Groups")
+                        .HasForeignKey("ProfileRoleLoadoutId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loadout_id");
+
+                    b.Navigation("ProfileRoleLoadout");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Loadouts")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_role_loadout_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("JobWhitelists")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_role_whitelists_player_player_user_id");
+
+                    b.Navigation("Player");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.HasOne("Content.Server.Database.Server", "Server")
+                        .WithMany("Rounds")
+                        .HasForeignKey("ServerId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_round_server_server_id");
+
+                    b.Navigation("Server");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminServerBansCreated")
+                        .HasForeignKey("BanningAdmin")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_ban_player_banning_admin");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminServerBansLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_ban_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_server_ban_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerBan", "Ban")
+                        .WithMany("BanHits")
+                        .HasForeignKey("BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_ban_hit_server_ban_ban_id");
+
+                    b.HasOne("Content.Server.Database.ConnectionLog", "Connection")
+                        .WithMany("BanHits")
+                        .HasForeignKey("ConnectionId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_ban_hit_connection_log_connection_id");
+
+                    b.Navigation("Ban");
+
+                    b.Navigation("Connection");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminServerRoleBansCreated")
+                        .HasForeignKey("BanningAdmin")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_role_ban_player_banning_admin");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminServerRoleBansLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_role_ban_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_server_role_ban_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerRoleBan", "Ban")
+                        .WithOne("Unban")
+                        .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id");
+
+                    b.Navigation("Ban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerBan", "Ban")
+                        .WithOne("Unban")
+                        .HasForeignKey("Content.Server.Database.ServerUnban", "BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_unban_server_ban_ban_id");
+
+                    b.Navigation("Ban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Trait", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Traits")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_trait_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("PlayerRound", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", null)
+                        .WithMany()
+                        .HasForeignKey("PlayersId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_player_round_player_players_id");
+
+                    b.HasOne("Content.Server.Database.Round", null)
+                        .WithMany()
+                        .HasForeignKey("RoundsId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_player_round_round_rounds_id");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.Navigation("Flags");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.Navigation("Players");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+                {
+                    b.Navigation("Admins");
+
+                    b.Navigation("Flags");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.Navigation("BanHits");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Player", b =>
+                {
+                    b.Navigation("AdminLogs");
+
+                    b.Navigation("AdminMessagesCreated");
+
+                    b.Navigation("AdminMessagesDeleted");
+
+                    b.Navigation("AdminMessagesLastEdited");
+
+                    b.Navigation("AdminMessagesReceived");
+
+                    b.Navigation("AdminNotesCreated");
+
+                    b.Navigation("AdminNotesDeleted");
+
+                    b.Navigation("AdminNotesLastEdited");
+
+                    b.Navigation("AdminNotesReceived");
+
+                    b.Navigation("AdminServerBansCreated");
+
+                    b.Navigation("AdminServerBansLastEdited");
+
+                    b.Navigation("AdminServerRoleBansCreated");
+
+                    b.Navigation("AdminServerRoleBansLastEdited");
+
+                    b.Navigation("AdminWatchlistsCreated");
+
+                    b.Navigation("AdminWatchlistsDeleted");
+
+                    b.Navigation("AdminWatchlistsLastEdited");
+
+                    b.Navigation("AdminWatchlistsReceived");
+
+                    b.Navigation("JobWhitelists");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Preference", b =>
+                {
+                    b.Navigation("Profiles");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.Navigation("Antags");
+
+                    b.Navigation("Jobs");
+
+                    b.Navigation("Loadouts");
+
+                    b.Navigation("Traits");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.Navigation("Loadouts");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.Navigation("Groups");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.Navigation("AdminLogs");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Server", b =>
+                {
+                    b.Navigation("ConnectionLogs");
+
+                    b.Navigation("Rounds");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.Navigation("BanHits");
+
+                    b.Navigation("Unban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.Navigation("Unban");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/Content.Server.Database/Migrations/Sqlite/20240606065717_RemoveLastReadRules.cs b/Content.Server.Database/Migrations/Sqlite/20240606065717_RemoveLastReadRules.cs
new file mode 100644
index 00000000000..7f98fc2410e
--- /dev/null
+++ b/Content.Server.Database/Migrations/Sqlite/20240606065717_RemoveLastReadRules.cs
@@ -0,0 +1,29 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Sqlite
+{
+    /// <inheritdoc />
+    public partial class RemoveLastReadRules : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropColumn(
+                name: "last_read_rules",
+                table: "player");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AddColumn<DateTime>(
+                name: "last_read_rules",
+                table: "player",
+                type: "TEXT",
+                nullable: true);
+        }
+    }
+}
diff --git a/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs
index c7a79b97171..9494b646eb7 100644
--- a/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs
+++ b/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs
@@ -676,10 +676,6 @@ protected override void BuildModel(ModelBuilder modelBuilder)
                         .HasColumnType("TEXT")
                         .HasColumnName("first_seen_time");
 
-                    b.Property<DateTime?>("LastReadRules")
-                        .HasColumnType("TEXT")
-                        .HasColumnName("last_read_rules");
-
                     b.Property<string>("LastSeenAddress")
                         .IsRequired()
                         .HasColumnType("TEXT")
diff --git a/Content.Server.Database/Model.cs b/Content.Server.Database/Model.cs
index e698805cfce..eb6ce3434b8 100644
--- a/Content.Server.Database/Model.cs
+++ b/Content.Server.Database/Model.cs
@@ -447,8 +447,6 @@ public class Player
         public List<Round> Rounds { get; set; } = null!;
         public List<AdminLogPlayer> AdminLogs { get; set; } = null!;
 
-        public DateTime? LastReadRules { get; set; }
-
         public List<AdminNote> AdminNotesReceived { get; set; } = null!;
         public List<AdminNote> AdminNotesCreated { get; set; } = null!;
         public List<AdminNote> AdminNotesLastEdited { get; set; } = null!;
diff --git a/Content.Server/Abilities/Mime/MimePowersComponent.cs b/Content.Server/Abilities/Mime/MimePowersComponent.cs
index fd4fc2c2af9..d56644ed191 100644
--- a/Content.Server/Abilities/Mime/MimePowersComponent.cs
+++ b/Content.Server/Abilities/Mime/MimePowersComponent.cs
@@ -1,3 +1,4 @@
+using Content.Shared.Alert;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
@@ -47,5 +48,12 @@ public sealed partial class MimePowersComponent : Component
         /// </summary>
         [DataField("vowCooldown")]
         public TimeSpan VowCooldown = TimeSpan.FromMinutes(5);
+
+        [DataField]
+        public ProtoId<AlertPrototype> VowAlert = "VowOfSilence";
+
+        [DataField]
+        public ProtoId<AlertPrototype> VowBrokenAlert = "VowBroken";
+
     }
 }
diff --git a/Content.Server/Abilities/Mime/MimePowersSystem.cs b/Content.Server/Abilities/Mime/MimePowersSystem.cs
index 57163a96a50..a1e50228ae2 100644
--- a/Content.Server/Abilities/Mime/MimePowersSystem.cs
+++ b/Content.Server/Abilities/Mime/MimePowersSystem.cs
@@ -1,5 +1,4 @@
 using Content.Server.Popups;
-using Content.Server.Speech.Muting;
 using Content.Shared.Actions;
 using Content.Shared.Actions.Events;
 using Content.Shared.Alert;
@@ -56,7 +55,7 @@ public override void Update(float frameTime)
         private void OnComponentInit(EntityUid uid, MimePowersComponent component, ComponentInit args)
         {
             EnsureComp<MutedComponent>(uid);
-            _alertsSystem.ShowAlert(uid, AlertType.VowOfSilence);
+            _alertsSystem.ShowAlert(uid, component.VowAlert);
             _actionsSystem.AddAction(uid, ref component.InvisibleWallActionEntity, component.InvisibleWallAction, uid);
         }
 
@@ -120,8 +119,8 @@ public void BreakVow(EntityUid uid, MimePowersComponent? mimePowers = null)
             mimePowers.VowBroken = true;
             mimePowers.VowRepentTime = _timing.CurTime + mimePowers.VowCooldown;
             RemComp<MutedComponent>(uid);
-            _alertsSystem.ClearAlert(uid, AlertType.VowOfSilence);
-            _alertsSystem.ShowAlert(uid, AlertType.VowBroken);
+            _alertsSystem.ClearAlert(uid, mimePowers.VowAlert);
+            _alertsSystem.ShowAlert(uid, mimePowers.VowBrokenAlert);
             _actionsSystem.RemoveAction(uid, mimePowers.InvisibleWallActionEntity);
         }
 
@@ -143,8 +142,8 @@ public void RetakeVow(EntityUid uid, MimePowersComponent? mimePowers = null)
             mimePowers.ReadyToRepent = false;
             mimePowers.VowBroken = false;
             AddComp<MutedComponent>(uid);
-            _alertsSystem.ClearAlert(uid, AlertType.VowBroken);
-            _alertsSystem.ShowAlert(uid, AlertType.VowOfSilence);
+            _alertsSystem.ClearAlert(uid, mimePowers.VowAlert);
+            _alertsSystem.ShowAlert(uid, mimePowers.VowBrokenAlert);
             _actionsSystem.AddAction(uid, ref mimePowers.InvisibleWallActionEntity, mimePowers.InvisibleWallAction, uid);
         }
     }
diff --git a/Content.Server/Administration/Managers/AdminManager.cs b/Content.Server/Administration/Managers/AdminManager.cs
index ec284fcadce..205024a71e4 100644
--- a/Content.Server/Administration/Managers/AdminManager.cs
+++ b/Content.Server/Administration/Managers/AdminManager.cs
@@ -7,6 +7,7 @@
 using Content.Server.Players;
 using Content.Shared.Administration;
 using Content.Shared.CCVar;
+using Content.Shared.Info;
 using Content.Shared.Players;
 using Robust.Server.Console;
 using Robust.Server.Player;
@@ -233,6 +234,7 @@ public void Initialize()
             _sawmill = _logManager.GetSawmill("admin");
 
             _netMgr.RegisterNetMessage<MsgUpdateAdminStatus>();
+            _netMgr.RegisterNetMessage<ShowRulesPopupMessage>();
 
             // Cache permissions for loaded console commands with the requisite attributes.
             foreach (var (cmdName, cmd) in _consoleHost.AvailableCommands)
diff --git a/Content.Server/Administration/ServerApi.cs b/Content.Server/Administration/ServerApi.cs
index 04fd38598fb..d0f23db637b 100644
--- a/Content.Server/Administration/ServerApi.cs
+++ b/Content.Server/Administration/ServerApi.cs
@@ -8,13 +8,13 @@
 using System.Threading.Tasks;
 using Content.Server.Administration.Systems;
 using Content.Server.GameTicking;
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Presets;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Maps;
 using Content.Server.RoundEnd;
 using Content.Shared.Administration.Managers;
 using Content.Shared.CCVar;
+using Content.Shared.GameTicking.Components;
 using Content.Shared.Prototypes;
 using Robust.Server.ServerStatus;
 using Robust.Shared.Asynchronous;
diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
index 4e6af6ceea5..bda60e9449a 100644
--- a/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
+++ b/Content.Server/Administration/Systems/AdminVerbSystem.Smites.cs
@@ -635,13 +635,13 @@ private void AddSmiteVerbs(GetVerbsEvent<Verb> args)
         {
             Text = "Remove gravity",
             Category = VerbCategory.Smite,
-            Icon = new SpriteSpecifier.Rsi(new ("/Textures/Structures/Machines/gravity_generator.rsi"), "off"),
+            Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Machines/gravity_generator.rsi"), "off"),
             Act = () =>
             {
                 var grav = EnsureComp<MovementIgnoreGravityComponent>(args.Target);
                 grav.Weightless = true;
 
-                Dirty(grav);
+                Dirty(args.Target, grav);
             },
             Impact = LogImpact.Extreme,
             Message = Loc.GetString("admin-smite-remove-gravity-description"),
@@ -738,7 +738,7 @@ private void AddSmiteVerbs(GetVerbsEvent<Verb> args)
                 var movementSpeed = EnsureComp<MovementSpeedModifierComponent>(args.Target);
                 (movementSpeed.BaseSprintSpeed, movementSpeed.BaseWalkSpeed) = (movementSpeed.BaseWalkSpeed, movementSpeed.BaseSprintSpeed);
 
-                Dirty(movementSpeed);
+                Dirty(args.Target, movementSpeed);
 
                 _popupSystem.PopupEntity(Loc.GetString("admin-smite-run-walk-swap-prompt"), args.Target,
                     args.Target, PopupType.LargeCaution);
diff --git a/Content.Server/Alert/Commands/ClearAlert.cs b/Content.Server/Alert/Commands/ClearAlert.cs
index 1759612702f..929c343b50f 100644
--- a/Content.Server/Alert/Commands/ClearAlert.cs
+++ b/Content.Server/Alert/Commands/ClearAlert.cs
@@ -9,6 +9,7 @@ namespace Content.Server.Alert.Commands
     [AdminCommand(AdminFlags.Debug)]
     public sealed class ClearAlert : IConsoleCommand
     {
+        [Dependency] private readonly IEntityManager _e = default!;
         public string Command => "clearalert";
         public string Description => "Clears an alert for a player, defaulting to current player";
         public string Help => "clearalert <alertType> <name or userID, omit for current player>";
@@ -37,14 +38,14 @@ public void Execute(IConsoleShell shell, string argStr, string[] args)
             }
 
             var alertType = args[0];
-            var alertsSystem = EntitySystem.Get<AlertsSystem>();
-            if (!alertsSystem.TryGet(Enum.Parse<AlertType>(alertType), out var alert))
+            var alertsSystem = _e.System<AlertsSystem>();
+            if (!alertsSystem.TryGet(alertType, out var alert))
             {
                 shell.WriteLine("unrecognized alertType " + alertType);
                 return;
             }
 
-            alertsSystem.ClearAlert(attachedEntity, alert.AlertType);
+            alertsSystem.ClearAlert(attachedEntity, alert.ID);
         }
     }
 }
diff --git a/Content.Server/Alert/Commands/ShowAlert.cs b/Content.Server/Alert/Commands/ShowAlert.cs
index 11901e9af00..a275dab4fa5 100644
--- a/Content.Server/Alert/Commands/ShowAlert.cs
+++ b/Content.Server/Alert/Commands/ShowAlert.cs
@@ -9,6 +9,7 @@ namespace Content.Server.Alert.Commands
     [AdminCommand(AdminFlags.Debug)]
     public sealed class ShowAlert : IConsoleCommand
     {
+        [Dependency] private readonly IEntityManager _e = default!;
         public string Command => "showalert";
         public string Description => "Shows an alert for a player, defaulting to current player";
         public string Help => "showalert <alertType> <severity, -1 if no severity> <name or userID, omit for current player>";
@@ -38,8 +39,8 @@ public void Execute(IConsoleShell shell, string argStr, string[] args)
 
             var alertType = args[0];
             var severity = args[1];
-            var alertsSystem = EntitySystem.Get<AlertsSystem>();
-            if (!alertsSystem.TryGet(Enum.Parse<AlertType>(alertType), out var alert))
+            var alertsSystem = _e.System<AlertsSystem>();
+            if (!alertsSystem.TryGet(alertType, out var alert))
             {
                 shell.WriteLine("unrecognized alertType " + alertType);
                 return;
@@ -51,7 +52,7 @@ public void Execute(IConsoleShell shell, string argStr, string[] args)
             }
 
             short? severity1 = sevint == -1 ? null : sevint;
-            alertsSystem.ShowAlert(attachedEntity, alert.AlertType, severity1, null);
+            alertsSystem.ShowAlert(attachedEntity, alert.ID, severity1, null);
         }
     }
 }
diff --git a/Content.Server/Antag/AntagSelectionSystem.cs b/Content.Server/Antag/AntagSelectionSystem.cs
index cd4d836e683..f5e6de64e53 100644
--- a/Content.Server/Antag/AntagSelectionSystem.cs
+++ b/Content.Server/Antag/AntagSelectionSystem.cs
@@ -2,7 +2,6 @@
 using Content.Server.Antag.Components;
 using Content.Server.Chat.Managers;
 using Content.Server.GameTicking;
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules;
 using Content.Server.Ghost.Roles;
 using Content.Server.Ghost.Roles.Components;
@@ -14,6 +13,7 @@
 using Content.Server.Station.Systems;
 using Content.Shared.Antag;
 using Content.Shared.GameTicking;
+using Content.Shared.GameTicking.Components;
 using Content.Shared.Ghost;
 using Content.Shared.Humanoid;
 using Content.Shared.Players;
diff --git a/Content.Server/Antag/Components/AntagSelectionComponent.cs b/Content.Server/Antag/Components/AntagSelectionComponent.cs
index 096be14049a..5b6699dab76 100644
--- a/Content.Server/Antag/Components/AntagSelectionComponent.cs
+++ b/Content.Server/Antag/Components/AntagSelectionComponent.cs
@@ -1,6 +1,6 @@
 using Content.Server.Administration.Systems;
-using Content.Server.Destructible.Thresholds;
 using Content.Shared.Antag;
+using Content.Shared.Destructible.Thresholds;
 using Content.Shared.Roles;
 using Content.Shared.Storage;
 using Content.Shared.Whitelist;
diff --git a/Content.Server/Antag/MobReplacementRuleSystem.cs b/Content.Server/Antag/MobReplacementRuleSystem.cs
index dc8103497ee..b2ad984884b 100644
--- a/Content.Server/Antag/MobReplacementRuleSystem.cs
+++ b/Content.Server/Antag/MobReplacementRuleSystem.cs
@@ -1,11 +1,8 @@
 using System.Numerics;
 using Content.Server.Antag.Mimic;
-using Content.Server.Chat.Systems;
 using Content.Server.GameTicking.Rules;
-using Content.Server.GameTicking.Components;
-using Content.Server.NPC.Systems;
-using Content.Server.Station.Systems;
-using Content.Server.GameTicking;
+using Content.Server.GameTicking.Rules.Components;
+using Content.Shared.GameTicking.Components;
 using Content.Shared.VendingMachines;
 using Robust.Shared.Map;
 using Robust.Shared.Prototypes;
@@ -23,6 +20,10 @@
 using Content.Server.Advertise.Components;
 using Content.Server.Power.Components;
 using Content.Shared.CombatMode;
+using Content.Server.Station.Systems;
+using Content.Server.GameTicking;
+using Content.Server.Chat.Systems;
+using Content.Server.NPC.Systems;
 
 namespace Content.Server.Antag;
 
diff --git a/Content.Server/Atmos/Components/BarotraumaComponent.cs b/Content.Server/Atmos/Components/BarotraumaComponent.cs
index 4e29699872e..d261c5ab030 100644
--- a/Content.Server/Atmos/Components/BarotraumaComponent.cs
+++ b/Content.Server/Atmos/Components/BarotraumaComponent.cs
@@ -1,5 +1,7 @@
+using Content.Shared.Alert;
 using Content.Shared.Damage;
 using Content.Shared.FixedPoint;
+using Robust.Shared.Prototypes;
 
 namespace Content.Server.Atmos.Components
 {
@@ -46,5 +48,13 @@ public sealed partial class BarotraumaComponent : Component
         [ViewVariables(VVAccess.ReadWrite)]
         public bool HasImmunity = false;
 
+        [DataField]
+        public ProtoId<AlertPrototype> HighPressureAlert = "HighPressure";
+
+        [DataField]
+        public ProtoId<AlertPrototype> LowPressureAlert = "LowPressure";
+
+        [DataField]
+        public ProtoId<AlertCategoryPrototype> PressureAlertCategory = "Pressure";
     }
 }
diff --git a/Content.Server/Atmos/Components/FlammableComponent.cs b/Content.Server/Atmos/Components/FlammableComponent.cs
index 9f39af540dd..e1c7974307b 100644
--- a/Content.Server/Atmos/Components/FlammableComponent.cs
+++ b/Content.Server/Atmos/Components/FlammableComponent.cs
@@ -1,5 +1,7 @@
+using Content.Shared.Alert;
 using Content.Shared.Damage;
 using Robust.Shared.Physics.Collision.Shapes;
+using Robust.Shared.Prototypes;
 
 namespace Content.Server.Atmos.Components
 {
@@ -83,5 +85,7 @@ public sealed partial class FlammableComponent : Component
         /// </summary>
         [DataField]
         public float FireStackIncreaseMultiplier = 1f;
+        [DataField]
+        public ProtoId<AlertPrototype> FireAlert = "Fire";
     }
 }
diff --git a/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs b/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs
index 35f5ec8a3bc..6bf76221c11 100644
--- a/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs
+++ b/Content.Server/Atmos/EntitySystems/BarotraumaSystem.cs
@@ -241,7 +241,7 @@ public override void Update(float frameTime)
                         _adminLogger.Add(LogType.Barotrauma, $"{ToPrettyString(uid):entity} started taking low pressure damage");
                     }
                     RaiseLocalEvent(uid, new MoodEffectEvent("MobLowPressure"));
-                    _alertsSystem.ShowAlert(uid, AlertType.LowPressure, 2);
+                    _alertsSystem.ShowAlert(uid, barotrauma.LowPressureAlert, 2);
                 }
                 else if (pressure >= Atmospherics.HazardHighPressure)
                 {
@@ -255,7 +255,8 @@ public override void Update(float frameTime)
                         barotrauma.TakingDamage = true;
                         _adminLogger.Add(LogType.Barotrauma, $"{ToPrettyString(uid):entity} started taking high pressure damage");
                     }
-                    _alertsSystem.ShowAlert(uid, AlertType.HighPressure, 2);
+
+                    _alertsSystem.ShowAlert(uid, barotrauma.HighPressureAlert, 2);
                 }
                 else
                 {
@@ -269,13 +270,13 @@ public override void Update(float frameTime)
                     switch (pressure)
                     {
                         case <= Atmospherics.WarningLowPressure:
-                            _alertsSystem.ShowAlert(uid, AlertType.LowPressure, 1);
+                            _alertsSystem.ShowAlert(uid, barotrauma.LowPressureAlert, 1);
                             break;
                         case >= Atmospherics.WarningHighPressure:
-                            _alertsSystem.ShowAlert(uid, AlertType.HighPressure, 1);
+                            _alertsSystem.ShowAlert(uid, barotrauma.HighPressureAlert, 1);
                             break;
                         default:
-                            _alertsSystem.ClearAlertCategory(uid, AlertCategory.Pressure);
+                            _alertsSystem.ClearAlertCategory(uid, barotrauma.PressureAlertCategory);
                             break;
                     }
                 }
diff --git a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs
index ec0e7b07092..cf6287d7001 100644
--- a/Content.Server/Atmos/EntitySystems/FlammableSystem.cs
+++ b/Content.Server/Atmos/EntitySystems/FlammableSystem.cs
@@ -423,12 +423,12 @@ public override void Update(float frameTime)
 
                 if (!flammable.OnFire)
                 {
-                    _alertsSystem.ClearAlert(uid, AlertType.Fire);
+                    _alertsSystem.ClearAlert(uid, flammable.FireAlert);
                     RaiseLocalEvent(uid, new MoodRemoveEffectEvent("OnFire"));
                     continue;
                 }
 
-                _alertsSystem.ShowAlert(uid, AlertType.Fire);
+                _alertsSystem.ShowAlert(uid, flammable.FireAlert);
                 RaiseLocalEvent(uid, new MoodEffectEvent("OnFire"));
 
                 if (flammable.FireStacks > 0)
diff --git a/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs b/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs
index 65977f8c140..b42f3626293 100644
--- a/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs
+++ b/Content.Server/Atmos/EntitySystems/GasAnalyzerSystem.cs
@@ -93,7 +93,7 @@ private void ActivateAnalyzer(EntityUid uid, GasAnalyzerComponent component, Ent
             else
                 component.LastPosition = null;
             component.Enabled = true;
-            Dirty(component);
+            Dirty(uid, component);
             UpdateAppearance(uid, component);
             EnsureComp<ActiveGasAnalyzerComponent>(uid);
             UpdateAnalyzer(uid, component);
@@ -105,7 +105,7 @@ private void ActivateAnalyzer(EntityUid uid, GasAnalyzerComponent component, Ent
         /// </summary>
         private void OnDropped(EntityUid uid, GasAnalyzerComponent component, DroppedEvent args)
         {
-            if(args.User is var userId && component.Enabled)
+            if (args.User is var userId && component.Enabled)
                 _popup.PopupEntity(Loc.GetString("gas-analyzer-shutoff"), userId, userId);
             DisableAnalyzer(uid, component, args.User);
         }
@@ -121,7 +121,7 @@ private void DisableAnalyzer(EntityUid uid, GasAnalyzerComponent? component = nu
             _userInterface.CloseUi(uid, GasAnalyzerUiKey.Key, user);
 
             component.Enabled = false;
-            Dirty(component);
+            Dirty(uid, component);
             UpdateAppearance(uid, component);
             RemCompDeferred<ActiveGasAnalyzerComponent>(uid);
         }
diff --git a/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs b/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs
index c46701a6a07..89b9c520787 100644
--- a/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs
+++ b/Content.Server/Atmos/EntitySystems/GasTileOverlaySystem.cs
@@ -122,10 +122,11 @@ private void OnPvsToggle(bool value)
             }
 
             // PVS was turned off, ensure data gets sent to all clients.
-            foreach (var (grid, meta) in EntityQuery<GasTileOverlayComponent, MetaDataComponent>(true))
+            var query = EntityQueryEnumerator<GasTileOverlayComponent, MetaDataComponent>();
+            while (query.MoveNext(out var uid, out var grid, out var meta))
             {
                 grid.ForceTick = _gameTiming.CurTick;
-                Dirty(grid, meta);
+                Dirty(uid, grid, meta);
             }
         }
 
@@ -268,9 +269,10 @@ private bool UpdateChunkTile(GridAtmosphereComponent gridAtmosphere, GasOverlayC
         private void UpdateOverlayData()
         {
             // TODO parallelize?
-            foreach (var (overlay, gam, meta) in EntityQuery<GasTileOverlayComponent, GridAtmosphereComponent, MetaDataComponent>(true))
+            var query = EntityQueryEnumerator<GasTileOverlayComponent, GridAtmosphereComponent, MetaDataComponent>();
+            while (query.MoveNext(out var uid, out var overlay, out var gam, out var meta))
             {
-                bool changed = false;
+                var changed = false;
                 foreach (var index in overlay.InvalidTiles)
                 {
                     var chunkIndex = GetGasChunkIndices(index);
@@ -282,7 +284,7 @@ private void UpdateOverlayData()
                 }
 
                 if (changed)
-                    Dirty(overlay, meta);
+                    Dirty(uid, overlay, meta);
 
                 overlay.InvalidTiles.Clear();
             }
diff --git a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasPortableSystem.cs b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasPortableSystem.cs
index 7cb8102a388..bf973e34c8e 100644
--- a/Content.Server/Atmos/Piping/Unary/EntitySystems/GasPortableSystem.cs
+++ b/Content.Server/Atmos/Piping/Unary/EntitySystems/GasPortableSystem.cs
@@ -17,6 +17,7 @@ public sealed class GasPortableSystem : EntitySystem
     {
         [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
         [Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
+        [Dependency] private readonly SharedMapSystem _sharedMapSystem = default!;
 
         public override void Initialize()
         {
@@ -33,7 +34,7 @@ private void OnPortableAnchorAttempt(EntityUid uid, GasPortableComponent compone
                 return;
 
             // If we can't find any ports, cancel the anchoring.
-            if(!FindGasPortIn(transform.GridUid, transform.Coordinates, out _))
+            if (!FindGasPortIn(transform.GridUid, transform.Coordinates, out _))
                 args.Cancel();
         }
 
@@ -54,10 +55,13 @@ public bool FindGasPortIn(EntityUid? gridId, EntityCoordinates coordinates, [Not
         {
             port = null;
 
+            if (gridId == null)
+                return false;
+
             if (!TryComp<MapGridComponent>(gridId, out var grid))
                 return false;
 
-            foreach (var entityUid in grid.GetLocal(coordinates))
+            foreach (var entityUid in _sharedMapSystem.GetLocal((EntityUid) gridId, grid, coordinates))
             {
                 if (EntityManager.TryGetComponent(entityUid, out port))
                 {
diff --git a/Content.Server/BarSign/Systems/BarSignSystem.cs b/Content.Server/BarSign/Systems/BarSignSystem.cs
index 4a481408452..e42394f5a30 100644
--- a/Content.Server/BarSign/Systems/BarSignSystem.cs
+++ b/Content.Server/BarSign/Systems/BarSignSystem.cs
@@ -34,7 +34,7 @@ private void OnMapInit(EntityUid uid, BarSignComponent component, MapInitEvent a
             _metaData.SetEntityDescription(uid, Loc.GetString(newPrototype.Description), meta);
 
             component.Current = newPrototype.ID;
-            Dirty(component);
+            Dirty(uid, component);
         }
     }
 }
diff --git a/Content.Server/Bed/BedSystem.cs b/Content.Server/Bed/BedSystem.cs
index 976ef5139c3..089ce322366 100644
--- a/Content.Server/Bed/BedSystem.cs
+++ b/Content.Server/Bed/BedSystem.cs
@@ -15,6 +15,7 @@
 using Content.Shared.Mobs.Systems;
 using Robust.Shared.Timing;
 using Content.Shared.Silicon.Components; // I shouldn't have to modify this.
+using Robust.Shared.Utility;
 
 namespace Content.Server.Bed
 {
@@ -30,27 +31,31 @@ public sealed class BedSystem : EntitySystem
         public override void Initialize()
         {
             base.Initialize();
-            SubscribeLocalEvent<HealOnBuckleComponent, BuckleChangeEvent>(ManageUpdateList);
-            SubscribeLocalEvent<StasisBedComponent, BuckleChangeEvent>(OnBuckleChange);
+            SubscribeLocalEvent<HealOnBuckleComponent, StrappedEvent>(OnStrapped);
+            SubscribeLocalEvent<HealOnBuckleComponent, UnstrappedEvent>(OnUnstrapped);
+            SubscribeLocalEvent<StasisBedComponent, StrappedEvent>(OnStasisStrapped);
+            SubscribeLocalEvent<StasisBedComponent, UnstrappedEvent>(OnStasisUnstrapped);
             SubscribeLocalEvent<StasisBedComponent, PowerChangedEvent>(OnPowerChanged);
             SubscribeLocalEvent<StasisBedComponent, GotEmaggedEvent>(OnEmagged);
             SubscribeLocalEvent<StasisBedComponent, RefreshPartsEvent>(OnRefreshParts);
             SubscribeLocalEvent<StasisBedComponent, UpgradeExamineEvent>(OnUpgradeExamine);
         }
 
-        private void ManageUpdateList(EntityUid uid, HealOnBuckleComponent component, ref BuckleChangeEvent args)
+        private void OnStrapped(Entity<HealOnBuckleComponent> bed, ref StrappedEvent args)
         {
-            if (args.Buckling)
-            {
-                AddComp<HealOnBuckleHealingComponent>(uid);
-                component.NextHealTime = _timing.CurTime + TimeSpan.FromSeconds(component.HealTime);
-                _actionsSystem.AddAction(args.BuckledEntity, ref component.SleepAction, SleepingSystem.SleepActionId, uid);
-                return;
-            }
+            EnsureComp<HealOnBuckleHealingComponent>(bed);
+            bed.Comp.NextHealTime = _timing.CurTime + TimeSpan.FromSeconds(bed.Comp.HealTime);
+            _actionsSystem.AddAction(args.Buckle, ref bed.Comp.SleepAction, SleepingSystem.SleepActionId, bed);
 
-            _actionsSystem.RemoveAction(args.BuckledEntity, component.SleepAction);
-            _sleepingSystem.TryWaking(args.BuckledEntity);
-            RemComp<HealOnBuckleHealingComponent>(uid);
+            // Single action entity, cannot strap multiple entities to the same bed.
+            DebugTools.AssertEqual(args.Strap.Comp.BuckledEntities.Count, 1);
+        }
+
+        private void OnUnstrapped(Entity<HealOnBuckleComponent> bed, ref UnstrappedEvent args)
+        {
+            _actionsSystem.RemoveAction(args.Buckle, bed.Comp.SleepAction);
+            _sleepingSystem.TryWaking(args.Buckle.Owner);
+            RemComp<HealOnBuckleHealingComponent>(bed);
         }
 
         public override void Update(float frameTime)
@@ -89,18 +94,22 @@ private void UpdateAppearance(EntityUid uid, bool isOn)
             _appearance.SetData(uid, StasisBedVisuals.IsOn, isOn);
         }
 
-        private void OnBuckleChange(EntityUid uid, StasisBedComponent component, ref BuckleChangeEvent args)
+        private void OnStasisStrapped(Entity<StasisBedComponent> bed, ref StrappedEvent args)
         {
-            // In testing this also received an unbuckle event when the bed is destroyed
-            // So don't worry about that
-            if (!HasComp<BodyComponent>(args.BuckledEntity))
+            if (!HasComp<BodyComponent>(args.Buckle) || !this.IsPowered(bed, EntityManager))
                 return;
 
-            if (!this.IsPowered(uid, EntityManager))
+            var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.Buckle, bed.Comp.Multiplier, true);
+            RaiseLocalEvent(args.Buckle, ref metabolicEvent);
+        }
+
+        private void OnStasisUnstrapped(Entity<StasisBedComponent> bed, ref UnstrappedEvent args)
+        {
+            if (!HasComp<BodyComponent>(args.Buckle) || !this.IsPowered(bed, EntityManager))
                 return;
 
-            var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.BuckledEntity, component.Multiplier, args.Buckling);
-            RaiseLocalEvent(args.BuckledEntity, ref metabolicEvent);
+            var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.Buckle, bed.Comp.Multiplier, false);
+            RaiseLocalEvent(args.Buckle, ref metabolicEvent);
         }
 
         private void OnPowerChanged(EntityUid uid, StasisBedComponent component, ref PowerChangedEvent args)
diff --git a/Content.Server/Bed/Components/HealOnBuckleComponent.cs b/Content.Server/Bed/Components/HealOnBuckleComponent.cs
index f29fe30429f..3c6f3a4382b 100644
--- a/Content.Server/Bed/Components/HealOnBuckleComponent.cs
+++ b/Content.Server/Bed/Components/HealOnBuckleComponent.cs
@@ -5,19 +5,26 @@ namespace Content.Server.Bed.Components
     [RegisterComponent]
     public sealed partial class HealOnBuckleComponent : Component
     {
-        [DataField("damage", required: true)]
-        [ViewVariables(VVAccess.ReadWrite)]
+        /// <summary>
+        /// Damage to apply to entities that are strapped to this entity.
+        /// </summary>
+        [DataField(required: true)]
         public DamageSpecifier Damage = default!;
 
-        [DataField("healTime", required: false)]
-        [ViewVariables(VVAccess.ReadWrite)]
-        public float HealTime = 1f; // How often the bed applies the damage
+        /// <summary>
+        /// How frequently the damage should be applied, in seconds.
+        /// </summary>
+        [DataField(required: false)]
+        public float HealTime = 1f;
 
-        [DataField("sleepMultiplier")]
+        /// <summary>
+        /// Damage multiplier that gets applied if the entity is sleeping.
+        /// </summary>
+        [DataField]
         public float SleepMultiplier = 3f;
 
         public TimeSpan NextHealTime = TimeSpan.Zero; //Next heal
 
-        [DataField("sleepAction")] public EntityUid? SleepAction;
+        [DataField] public EntityUid? SleepAction;
     }
 }
diff --git a/Content.Server/Bed/Components/HealOnBuckleHealing.cs b/Content.Server/Bed/Components/HealOnBuckleHealing.cs
index a944e67e12d..aaa82c737c5 100644
--- a/Content.Server/Bed/Components/HealOnBuckleHealing.cs
+++ b/Content.Server/Bed/Components/HealOnBuckleHealing.cs
@@ -1,5 +1,6 @@
 namespace Content.Server.Bed.Components
 {
+    // TODO rename this component
     [RegisterComponent]
     public sealed partial class HealOnBuckleHealingComponent : Component
     {}
diff --git a/Content.Server/Bed/Components/StasisBedComponent.cs b/Content.Server/Bed/Components/StasisBedComponent.cs
index bb4096a2a5e..6e0042b2df8 100644
--- a/Content.Server/Bed/Components/StasisBedComponent.cs
+++ b/Content.Server/Bed/Components/StasisBedComponent.cs
@@ -12,7 +12,8 @@ public sealed partial class StasisBedComponent : Component
         /// <summary>
         ///     What the metabolic update rate will be multiplied by (higher = slower metabolism)
         /// </summary>
-        [ViewVariables(VVAccess.ReadWrite)]
+        [ViewVariables(VVAccess.ReadOnly)] // Writing is is not supported. ApplyMetabolicMultiplierEvent needs to be refactored first
+        [DataField]
         public float Multiplier = 10f;
 
         [DataField(customTypeSerializer: typeof(PrototypeIdSerializer<MachinePartPrototype>))]
diff --git a/Content.Server/Body/Components/BloodstreamComponent.cs b/Content.Server/Body/Components/BloodstreamComponent.cs
index f7024a63951..ee0de4aa4dc 100644
--- a/Content.Server/Body/Components/BloodstreamComponent.cs
+++ b/Content.Server/Body/Components/BloodstreamComponent.cs
@@ -2,6 +2,7 @@
 using Content.Server.Chemistry.EntitySystems;
 using Content.Server.Traits;
 using Content.Server.Traits.Assorted;
+using Content.Shared.Alert;
 using Content.Shared.Chemistry.Components;
 using Content.Shared.Chemistry.Reagent;
 using Content.Shared.Damage;
@@ -181,5 +182,8 @@ public sealed partial class BloodstreamComponent : Component
         /// </summary>
         [ViewVariables(VVAccess.ReadWrite)]
         public TimeSpan StatusTime;
+
+        [DataField]
+        public ProtoId<AlertPrototype> BleedingAlert = "Bleed";
     }
 }
diff --git a/Content.Server/Body/Components/InternalsComponent.cs b/Content.Server/Body/Components/InternalsComponent.cs
index 18caab8dcf0..098f1789218 100644
--- a/Content.Server/Body/Components/InternalsComponent.cs
+++ b/Content.Server/Body/Components/InternalsComponent.cs
@@ -1,3 +1,6 @@
+using Content.Shared.Alert;
+using Robust.Shared.Prototypes;
+
 namespace Content.Server.Body.Components
 {
     /// <summary>
@@ -18,5 +21,8 @@ public sealed partial class InternalsComponent : Component
         [ViewVariables(VVAccess.ReadWrite)]
         [DataField]
         public TimeSpan Delay = TimeSpan.FromSeconds(3);
+
+        [DataField]
+        public ProtoId<AlertPrototype> InternalsAlert = "Internals";
     }
 }
diff --git a/Content.Server/Body/Components/LungComponent.cs b/Content.Server/Body/Components/LungComponent.cs
index 46600b30207..72af4d9e63a 100644
--- a/Content.Server/Body/Components/LungComponent.cs
+++ b/Content.Server/Body/Components/LungComponent.cs
@@ -1,8 +1,8 @@
-using Content.Server.Atmos;
 using Content.Server.Body.Systems;
 using Content.Shared.Alert;
 using Content.Shared.Atmos;
 using Content.Shared.Chemistry.Components;
+using Robust.Shared.Prototypes;
 
 namespace Content.Server.Body.Components;
 
@@ -33,5 +33,5 @@ public sealed partial class LungComponent : Component
     /// The type of gas this lung needs. Used only for the breathing alerts, not actual metabolism.
     /// </summary>
     [DataField]
-    public AlertType Alert = AlertType.LowOxygen;
+    public ProtoId<AlertPrototype> Alert = "LowOxygen";
 }
diff --git a/Content.Server/Body/Systems/BloodstreamSystem.cs b/Content.Server/Body/Systems/BloodstreamSystem.cs
index d1fad6541ba..095018f9b9a 100644
--- a/Content.Server/Body/Systems/BloodstreamSystem.cs
+++ b/Content.Server/Body/Systems/BloodstreamSystem.cs
@@ -280,6 +280,9 @@ private void OnApplyMetabolicMultiplier(
         Entity<BloodstreamComponent> ent,
         ref ApplyMetabolicMultiplierEvent args)
     {
+        // TODO REFACTOR THIS
+        // This will slowly drift over time due to floating point errors.
+        // Instead, raise an event with the base rates and allow modifiers to get applied to it.
         if (args.Apply)
         {
             ent.Comp.UpdateInterval *= args.Multiplier;
@@ -412,11 +415,11 @@ public bool TryModifyBleedAmount(EntityUid uid, float amount, BloodstreamCompone
         component.BleedAmount = Math.Clamp(component.BleedAmount, 0, component.MaxBleedAmount);
 
         if (component.BleedAmount == 0)
-            _alertsSystem.ClearAlert(uid, AlertType.Bleed);
+            _alertsSystem.ClearAlert(uid, component.BleedingAlert);
         else
         {
             var severity = (short) Math.Clamp(Math.Round(component.BleedAmount, MidpointRounding.ToZero), 0, 10);
-            _alertsSystem.ShowAlert(uid, AlertType.Bleed, severity);
+            _alertsSystem.ShowAlert(uid, component.BleedingAlert, severity);
         }
 
         return true;
diff --git a/Content.Server/Body/Systems/InternalsSystem.cs b/Content.Server/Body/Systems/InternalsSystem.cs
index db078e2f291..fdcc76718cf 100644
--- a/Content.Server/Body/Systems/InternalsSystem.cs
+++ b/Content.Server/Body/Systems/InternalsSystem.cs
@@ -145,12 +145,12 @@ private void OnDoAfter(Entity<InternalsComponent> ent, ref InternalsDoAfterEvent
 
     private void OnInternalsStartup(Entity<InternalsComponent> ent, ref ComponentStartup args)
     {
-        _alerts.ShowAlert(ent, AlertType.Internals, GetSeverity(ent));
+        _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
     }
 
     private void OnInternalsShutdown(Entity<InternalsComponent> ent, ref ComponentShutdown args)
     {
-        _alerts.ClearAlert(ent, AlertType.Internals);
+        _alerts.ClearAlert(ent, ent.Comp.InternalsAlert);
     }
 
     private void OnInhaleLocation(Entity<InternalsComponent> ent, ref InhaleLocationEvent args)
@@ -160,7 +160,7 @@ private void OnInhaleLocation(Entity<InternalsComponent> ent, ref InhaleLocation
             var gasTank = Comp<GasTankComponent>(ent.Comp.GasTankEntity!.Value);
             args.Gas = _gasTank.RemoveAirVolume((ent.Comp.GasTankEntity.Value, gasTank), Atmospherics.BreathVolume);
             // TODO: Should listen to gas tank updates instead I guess?
-            _alerts.ShowAlert(ent, AlertType.Internals, GetSeverity(ent));
+            _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
         }
     }
     public void DisconnectBreathTool(Entity<InternalsComponent> ent)
@@ -174,7 +174,7 @@ public void DisconnectBreathTool(Entity<InternalsComponent> ent)
             DisconnectTank(ent);
         }
 
-        _alerts.ShowAlert(ent, AlertType.Internals, GetSeverity(ent));
+        _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
     }
 
     public void ConnectBreathTool(Entity<InternalsComponent> ent, EntityUid toolEntity)
@@ -185,7 +185,7 @@ public void ConnectBreathTool(Entity<InternalsComponent> ent, EntityUid toolEnti
         }
 
         ent.Comp.BreathToolEntity = toolEntity;
-        _alerts.ShowAlert(ent, AlertType.Internals, GetSeverity(ent));
+        _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
     }
 
     public void DisconnectTank(InternalsComponent? component)
@@ -197,7 +197,7 @@ public void DisconnectTank(InternalsComponent? component)
             _gasTank.DisconnectFromInternals((component.GasTankEntity.Value, tank));
 
         component.GasTankEntity = null;
-        _alerts.ShowAlert(component.Owner, AlertType.Internals, GetSeverity(component));
+        _alerts.ShowAlert(component.Owner, component.InternalsAlert, GetSeverity(component));
     }
 
     public bool TryConnectTank(Entity<InternalsComponent> ent, EntityUid tankEntity)
@@ -209,7 +209,7 @@ public bool TryConnectTank(Entity<InternalsComponent> ent, EntityUid tankEntity)
             _gasTank.DisconnectFromInternals((ent.Comp.GasTankEntity.Value, tank));
 
         ent.Comp.GasTankEntity = tankEntity;
-        _alerts.ShowAlert(ent, AlertType.Internals, GetSeverity(ent));
+        _alerts.ShowAlert(ent, ent.Comp.InternalsAlert, GetSeverity(ent));
         return true;
     }
 
diff --git a/Content.Server/Body/Systems/MetabolizerSystem.cs b/Content.Server/Body/Systems/MetabolizerSystem.cs
index 066bf0a1c5b..a7eec8e3c02 100644
--- a/Content.Server/Body/Systems/MetabolizerSystem.cs
+++ b/Content.Server/Body/Systems/MetabolizerSystem.cs
@@ -67,6 +67,9 @@ private void OnApplyMetabolicMultiplier(
             Entity<MetabolizerComponent> ent,
             ref ApplyMetabolicMultiplierEvent args)
         {
+            // TODO REFACTOR THIS
+            // This will slowly drift over time due to floating point errors.
+            // Instead, raise an event with the base rates and allow modifiers to get applied to it.
             if (args.Apply)
             {
                 ent.Comp.UpdateInterval *= args.Multiplier;
@@ -232,6 +235,9 @@ private void TryMetabolize(Entity<MetabolizerComponent, OrganComponent?, Solutio
         }
     }
 
+    // TODO REFACTOR THIS
+    // This will cause rates to slowly drift over time due to floating point errors.
+    // Instead, the system that raised this should trigger an update and subscribe to get-modifier events.
     [ByRefEvent]
     public readonly record struct ApplyMetabolicMultiplierEvent(
         EntityUid Uid,
diff --git a/Content.Server/Body/Systems/RespiratorSystem.cs b/Content.Server/Body/Systems/RespiratorSystem.cs
index 8b2b19a2ac0..4b669009f59 100644
--- a/Content.Server/Body/Systems/RespiratorSystem.cs
+++ b/Content.Server/Body/Systems/RespiratorSystem.cs
@@ -218,6 +218,9 @@ private void OnApplyMetabolicMultiplier(
         Entity<RespiratorComponent> ent,
         ref ApplyMetabolicMultiplierEvent args)
     {
+        // TODO REFACTOR THIS
+        // This will slowly drift over time due to floating point errors.
+        // Instead, raise an event with the base rates and allow modifiers to get applied to it.
         if (args.Apply)
         {
             ent.Comp.UpdateInterval *= args.Multiplier;
diff --git a/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs b/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs
index 1c5c7ed1c0d..1480c28bf22 100644
--- a/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs
+++ b/Content.Server/Cargo/Systems/CargoSystem.Shuttle.cs
@@ -20,6 +20,7 @@
 using Robust.Shared.Physics.Components;
 using Robust.Shared.Utility;
 using Robust.Shared.Configuration;
+using Robust.Shared.Map.Components;
 
 namespace Content.Server.Cargo.Systems;
 
@@ -91,14 +92,7 @@ private void UpdatePalletConsoleInterface(EntityUid uid)
     }
 
     private void OnPalletUIOpen(EntityUid uid, CargoPalletConsoleComponent component, BoundUIOpenedEvent args)
-    {
-        var player = args.Actor;
-
-        if (player == null)
-            return;
-
-        UpdatePalletConsoleInterface(uid);
-    }
+        => UpdatePalletConsoleInterface(uid);
 
     /// <summary>
     /// Ok so this is just the same thing as opening the UI, its a refresh button.
@@ -109,20 +103,10 @@ private void OnPalletUIOpen(EntityUid uid, CargoPalletConsoleComponent component
     /// </summary>
 
     private void OnPalletAppraise(EntityUid uid, CargoPalletConsoleComponent component, CargoPalletAppraiseMessage args)
-    {
-        var player = args.Actor;
-
-        if (player == null)
-            return;
-
-        UpdatePalletConsoleInterface(uid);
-    }
+        => UpdatePalletConsoleInterface(uid);
 
     private void OnCargoShuttleConsoleStartup(EntityUid uid, CargoShuttleConsoleComponent component, ComponentStartup args)
-    {
-        var station = _station.GetOwningStation(uid);
-        UpdateShuttleState(uid, station);
-    }
+        => UpdateShuttleState(uid, _station.GetOwningStation(uid));
 
     private void UpdateShuttleState(EntityUid uid, EntityUid? station = null)
     {
@@ -339,10 +323,6 @@ private bool CanSell(EntityUid uid, TransformComponent xform)
     private void OnPalletSale(EntityUid uid, CargoPalletConsoleComponent component, CargoPalletSellMessage args)
     {
         var player = args.Actor;
-
-        if (player == null)
-            return;
-
         var xform = Transform(uid);
 
         if (xform.GridUid is not EntityUid gridUid)
@@ -380,7 +360,7 @@ private void OnStationInitialize(StationInitializedEvent args)
 
     private void CleanupTradeStation()
     {
-        if (CargoMap == null || !_mapManager.MapExists(CargoMap.Value))
+        if (CargoMap == null || !_sharedMapSystem.MapExists(CargoMap.Value))
         {
             CargoMap = null;
             DebugTools.Assert(!EntityQuery<CargoShuttleComponent>().Any());
@@ -393,13 +373,14 @@ private void CleanupTradeStation()
 
     private void SetupTradePost()
     {
-        if (CargoMap != null && _mapManager.MapExists(CargoMap.Value))
+        if (CargoMap != null && _sharedMapSystem.MapExists(CargoMap.Value))
         {
             return;
         }
 
         // It gets mapinit which is okay... buuutt we still want it paused to avoid power draining.
-        CargoMap = _mapManager.CreateMap();
+        var mapEntId = _mapSystem.CreateMap();
+        CargoMap = _entityManager.GetComponent<MapComponent>(mapEntId).MapId;
 
         var options = new MapLoadOptions
         {
@@ -420,11 +401,12 @@ private void SetupTradePost()
             var shuttleComponent = EnsureComp<ShuttleComponent>(grid);
             shuttleComponent.AngularDamping = 10000;
             shuttleComponent.LinearDamping = 10000;
-            Dirty(shuttleComponent);
+            Dirty(grid, shuttleComponent);
         }
 
-        var mapUid = _mapManager.GetMapEntityId(CargoMap.Value);
-        var ftl = EnsureComp<FTLDestinationComponent>(_mapManager.GetMapEntityId(CargoMap.Value));
+        var mapUid = _sharedMapSystem.GetMap(CargoMap.Value);
+        var ftl = EnsureComp<FTLDestinationComponent>(mapUid);
+
         ftl.Whitelist = new EntityWhitelist()
         {
             Components =
diff --git a/Content.Server/Cargo/Systems/CargoSystem.cs b/Content.Server/Cargo/Systems/CargoSystem.cs
index e593299321e..16597fa1bcd 100644
--- a/Content.Server/Cargo/Systems/CargoSystem.cs
+++ b/Content.Server/Cargo/Systems/CargoSystem.cs
@@ -47,7 +47,10 @@ public sealed partial class CargoSystem : SharedCargoSystem
     [Dependency] private readonly MetaDataSystem _metaSystem = default!;
     [Dependency] private readonly RadioSystem _radio = default!;
     [Dependency] private readonly IConfigurationManager _cfgManager = default!;
+    [Dependency] private readonly SharedMapSystem _sharedMapSystem = default!;
+    [Dependency] private readonly MapSystem _mapSystem = default!;
     [Dependency] private readonly IMapManager _mapManager = default!;
+    [Dependency] private readonly IEntityManager _entityManager = default!;
     [Dependency] private readonly IComponentFactory _factory = default!;
     [Dependency] private readonly MapLoaderSystem _mapLoader = default!;
 
diff --git a/Content.Server/Carrying/CarryingSystem.cs b/Content.Server/Carrying/CarryingSystem.cs
index 857c3861a74..ca69d2f9299 100644
--- a/Content.Server/Carrying/CarryingSystem.cs
+++ b/Content.Server/Carrying/CarryingSystem.cs
@@ -67,7 +67,7 @@ public override void Initialize()
             SubscribeLocalEvent<BeingCarriedComponent, GettingInteractedWithAttemptEvent>(OnInteractedWith);
             SubscribeLocalEvent<BeingCarriedComponent, PullAttemptEvent>(OnPullAttempt);
             SubscribeLocalEvent<BeingCarriedComponent, StartClimbEvent>(OnStartClimb);
-            SubscribeLocalEvent<BeingCarriedComponent, BuckleChangeEvent>(OnBuckleChange);
+            SubscribeLocalEvent<BeingCarriedComponent, BuckledEvent>(OnBuckled);
             SubscribeLocalEvent<CarriableComponent, CarryDoAfterEvent>(OnDoAfter);
         }
 
@@ -215,7 +215,7 @@ private void OnStartClimb(EntityUid uid, BeingCarriedComponent component, ref St
             DropCarried(component.Carrier, uid);
         }
 
-        private void OnBuckleChange(EntityUid uid, BeingCarriedComponent component, ref BuckleChangeEvent args)
+        private void OnBuckled(EntityUid uid, BeingCarriedComponent component, ref BuckledEvent args)
         {
             DropCarried(component.Carrier, uid);
         }
diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs
index bbca6f4d153..5ba5845f087 100644
--- a/Content.Server/Chat/Systems/ChatSystem.cs
+++ b/Content.Server/Chat/Systems/ChatSystem.cs
@@ -43,7 +43,7 @@ namespace Content.Server.Chat.Systems;
 
 // Dear contributor. When I was introducing changes to this system only god and I knew what I was doing.
 // Now only god knows. Please don't touch this code ever again. If you do have to, increment this counter as a warning for others:
-// TOTAL_HOURS_WASTED_HERE_EE = 18
+// TOTAL_HOURS_WASTED_HERE_EE = 19
 
 // TODO refactor whatever active warzone this class and chatmanager have become
 /// <summary>
@@ -343,7 +343,7 @@ public void DispatchGlobalAnnouncement(
         _chatManager.ChatMessageToAll(ChatChannel.Radio, message, wrappedMessage, default, false, true, colorOverride);
         if (playSound)
         {
-            _audio.PlayGlobal(announcementSound?.GetSound() ?? DefaultAnnouncementSound, Filter.Broadcast(), true, AudioParams.Default.WithVolume(-2f));
+            _audio.PlayGlobal(announcementSound != null ? announcementSound.ToString() : DefaultAnnouncementSound, Filter.Broadcast(), true, AudioParams.Default.WithVolume(-2f));
         }
         _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Global station announcement from {sender}: {message}");
     }
@@ -381,7 +381,7 @@ public void DispatchStationAnnouncement(
 
         if (playDefaultSound)
         {
-            _audio.PlayGlobal(announcementSound?.GetSound() ?? DefaultAnnouncementSound, filter, true, AudioParams.Default.WithVolume(-2f));
+            _audio.PlayGlobal(announcementSound != null ? announcementSound.ToString() : DefaultAnnouncementSound, filter, true, AudioParams.Default.WithVolume(-2f));
         }
 
         _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Station Announcement on {station} from {sender}: {message}");
diff --git a/Content.Server/Chat/TelepathicChatSystem.cs b/Content.Server/Chat/TelepathicChatSystem.cs
index b1338035adb..e0844b3eee2 100644
--- a/Content.Server/Chat/TelepathicChatSystem.cs
+++ b/Content.Server/Chat/TelepathicChatSystem.cs
@@ -52,20 +52,22 @@ public override void Initialize()
     private IEnumerable<INetChannel> GetAdminClients()
     {
         return _adminManager.ActiveAdmins
-            .Select(p => p.ConnectedClient);
+            .Select(p => p.Channel);
     }
 
     private List<INetChannel> GetDreamers(IEnumerable<INetChannel> removeList)
     {
+        var filteredList = new List<INetChannel>();
         var filtered = Filter.Empty()
             .AddWhereAttachedEntity(entity =>
                 HasComp<PsionicComponent>(entity) && !HasComp<TelepathyComponent>(entity)
                 || HasComp<SleepingComponent>(entity)
                 || HasComp<SeeingRainbowsComponent>(entity) && !HasComp<PsionicsDisabledComponent>(entity) && !HasComp<PsionicInsulationComponent>(entity))
             .Recipients
-            .Select(p => p.ConnectedClient);
+            .Select(p => p.Channel);
 
-        var filteredList = filtered.ToList();
+        if (filtered.ToList() != null)
+            filteredList = filtered.ToList();
 
         foreach (var entity in removeList)
             filteredList.Remove(entity);
@@ -134,7 +136,7 @@ private string ObfuscateMessageReadability(string message, float chance)
 
         for (var i = 0; i < message.Length; i++)
         {
-            if (char.IsWhiteSpace((modifiedMessage[i])))
+            if (char.IsWhiteSpace(modifiedMessage[i]))
             {
                 continue;
             }
diff --git a/Content.Server/Chemistry/ReagentEffects/AdjustAlert.cs b/Content.Server/Chemistry/ReagentEffects/AdjustAlert.cs
index 8d475570ad0..40858176bd1 100644
--- a/Content.Server/Chemistry/ReagentEffects/AdjustAlert.cs
+++ b/Content.Server/Chemistry/ReagentEffects/AdjustAlert.cs
@@ -10,8 +10,8 @@ public sealed partial class AdjustAlert : ReagentEffect
     /// <summary>
     /// The specific Alert that will be adjusted
     /// </summary>
-    [DataField("alertType", required: true)]
-    public AlertType Type;
+    [DataField(required: true)]
+    public ProtoId<AlertPrototype> AlertType;
 
     /// <summary>
     /// If true, the alert is removed after Time seconds. If Time was not specified the alert is removed immediately.
@@ -42,7 +42,7 @@ public override void Effect(ReagentEffectArgs args)
 
         if (Clear && Time <= 0)
         {
-                alertSys.ClearAlert(args.SolutionEntity, Type);
+                alertSys.ClearAlert(args.SolutionEntity, AlertType);
         }
         else
         {
@@ -52,7 +52,7 @@ public override void Effect(ReagentEffectArgs args)
             if ((ShowCooldown || Clear) && Time > 0)
                 cooldown = (timing.CurTime, timing.CurTime + TimeSpan.FromSeconds(Time));
 
-            alertSys.ShowAlert(args.SolutionEntity, Type, cooldown: cooldown, autoRemove: Clear, showCooldown: ShowCooldown);
+            alertSys.ShowAlert(args.SolutionEntity, AlertType, cooldown: cooldown, autoRemove: Clear, showCooldown: ShowCooldown);
         }
 
     }
diff --git a/Content.Server/Clothing/MagbootsSystem.cs b/Content.Server/Clothing/MagbootsSystem.cs
index f12558389e3..3838ad168d1 100644
--- a/Content.Server/Clothing/MagbootsSystem.cs
+++ b/Content.Server/Clothing/MagbootsSystem.cs
@@ -29,11 +29,11 @@ protected override void UpdateMagbootEffects(EntityUid parent, EntityUid uid, bo
 
         if (state)
         {
-            _alerts.ShowAlert(parent, AlertType.Magboots);
+            _alerts.ShowAlert(parent, component.MagbootsAlert);
         }
         else
         {
-            _alerts.ClearAlert(parent, AlertType.Magboots);
+            _alerts.ClearAlert(parent, component.MagbootsAlert);
         }
     }
 
diff --git a/Content.Server/Clothing/Systems/ChameleonClothingSystem.cs b/Content.Server/Clothing/Systems/ChameleonClothingSystem.cs
index e20a6c3da97..feb3428884c 100644
--- a/Content.Server/Clothing/Systems/ChameleonClothingSystem.cs
+++ b/Content.Server/Clothing/Systems/ChameleonClothingSystem.cs
@@ -39,7 +39,7 @@ private void OnVerb(EntityUid uid, ChameleonClothingComponent component, GetVerb
         args.Verbs.Add(new InteractionVerb()
         {
             Text = Loc.GetString("chameleon-component-verb-text"),
-            Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")),
+            Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/settings.svg.192dpi.png")),
             Act = () => TryOpenUi(uid, args.User, component)
         });
     }
@@ -91,7 +91,7 @@ public void SetSelectedPrototype(EntityUid uid, string? protoId, bool forceUpdat
         UpdateIdentityBlocker(uid, component, proto);
         UpdateVisuals(uid, component);
         UpdateUi(uid, component);
-        Dirty(component);
+        Dirty(uid, component);
     }
 
     private void UpdateIdentityBlocker(EntityUid uid, ChameleonClothingComponent component, EntityPrototype proto)
diff --git a/Content.Server/Construction/PartExchangerSystem.cs b/Content.Server/Construction/PartExchangerSystem.cs
index f364d1b547d..c84d65b75e0 100644
--- a/Content.Server/Construction/PartExchangerSystem.cs
+++ b/Content.Server/Construction/PartExchangerSystem.cs
@@ -170,7 +170,12 @@ private void OnAfterInteract(EntityUid uid, PartExchangerComponent component, Af
             return;
         }
 
-        component.AudioStream = _audio.PlayPvs(component.ExchangeSound, uid).Value.Entity;
+        var audioStream = _audio.PlayPvs(component.ExchangeSound, uid);
+
+        if (audioStream == null)
+            return;
+
+        component.AudioStream = audioStream!.Value.Entity;
 
         _doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, args.User, component.ExchangeDuration, new ExchangerDoAfterEvent(), uid, target: args.Target, used: uid)
         {
diff --git a/Content.Server/Database/ServerDbBase.cs b/Content.Server/Database/ServerDbBase.cs
index 8286defd117..3b1f97f2635 100644
--- a/Content.Server/Database/ServerDbBase.cs
+++ b/Content.Server/Database/ServerDbBase.cs
@@ -998,30 +998,6 @@ public async Task RemoveFromWhitelistAsync(NetUserId player)
             await db.DbContext.SaveChangesAsync();
         }
 
-        public async Task<DateTimeOffset?> GetLastReadRules(NetUserId player)
-        {
-            await using var db = await GetDb();
-
-            return NormalizeDatabaseTime(await db.DbContext.Player
-                .Where(dbPlayer => dbPlayer.UserId == player)
-                .Select(dbPlayer => dbPlayer.LastReadRules)
-                .SingleOrDefaultAsync());
-        }
-
-        public async Task SetLastReadRules(NetUserId player, DateTimeOffset date)
-        {
-            await using var db = await GetDb();
-
-            var dbPlayer = await db.DbContext.Player.Where(dbPlayer => dbPlayer.UserId == player).SingleOrDefaultAsync();
-            if (dbPlayer == null)
-            {
-                return;
-            }
-
-            dbPlayer.LastReadRules = date.UtcDateTime;
-            await db.DbContext.SaveChangesAsync();
-        }
-
         #endregion
 
         #region Uploaded Resources Logs
diff --git a/Content.Server/Database/ServerDbManager.cs b/Content.Server/Database/ServerDbManager.cs
index a9c5e8c43ac..dee6248cc39 100644
--- a/Content.Server/Database/ServerDbManager.cs
+++ b/Content.Server/Database/ServerDbManager.cs
@@ -252,13 +252,6 @@ Task<int> AddConnectionLogAsync(
 
         #endregion
 
-        #region Rules
-
-        Task<DateTimeOffset?> GetLastReadRules(NetUserId player);
-        Task SetLastReadRules(NetUserId player, DateTimeOffset time);
-
-        #endregion
-
         #region Admin Notes
 
         Task<int> AddAdminNote(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, NoteSeverity severity, bool secret, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime);
@@ -707,18 +700,6 @@ public Task PurgeUploadedResourceLogAsync(int days)
             return RunDbCommand(() => _db.PurgeUploadedResourceLogAsync(days));
         }
 
-        public Task<DateTimeOffset?> GetLastReadRules(NetUserId player)
-        {
-            DbReadOpsMetric.Inc();
-            return RunDbCommand(() => _db.GetLastReadRules(player));
-        }
-
-        public Task SetLastReadRules(NetUserId player, DateTimeOffset time)
-        {
-            DbWriteOpsMetric.Inc();
-            return RunDbCommand(() => _db.SetLastReadRules(player, time));
-        }
-
         public Task<int> AddAdminNote(int? roundId, Guid player, TimeSpan playtimeAtNote, string message, NoteSeverity severity, bool secret, Guid createdBy, DateTimeOffset createdAt, DateTimeOffset? expiryTime)
         {
             DbWriteOpsMetric.Inc();
diff --git a/Content.Server/Decals/DecalSystem.cs b/Content.Server/Decals/DecalSystem.cs
index da95401d206..c8e062ce6ff 100644
--- a/Content.Server/Decals/DecalSystem.cs
+++ b/Content.Server/Decals/DecalSystem.cs
@@ -89,10 +89,11 @@ private void OnPvsToggle(bool value)
                 playerData.Clear();
             }
 
-            foreach (var (grid, meta) in EntityQuery<DecalGridComponent, MetaDataComponent>(true))
+            var query = EntityQueryEnumerator<DecalGridComponent, MetaDataComponent>();
+            while (query.MoveNext(out var uid, out var grid, out var meta))
             {
                 grid.ForceTick = _timing.CurTick;
-                Dirty(grid, meta);
+                Dirty(uid, grid, meta);
             }
         }
 
diff --git a/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs b/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs
index 65851f17360..093f05fceee 100644
--- a/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs
+++ b/Content.Server/Destructible/Thresholds/Behaviors/SpawnEntitiesBehavior.cs
@@ -1,12 +1,11 @@
 using System.Numerics;
 using Content.Shared.Forensics;
 using Content.Server.Stack;
+using Content.Shared.Destructible.Thresholds;
 using Content.Shared.Prototypes;
 using Content.Shared.Stacks;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
-using Content.Server.Administration.Commands;
 
 namespace Content.Server.Destructible.Thresholds.Behaviors
 {
@@ -17,8 +16,8 @@ public sealed partial class SpawnEntitiesBehavior : IThresholdBehavior
         /// <summary>
         ///     Entities spawned on reaching this threshold, from a min to a max.
         /// </summary>
-        [DataField("spawn", customTypeSerializer:typeof(PrototypeIdDictionarySerializer<MinMax, EntityPrototype>))]
-        public Dictionary<string, MinMax> Spawn { get; set; } = new();
+        [DataField]
+        public Dictionary<EntProtoId, MinMax> Spawn = new();
 
         [DataField("offset")]
         public float Offset { get; set; } = 0.5f;
diff --git a/Content.Server/Destructible/Thresholds/MinMax.cs b/Content.Server/Destructible/Thresholds/MinMax.cs
deleted file mode 100644
index c44864183ab..00000000000
--- a/Content.Server/Destructible/Thresholds/MinMax.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-using Robust.Shared.Random;
-
-namespace Content.Server.Destructible.Thresholds
-{
-    [Serializable]
-    [DataDefinition]
-    public partial struct MinMax
-    {
-        [DataField("min")]
-        public int Min;
-
-        [DataField("max")]
-        public int Max;
-
-        public MinMax(int min, int max)
-        {
-            Min = min;
-            Max = max;
-        }
-
-        public int Next(IRobustRandom random)
-        {
-            return random.Next(Min, Max + 1);
-        }
-    }
-}
diff --git a/Content.Server/DeviceNetwork/Systems/NetworkConfiguratorSystem.cs b/Content.Server/DeviceNetwork/Systems/NetworkConfiguratorSystem.cs
index dc1bf499df5..34601241585 100644
--- a/Content.Server/DeviceNetwork/Systems/NetworkConfiguratorSystem.cs
+++ b/Content.Server/DeviceNetwork/Systems/NetworkConfiguratorSystem.cs
@@ -254,7 +254,7 @@ private void SetMode(EntityUid configuratorUid, NetworkConfiguratorComponent con
     /// </summary>
     private void UpdateModeAppearance(EntityUid userUid, EntityUid configuratorUid, NetworkConfiguratorComponent configurator)
     {
-        Dirty(configurator);
+        Dirty(configuratorUid, configurator);
         _appearanceSystem.SetData(configuratorUid, NetworkConfiguratorVisuals.Mode, configurator.LinkModeActive);
 
         var pitch = configurator.LinkModeActive ? 1 : 0.8f;
diff --git a/Content.Server/Disposal/Tube/DisposalTubeSystem.cs b/Content.Server/Disposal/Tube/DisposalTubeSystem.cs
index f0f6e9142c6..5a800d5c0eb 100644
--- a/Content.Server/Disposal/Tube/DisposalTubeSystem.cs
+++ b/Content.Server/Disposal/Tube/DisposalTubeSystem.cs
@@ -31,6 +31,9 @@ public sealed class DisposalTubeSystem : EntitySystem
         [Dependency] private readonly DisposableSystem _disposableSystem = default!;
         [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
         [Dependency] private readonly AtmosphereSystem _atmosSystem = default!;
+        [Dependency] private readonly SharedTransformSystem _transformSystem = default!;
+        [Dependency] private readonly SharedMapSystem _mapSystem = default!;
+
         public override void Initialize()
         {
             base.Initialize();
@@ -344,22 +347,18 @@ private void UpdateAnchored(EntityUid uid, DisposalTubeComponent component, bool
                 return null;
 
             var position = xform.Coordinates;
-            foreach (var entity in grid.GetInDir(position, nextDirection))
+            var entities = _mapSystem.GetInDir(target, grid, position, nextDirection);
+
+            foreach (var entity in entities)
             {
                 if (!TryComp(entity, out DisposalTubeComponent? tube))
-                {
                     continue;
-                }
 
                 if (!CanConnect(entity, tube, oppositeDirection))
-                {
                     continue;
-                }
 
                 if (!CanConnect(target, targetTube, nextDirection))
-                {
                     continue;
-                }
 
                 return entity;
             }
@@ -422,7 +421,8 @@ public bool TryInsert(EntityUid uid, DisposalUnitComponent from, IEnumerable<str
                 return false;
 
             var xform = Transform(uid);
-            var holder = Spawn(DisposalEntryComponent.HolderPrototypeId, xform.MapPosition);
+            var mapCoords = _transformSystem.GetMapCoordinates(xform);
+            var holder = Spawn(DisposalEntryComponent.HolderPrototypeId, mapCoords);
             var holderComponent = Comp<DisposalHolderComponent>(holder);
 
             foreach (var entity in from.Container.ContainedEntities.ToArray())
diff --git a/Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs b/Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs
index 3e81ebfb79f..3c7636cfd07 100644
--- a/Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs
+++ b/Content.Server/Disposal/Unit/EntitySystems/DisposalUnitSystem.cs
@@ -54,6 +54,7 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
     [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
     [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
     [Dependency] private readonly TransformSystem _transformSystem = default!;
+    [Dependency] private readonly SharedMapSystem _sharedMapSystem = default!;
     [Dependency] private readonly UserInterfaceSystem _ui = default!;
 
     public override void Initialize()
@@ -310,7 +311,7 @@ private void OnPowerChange(EntityUid uid, SharedDisposalUnitComponent component,
         if (!args.Powered)
         {
             component.NextFlush = null;
-            Dirty(component);
+            Dirty(uid, component);
             return;
         }
 
@@ -366,7 +367,7 @@ private void UpdateState(EntityUid uid, DisposalsPressureState state, SharedDisp
         component.State = state;
         UpdateVisualState(uid, component);
         UpdateInterface(uid, component, component.Powered);
-        Dirty(component, metadata);
+        Dirty(uid, component, metadata);
 
         if (state == DisposalsPressureState.Ready)
         {
@@ -447,7 +448,7 @@ private void Update(EntityUid uid, SharedDisposalUnitComponent component, MetaDa
         }
 
         if (count != component.RecentlyEjected.Count)
-            Dirty(component, metadata);
+            Dirty(uid, component, metadata);
     }
 
     public bool TryInsert(EntityUid unitId, EntityUid toInsertId, EntityUid? userId, DisposalUnitComponent? unit = null)
@@ -516,7 +517,7 @@ public bool TryFlush(EntityUid uid, SharedDisposalUnitComponent component)
             return false;
 
         var coords = xform.Coordinates;
-        var entry = grid.GetLocal(coords)
+        var entry = _sharedMapSystem.GetLocal(uid, grid, coords)
             .FirstOrDefault(HasComp<DisposalEntryComponent>);
 
         if (entry == default || component is not DisposalUnitComponent sDisposals)
@@ -754,7 +755,7 @@ public void QueueAutomaticEngage(EntityUid uid, SharedDisposalUnitComponent comp
         var flushTime = TimeSpan.FromSeconds(Math.Min((component.NextFlush ?? TimeSpan.MaxValue).TotalSeconds, automaticTime.TotalSeconds));
 
         component.NextFlush = flushTime;
-        Dirty(component);
+        Dirty(uid, component);
     }
 
     public void AfterInsert(EntityUid uid, SharedDisposalUnitComponent component, EntityUid inserted, EntityUid? user = null, bool doInsert = false)
diff --git a/Content.Server/Dragon/DragonRiftSystem.cs b/Content.Server/Dragon/DragonRiftSystem.cs
index c0a81d0d24e..b0dd87d3fdb 100644
--- a/Content.Server/Dragon/DragonRiftSystem.cs
+++ b/Content.Server/Dragon/DragonRiftSystem.cs
@@ -70,7 +70,7 @@ public override void Update(float frameTime)
             if (comp.State < DragonRiftState.AlmostFinished && comp.Accumulator > comp.MaxAccumulator / 2f)
             {
                 comp.State = DragonRiftState.AlmostFinished;
-                Dirty(comp);
+                Dirty(uid, comp);
 
                 _announcer.SendAnnouncement(_announcer.GetAnnouncementId("CarpRift"), Filter.Broadcast(),
                     "carp-rift-warning", colorOverride: Color.Red, localeArgs: ("location", FormattedMessage.RemoveMarkupPermissive(_navMap.GetNearestBeaconString((uid, xform)))));
diff --git a/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs b/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs
index 202d03bcda9..5117d67e944 100644
--- a/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs
+++ b/Content.Server/Ensnaring/EnsnareableSystem.Ensnaring.cs
@@ -93,7 +93,7 @@ public void TryEnsnare(EntityUid target, EntityUid ensnare, EnsnaringComponent c
         component.Ensnared = target;
         _container.Insert(ensnare, ensnareable.Container);
         ensnareable.IsEnsnared = true;
-        Dirty(ensnareable);
+        Dirty(target, ensnareable);
 
         UpdateAlert(target, ensnareable);
         var ev = new EnsnareEvent(component.WalkSpeed, component.SprintSpeed);
@@ -107,7 +107,7 @@ public void TryEnsnare(EntityUid target, EntityUid ensnare, EnsnaringComponent c
     /// <param name="user">The entity that is freeing the target</param>
     /// <param name="ensnare">The entity used to ensnare</param>
     /// <param name="component">The ensnaring component</param>
-    public void TryFree(EntityUid target,  EntityUid user, EntityUid ensnare, EnsnaringComponent component)
+    public void TryFree(EntityUid target, EntityUid user, EntityUid ensnare, EnsnaringComponent component)
     {
         //Don't do anything if they don't have the ensnareable component.
         if (!HasComp<EnsnareableComponent>(target))
@@ -149,7 +149,7 @@ public void ForceFree(EntityUid ensnare, EnsnaringComponent component)
 
         _container.Remove(ensnare, ensnareable.Container, force: true);
         ensnareable.IsEnsnared = ensnareable.Container.ContainedEntities.Count > 0;
-        Dirty(ensnareable);
+        Dirty(component.Ensnared.Value, ensnareable);
         component.Ensnared = null;
 
         UpdateAlert(target, ensnareable);
@@ -164,8 +164,8 @@ public void ForceFree(EntityUid ensnare, EnsnaringComponent component)
     public void UpdateAlert(EntityUid target, EnsnareableComponent component)
     {
         if (!component.IsEnsnared)
-            _alerts.ClearAlert(target, AlertType.Ensnared);
+            _alerts.ClearAlert(target, component.EnsnaredAlert);
         else
-            _alerts.ShowAlert(target, AlertType.Ensnared);
+            _alerts.ShowAlert(target, component.EnsnaredAlert);
     }
 }
diff --git a/Content.Server/Ensnaring/EnsnareableSystem.cs b/Content.Server/Ensnaring/EnsnareableSystem.cs
index 7b810b4f49c..0cf4efa21b2 100644
--- a/Content.Server/Ensnaring/EnsnareableSystem.cs
+++ b/Content.Server/Ensnaring/EnsnareableSystem.cs
@@ -45,7 +45,7 @@ private void OnDoAfter(EntityUid uid, EnsnareableComponent component, DoAfterEve
         }
 
         component.IsEnsnared = component.Container.ContainedEntities.Count > 0;
-        Dirty(component);
+        Dirty(uid, component);
         ensnaring.Ensnared = null;
 
         if (ensnaring.DestroyOnRemove)
diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs
index dda783c4325..dd8cdd0ed08 100644
--- a/Content.Server/Entry/EntryPoint.cs
+++ b/Content.Server/Entry/EntryPoint.cs
@@ -12,7 +12,6 @@
 using Content.Server.GameTicking;
 using Content.Server.GhostKick;
 using Content.Server.GuideGenerator;
-using Content.Server.Info;
 using Content.Server.IoC;
 using Content.Server.Players.JobWhitelist;
 using Content.Server.Maps;
@@ -141,7 +140,6 @@ public override void PostInit()
                 IoCManager.Resolve<RecipeManager>().Initialize();
                 IoCManager.Resolve<IAdminManager>().Initialize();
                 IoCManager.Resolve<IAfkManager>().Initialize();
-                IoCManager.Resolve<RulesManager>().Initialize();
                 _euiManager.Initialize();
 
                 IoCManager.Resolve<IGameMapManager>().Initialize();
diff --git a/Content.Server/GameTicking/GameTicker.GameRule.cs b/Content.Server/GameTicking/GameTicker.GameRule.cs
index f52a3cb296d..9b0ca6b2bbc 100644
--- a/Content.Server/GameTicking/GameTicker.GameRule.cs
+++ b/Content.Server/GameTicking/GameTicker.GameRule.cs
@@ -1,8 +1,9 @@
 using System.Linq;
 using Content.Server.Administration;
-using Content.Server.GameTicking.Components;
+using Content.Server.GameTicking.Rules.Components;
 using Content.Shared.Administration;
 using Content.Shared.Database;
+using Content.Shared.GameTicking.Components;
 using Content.Shared.Prototypes;
 using JetBrains.Annotations;
 using Robust.Shared.Console;
diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs
index 3016195f4b7..c6c8bf50f75 100644
--- a/Content.Server/GameTicking/GameTicker.Spawning.cs
+++ b/Content.Server/GameTicking/GameTicker.Spawning.cs
@@ -460,11 +460,16 @@ private EntityUid SpawnObserverMob()
         public EntityCoordinates GetObserverSpawnPoint()
         {
             _possiblePositions.Clear();
-
-            foreach (var (point, transform) in EntityManager.EntityQuery<SpawnPointComponent, TransformComponent>(true))
+            var spawnPointQuery = EntityManager.EntityQueryEnumerator<SpawnPointComponent, TransformComponent>();
+            while (spawnPointQuery.MoveNext(out var uid, out var point, out var transform))
             {
-                if (point.SpawnType != SpawnPointType.Observer)
+                if (point.SpawnType != SpawnPointType.Observer
+                   || TerminatingOrDeleted(uid)
+                   || transform.MapUid == null
+                   || TerminatingOrDeleted(transform.MapUid.Value))
+                {
                     continue;
+                }
 
                 _possiblePositions.Add(transform.Coordinates);
             }
@@ -506,7 +511,9 @@ public EntityCoordinates GetObserverSpawnPoint()
 
             if (_mapManager.MapExists(DefaultMap))
             {
-                return new EntityCoordinates(_mapManager.GetMapEntityId(DefaultMap), Vector2.Zero);
+                var mapUid = _mapManager.GetMapEntityId(DefaultMap);
+                if (!TerminatingOrDeleted(mapUid))
+                    return new EntityCoordinates(mapUid, Vector2.Zero);
             }
 
             // Just pick a point at this point I guess.
diff --git a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs
index 78b8a8a85c8..d03d040261a 100644
--- a/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/DeathMatchRuleSystem.cs
@@ -1,12 +1,12 @@
 using System.Linq;
 using Content.Server.Administration.Commands;
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.KillTracking;
 using Content.Server.Mind;
 using Content.Server.Points;
 using Content.Server.RoundEnd;
 using Content.Server.Station.Systems;
+using Content.Shared.GameTicking.Components;
 using Content.Shared.Points;
 using Content.Shared.Storage;
 using Robust.Server.Player;
diff --git a/Content.Server/GameTicking/Rules/GameRulePrototype.cs b/Content.Server/GameTicking/Rules/GameRulePrototype.cs
deleted file mode 100644
index 47f99184f73..00000000000
--- a/Content.Server/GameTicking/Rules/GameRulePrototype.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-namespace Content.Server.GameTicking.Rules;
-
-/*
-[Prototype("gameRule")]
-public sealed partial class GameRulePrototype : IPrototype
-{
-    [IdDataField]
-    public string ID { get; private set; } = default!;
-
-    [DataField("config", required: true)]
-    public GameRuleConfiguration Configuration { get; private set; } = default!;
-}
-*/
diff --git a/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs b/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs
index cbd981e99e6..b72ba59ca27 100644
--- a/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs
+++ b/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs
@@ -1,8 +1,8 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Station.Components;
+using Content.Shared.GameTicking.Components;
 using Content.Shared.Random.Helpers;
 using Robust.Server.GameObjects;
 using Robust.Shared.Collections;
diff --git a/Content.Server/GameTicking/Rules/GameRuleSystem.cs b/Content.Server/GameTicking/Rules/GameRuleSystem.cs
index 05374aa1396..730748ce6b9 100644
--- a/Content.Server/GameTicking/Rules/GameRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/GameRuleSystem.cs
@@ -1,6 +1,6 @@
 using Content.Server.Atmos.EntitySystems;
 using Content.Server.Chat.Managers;
-using Content.Server.GameTicking.Components;
+using Content.Shared.GameTicking.Components;
 using Robust.Server.GameObjects;
 using Robust.Shared.Random;
 using Robust.Shared.Timing;
diff --git a/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs b/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs
index 01fa387595c..e56537c4381 100644
--- a/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/InactivityTimeRestartRuleSystem.cs
@@ -1,7 +1,7 @@
 using System.Threading;
 using Content.Server.Chat.Managers;
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
+using Content.Shared.GameTicking.Components;
 using Robust.Server.Player;
 using Robust.Shared.Player;
 using Timer = Robust.Shared.Timing.Timer;
diff --git a/Content.Server/GameTicking/Rules/KillCalloutRuleSystem.cs b/Content.Server/GameTicking/Rules/KillCalloutRuleSystem.cs
index 3da55e30c9e..8f706fd2ad3 100644
--- a/Content.Server/GameTicking/Rules/KillCalloutRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/KillCalloutRuleSystem.cs
@@ -1,8 +1,8 @@
 using Content.Server.Chat.Managers;
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.KillTracking;
 using Content.Shared.Chat;
+using Content.Shared.GameTicking.Components;
 using Robust.Server.Player;
 using Robust.Shared.Player;
 using Robust.Shared.Random;
diff --git a/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs b/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs
index 3a80d82fd92..e655bd472c6 100644
--- a/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/LoadMapRuleSystem.cs
@@ -1,8 +1,8 @@
 using Content.Server.Antag;
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
-using Content.Server.GridPreloader;
 using Content.Server.Spawners.Components;
+using Content.Server.GridPreloader;
+using Content.Shared.GameTicking.Components;
 using Robust.Server.GameObjects;
 using Robust.Server.Maps;
 using Robust.Shared.Map;
diff --git a/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs b/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs
index cae99fee9fc..db9df8a5b00 100644
--- a/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/MaxTimeRestartRuleSystem.cs
@@ -1,7 +1,7 @@
 using System.Threading;
 using Content.Server.Chat.Managers;
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
+using Content.Shared.GameTicking.Components;
 using Timer = Robust.Shared.Timing.Timer;
 
 namespace Content.Server.GameTicking.Rules;
diff --git a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs
index 3e61fda8744..7cc51db5766 100644
--- a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs
@@ -2,6 +2,8 @@
 using Content.Server.Communications;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Humanoid;
+using Content.Server.NPC.Components;
+using Content.Server.NPC.Systems;
 using Content.Server.Nuke;
 using Content.Server.NukeOps;
 using Content.Server.Popups;
@@ -13,6 +15,7 @@
 using Content.Server.Station.Components;
 using Content.Server.Store.Components;
 using Content.Server.Store.Systems;
+using Content.Shared.GameTicking.Components;
 using Content.Shared.Humanoid;
 using Content.Shared.Humanoid.Prototypes;
 using Content.Shared.Mobs;
@@ -28,9 +31,6 @@
 using Robust.Shared.Random;
 using Robust.Shared.Utility;
 using System.Linq;
-using Content.Server.GameTicking.Components;
-using Content.Server.NPC.Components;
-using Content.Server.NPC.Systems;
 
 namespace Content.Server.GameTicking.Rules;
 
diff --git a/Content.Server/GameTicking/Rules/RespawnRuleSystem.cs b/Content.Server/GameTicking/Rules/RespawnRuleSystem.cs
index 5215da96aa8..920668472ad 100644
--- a/Content.Server/GameTicking/Rules/RespawnRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/RespawnRuleSystem.cs
@@ -1,8 +1,9 @@
 using Content.Server.Chat.Managers;
-using Content.Server.GameTicking.Components;
+using Content.Server.Database.Migrations.Postgres;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Station.Systems;
 using Content.Shared.Chat;
+using Content.Shared.GameTicking.Components;
 using Content.Shared.Interaction.Events;
 using Content.Shared.Mind;
 using Content.Shared.Mobs;
diff --git a/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs b/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs
index 577b0c30303..29f69db14a2 100644
--- a/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/RevolutionaryRuleSystem.cs
@@ -14,6 +14,7 @@
 using Content.Server.Shuttles.Systems;
 using Content.Server.Station.Systems;
 using Content.Shared.Database;
+using Content.Shared.GameTicking.Components;
 using Content.Shared.Humanoid;
 using Content.Shared.IdentityManagement;
 using Content.Shared.Mind;
@@ -27,7 +28,6 @@
 using Content.Shared.Zombies;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Timing;
-using Content.Server.GameTicking.Components;
 using Content.Shared.Cuffs.Components;
 
 namespace Content.Server.GameTicking.Rules;
diff --git a/Content.Server/GameTicking/Rules/RoundstartStationVariationRuleSystem.cs b/Content.Server/GameTicking/Rules/RoundstartStationVariationRuleSystem.cs
index f09ed3ebc3c..570889155b3 100644
--- a/Content.Server/GameTicking/Rules/RoundstartStationVariationRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/RoundstartStationVariationRuleSystem.cs
@@ -1,9 +1,9 @@
 using System.Linq;
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Shuttles.Systems;
 using Content.Server.Station.Components;
 using Content.Server.Station.Events;
+using Content.Shared.GameTicking.Components;
 using Content.Shared.Storage;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
diff --git a/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs b/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs
index c60670a3ad7..23e9ee5a7d2 100644
--- a/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/SandboxRuleSystem.cs
@@ -1,6 +1,6 @@
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Sandbox;
+using Content.Shared.GameTicking.Components;
 
 namespace Content.Server.GameTicking.Rules;
 
diff --git a/Content.Server/GameTicking/Rules/SecretRuleSystem.cs b/Content.Server/GameTicking/Rules/SecretRuleSystem.cs
index 95bf5986a5a..3542b2e0864 100644
--- a/Content.Server/GameTicking/Rules/SecretRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/SecretRuleSystem.cs
@@ -1,10 +1,10 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using Content.Server.Administration.Logs;
-using Content.Server.GameTicking.Components;
 using Content.Server.Chat.Managers;
 using Content.Server.GameTicking.Presets;
 using Content.Server.GameTicking.Rules.Components;
+using Content.Shared.GameTicking.Components;
 using Content.Shared.Random;
 using Content.Shared.CCVar;
 using Content.Shared.Database;
diff --git a/Content.Server/GameTicking/Rules/SubGamemodesSystem.cs b/Content.Server/GameTicking/Rules/SubGamemodesSystem.cs
index 4486ee40fbb..4fe3827ce5c 100644
--- a/Content.Server/GameTicking/Rules/SubGamemodesSystem.cs
+++ b/Content.Server/GameTicking/Rules/SubGamemodesSystem.cs
@@ -1,5 +1,5 @@
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
+using Content.Shared.GameTicking.Components;
 using Content.Shared.Storage;
 
 namespace Content.Server.GameTicking.Rules;
diff --git a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs
index 1db288799f2..3d0a02d6aa9 100644
--- a/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/TraitorRuleSystem.cs
@@ -5,8 +5,11 @@
 using Content.Server.Objectives;
 using Content.Server.PDA.Ringer;
 using Content.Server.Roles;
+using Content.Server.Traitor.Components;
 using Content.Server.Traitor.Uplink;
+using Content.Shared.GameTicking.Components;
 using Content.Shared.Mind;
+using Content.Shared.Mood;
 using Content.Shared.Objectives.Components;
 using Content.Shared.PDA;
 using Content.Shared.Roles;
@@ -15,9 +18,6 @@
 using Robust.Shared.Random;
 using System.Linq;
 using System.Text;
-using Content.Shared.Mood;
-using Content.Server.GameTicking.Components;
-using Content.Server.Traitor.Components;
 
 namespace Content.Server.GameTicking.Rules;
 
diff --git a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs
index 1361ab37338..6c96bfd18c7 100644
--- a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs
+++ b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs
@@ -1,4 +1,5 @@
 using Content.Server.Antag;
+using Content.Server.Announcements.Systems;
 using Content.Server.Chat.Systems;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Popups;
@@ -6,6 +7,7 @@
 using Content.Server.Station.Components;
 using Content.Server.Station.Systems;
 using Content.Server.Zombies;
+using Content.Shared.GameTicking.Components;
 using Content.Shared.Humanoid;
 using Content.Shared.Mind;
 using Content.Shared.Mobs;
@@ -15,8 +17,6 @@
 using Robust.Shared.Player;
 using Robust.Shared.Timing;
 using System.Globalization;
-using Content.Server.Announcements.Systems;
-using Content.Server.GameTicking.Components;
 
 namespace Content.Server.GameTicking.Rules;
 
diff --git a/Content.Server/Geras/GerasComponent.cs b/Content.Server/Geras/GerasComponent.cs
index eaf792502f4..df1fccd7185 100644
--- a/Content.Server/Geras/GerasComponent.cs
+++ b/Content.Server/Geras/GerasComponent.cs
@@ -12,7 +12,7 @@ public sealed partial class GerasComponent : Component
 {
     [DataField] public ProtoId<PolymorphPrototype> GerasPolymorphId = "SlimeMorphGeras";
 
-    [DataField] public ProtoId<EntityPrototype> GerasAction = "ActionMorphGeras";
+    [DataField] public EntProtoId GerasAction = "ActionMorphGeras";
 
     [DataField] public EntityUid? GerasActionEntity;
 }
diff --git a/Content.Server/Ghost/GhostSystem.cs b/Content.Server/Ghost/GhostSystem.cs
index 254d478bb0f..effed5cdbe1 100644
--- a/Content.Server/Ghost/GhostSystem.cs
+++ b/Content.Server/Ghost/GhostSystem.cs
@@ -409,23 +409,41 @@ public bool DoGhostBooEvent(EntityUid target)
             return SpawnGhost(mind, spawnPosition, canReturn);
         }
 
+        private bool IsValidSpawnPosition(EntityCoordinates? spawnPosition)
+        {
+            if (spawnPosition?.IsValid(EntityManager) != true)
+                return false;
+
+            var mapUid = spawnPosition?.GetMapUid(EntityManager);
+            var gridUid = spawnPosition?.EntityId;
+            // Test if the map is being deleted
+            if (mapUid == null || TerminatingOrDeleted(mapUid.Value))
+                return false;
+            // Test if the grid is being deleted
+            if (gridUid != null && TerminatingOrDeleted(gridUid.Value))
+                return false;
+
+            return true;
+        }
+
         public EntityUid? SpawnGhost(Entity<MindComponent?> mind, EntityCoordinates? spawnPosition = null,
             bool canReturn = false)
         {
             if (!Resolve(mind, ref mind.Comp))
                 return null;
 
-            // Test if the map is being deleted
-            var mapUid = spawnPosition?.GetMapUid(EntityManager);
-            if (mapUid == null || TerminatingOrDeleted(mapUid.Value))
+            // Test if the map or grid is being deleted
+            if (!IsValidSpawnPosition(spawnPosition))
                 spawnPosition = null;
 
+            // If it's bad, look for a valid point to spawn
             spawnPosition ??= _ticker.GetObserverSpawnPoint();
 
-            if (!spawnPosition.Value.IsValid(EntityManager))
+            // Make sure the new point is valid too
+            if (!IsValidSpawnPosition(spawnPosition))
             {
                 Log.Warning($"No spawn valid ghost spawn position found for {mind.Comp.CharacterName}"
-                    + " \"{ToPrettyString(mind)}\"");
+                    + $" \"{ToPrettyString(mind)}\"");
                 _minds.TransferTo(mind.Owner, null, createGhost: false, mind: mind.Comp);
                 return null;
             }
diff --git a/Content.Server/Gravity/GravitySystem.cs b/Content.Server/Gravity/GravitySystem.cs
index 5e0332ae491..ea62d4a8195 100644
--- a/Content.Server/Gravity/GravitySystem.cs
+++ b/Content.Server/Gravity/GravitySystem.cs
@@ -41,7 +41,7 @@ public void RefreshGravity(EntityUid uid, GravityComponent? gravity = null)
                 gravity.Enabled = enabled;
                 var ev = new GravityChangedEvent(uid, enabled);
                 RaiseLocalEvent(uid, ref ev, true);
-                Dirty(gravity);
+                Dirty(uid, gravity);
 
                 if (HasComp<MapGridComponent>(uid))
                 {
@@ -71,7 +71,7 @@ public void EnableGravity(EntityUid uid, GravityComponent? gravity = null)
             gravity.Enabled = true;
             var ev = new GravityChangedEvent(uid, true);
             RaiseLocalEvent(uid, ref ev, true);
-            Dirty(gravity);
+            Dirty(uid, gravity);
 
             if (HasComp<MapGridComponent>(uid))
             {
diff --git a/Content.Server/Holiday/Christmas/RandomGiftSystem.cs b/Content.Server/Holiday/Christmas/RandomGiftSystem.cs
index 9e56d0a4937..48af65cb378 100644
--- a/Content.Server/Holiday/Christmas/RandomGiftSystem.cs
+++ b/Content.Server/Holiday/Christmas/RandomGiftSystem.cs
@@ -93,7 +93,7 @@ private void BuildIndex()
 
         foreach (var proto in _prototype.EnumeratePrototypes<EntityPrototype>())
         {
-            if (proto.Abstract || proto.NoSpawn || proto.Components.ContainsKey(mapGridCompName) || !proto.Components.ContainsKey(physicsCompName))
+            if (proto.Abstract || proto.HideSpawnMenu || proto.Components.ContainsKey(mapGridCompName) || !proto.Components.ContainsKey(physicsCompName))
                 continue;
 
             _possibleGiftsUnsafe.Add(proto.ID);
diff --git a/Content.Server/HotPotato/HotPotatoSystem.cs b/Content.Server/HotPotato/HotPotatoSystem.cs
index 8091eea6fdd..115a7b6cb76 100644
--- a/Content.Server/HotPotato/HotPotatoSystem.cs
+++ b/Content.Server/HotPotato/HotPotatoSystem.cs
@@ -29,7 +29,7 @@ private void OnActiveTimer(EntityUid uid, HotPotatoComponent comp, ref ActiveTim
         comp.CanTransfer = false;
         _ambientSound.SetAmbience(uid, true);
         _damageOnHolding.SetEnabled(uid, true);
-        Dirty(comp);
+        Dirty(uid, comp);
     }
 
     private void OnMeleeHit(EntityUid uid, HotPotatoComponent comp, MeleeHitEvent args)
@@ -56,6 +56,6 @@ private void OnMeleeHit(EntityUid uid, HotPotatoComponent comp, MeleeHitEvent ar
             break;
         }
         comp.CanTransfer = false;
-        Dirty(comp);
+        Dirty(uid, comp);
     }
 }
diff --git a/Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.Modifier.cs b/Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.Modifier.cs
index 06225c9d57c..7744d161519 100644
--- a/Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.Modifier.cs
+++ b/Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.Modifier.cs
@@ -29,7 +29,7 @@ private void OnVerbsRequest(EntityUid uid, HumanoidAppearanceComponent component
         {
             Text = "Modify markings",
             Category = VerbCategory.Tricks,
-            Icon = new SpriteSpecifier.Rsi(new ("/Textures/Mobs/Customization/reptilian_parts.rsi"), "tail_smooth"),
+            Icon = new SpriteSpecifier.Rsi(new("/Textures/Mobs/Customization/reptilian_parts.rsi"), "tail_smooth"),
             Act = () =>
             {
                 _uiSystem.OpenUi(uid, HumanoidMarkingModifierKey.Key, actor.PlayerSession);
@@ -62,7 +62,7 @@ private void OnBaseLayersSet(EntityUid uid, HumanoidAppearanceComponent componen
             component.CustomBaseLayers[message.Layer] = message.Info.Value;
         }
 
-        Dirty(component);
+        Dirty(uid, component);
 
         if (message.ResendState)
         {
@@ -86,7 +86,7 @@ private void OnMarkingsSet(EntityUid uid, HumanoidAppearanceComponent component,
         }
 
         component.MarkingSet = message.MarkingSet;
-        Dirty(component);
+        Dirty(uid, component);
 
         if (message.ResendState)
         {
diff --git a/Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.cs b/Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.cs
index ed6d91f56c7..1811567d270 100644
--- a/Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.cs
+++ b/Content.Server/Humanoid/Systems/HumanoidAppearanceSystem.cs
@@ -53,9 +53,8 @@ public void CloneAppearance(EntityUid source, EntityUid target, HumanoidAppearan
             grammar.Gender = sourceHumanoid.Gender;
         }
 
-        targetHumanoid.LastProfileLoaded = sourceHumanoid.LastProfileLoaded; // DeltaV - let paradox anomaly be cloned
-
-        Dirty(targetHumanoid);
+        targetHumanoid.LastProfileLoaded = sourceHumanoid.LastProfileLoaded;
+        Dirty(target, targetHumanoid);
     }
 
     /// <summary>
@@ -76,7 +75,7 @@ public void RemoveMarking(EntityUid uid, string marking, bool sync = true, Human
         humanoid.MarkingSet.Remove(prototype.MarkingCategory, marking);
 
         if (sync)
-            Dirty(humanoid);
+            Dirty(uid, humanoid);
     }
 
     /// <summary>
@@ -97,7 +96,7 @@ public void RemoveMarking(EntityUid uid, MarkingCategories category, int index,
         }
 
         humanoid.MarkingSet.Remove(category, index);
-        Dirty(humanoid);
+        Dirty(uid, humanoid);
     }
 
     /// <summary>
@@ -126,7 +125,7 @@ public void SetMarkingId(EntityUid uid, MarkingCategories category, int index, s
         }
 
         humanoid.MarkingSet.Replace(category, index, marking);
-        Dirty(humanoid);
+        Dirty(uid, humanoid);
     }
 
     /// <summary>
@@ -153,6 +152,6 @@ public void SetMarkingColor(EntityUid uid, MarkingCategories category, int index
             markings[index].SetColor(i, colors[i]);
         }
 
-        Dirty(humanoid);
+        Dirty(uid, humanoid);
     }
 }
diff --git a/Content.Server/Info/InfoSystem.cs b/Content.Server/Info/InfoSystem.cs
deleted file mode 100644
index 350ae033cfd..00000000000
--- a/Content.Server/Info/InfoSystem.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using Content.Shared.CCVar;
-using Content.Shared.Info;
-using Robust.Shared.Configuration;
-using Robust.Shared.ContentPack;
-
-namespace Content.Server.Info;
-
-public sealed class InfoSystem : EntitySystem
-{
-    [Dependency] private readonly IResourceManager _res = default!;
-    [Dependency] private readonly IConfigurationManager _cfg = default!;
-    public override void Initialize()
-    {
-        base.Initialize();
-        SubscribeNetworkEvent<RequestRulesMessage>(OnRequestRules);
-    }
-
-    private void OnRequestRules(RequestRulesMessage message, EntitySessionEventArgs eventArgs)
-    {
-        Log.Debug("Client requested rules.");
-        var title = Loc.GetString(_cfg.GetCVar(CCVars.RulesHeader));
-        var path = _cfg.GetCVar(CCVars.RulesFile);
-        var rules = "Server could not read its rules.";
-        try
-        {
-            rules = _res.ContentFileReadAllText($"/ServerInfo/{path}");
-        }
-        catch (Exception)
-        {
-            Log.Debug("Could not read server rules file.");
-        }
-        var response = new RulesMessage(title, rules);
-        RaiseNetworkEvent(response, eventArgs.SenderSession.Channel);
-    }
-}
diff --git a/Content.Server/Info/RulesManager.cs b/Content.Server/Info/RulesManager.cs
deleted file mode 100644
index d9d744dcbd5..00000000000
--- a/Content.Server/Info/RulesManager.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-using System.Net;
-using Content.Server.Database;
-using Content.Shared.CCVar;
-using Content.Shared.Info;
-using Robust.Shared.Configuration;
-using Robust.Shared.Network;
-
-namespace Content.Server.Info;
-
-public sealed class RulesManager : SharedRulesManager
-{
-    [Dependency] private readonly IServerDbManager _dbManager = default!;
-    [Dependency] private readonly INetManager _netManager = default!;
-    [Dependency] private readonly IConfigurationManager _cfg = default!;
-
-    private static DateTime LastValidReadTime => DateTime.UtcNow - TimeSpan.FromDays(60);
-
-    public void Initialize()
-    {
-        _netManager.RegisterNetMessage<ShouldShowRulesPopupMessage>();
-        _netManager.RegisterNetMessage<ShowRulesPopupMessage>();
-        _netManager.RegisterNetMessage<RulesAcceptedMessage>(OnRulesAccepted);
-        _netManager.Connected += OnConnected;
-    }
-
-    private async void OnConnected(object? sender, NetChannelArgs e)
-    {
-        if (IPAddress.IsLoopback(e.Channel.RemoteEndPoint.Address) && _cfg.GetCVar(CCVars.RulesExemptLocal))
-        {
-            return;
-        }
-
-        var lastRead = await _dbManager.GetLastReadRules(e.Channel.UserId);
-        if (lastRead > LastValidReadTime)
-        {
-            return;
-        }
-
-        var message = new ShouldShowRulesPopupMessage();
-        _netManager.ServerSendMessage(message, e.Channel);
-    }
-
-    private async void OnRulesAccepted(RulesAcceptedMessage message)
-    {
-        var date = DateTime.UtcNow;
-        await _dbManager.SetLastReadRules(message.MsgChannel.UserId, date);
-    }
-}
diff --git a/Content.Server/Info/ShowRulesCommand.cs b/Content.Server/Info/ShowRulesCommand.cs
index 32c24c29995..b13b8d11a5a 100644
--- a/Content.Server/Info/ShowRulesCommand.cs
+++ b/Content.Server/Info/ShowRulesCommand.cs
@@ -47,20 +47,16 @@ public async void Execute(IConsoleShell shell, string argStr, string[] args)
             }
         }
 
-        var locator = IoCManager.Resolve<IPlayerLocator>();
-        var located = await locator.LookupIdByNameOrIdAsync(target);
-        if (located == null)
+
+        var message = new ShowRulesPopupMessage { PopupTime = seconds };
+
+        if (!IoCManager.Resolve<IPlayerManager>().TryGetSessionByUsername(target, out var player))
         {
             shell.WriteError("Unable to find a player with that name.");
-            return;
+           return;
         }
 
         var netManager = IoCManager.Resolve<INetManager>();
-
-        var message = new SharedRulesManager.ShowRulesPopupMessage();
-        message.PopupTime = seconds;
-
-        var player = IoCManager.Resolve<IPlayerManager>().GetSessionById(located.UserId);
         netManager.ServerSendMessage(message, player.Channel);
     }
 }
diff --git a/Content.Server/Instruments/SwappableInstrumentSystem.cs b/Content.Server/Instruments/SwappableInstrumentSystem.cs
index 3f3cfb9e6db..9aef875cd65 100644
--- a/Content.Server/Instruments/SwappableInstrumentSystem.cs
+++ b/Content.Server/Instruments/SwappableInstrumentSystem.cs
@@ -35,7 +35,7 @@ private void AddStyleVerb(EntityUid uid, SwappableInstrumentComponent component,
                 Priority = priority,
                 Act = () =>
                 {
-                    _sharedInstrument.SetInstrumentProgram(instrument, entry.Value.Item1, entry.Value.Item2);
+                    _sharedInstrument.SetInstrumentProgram(uid, instrument, entry.Value.Item1, entry.Value.Item2);
                     _popup.PopupEntity(Loc.GetString("swappable-instrument-component-style-set", ("style", entry.Key)),
                         args.User, args.User);
                 }
diff --git a/Content.Server/InteractionVerbs/Actions/ChangeStandingStateAction.cs b/Content.Server/InteractionVerbs/Actions/ChangeStandingStateAction.cs
index 59340df58f2..0d74781f62d 100644
--- a/Content.Server/InteractionVerbs/Actions/ChangeStandingStateAction.cs
+++ b/Content.Server/InteractionVerbs/Actions/ChangeStandingStateAction.cs
@@ -33,7 +33,7 @@ public override bool Perform(InteractionArgs args, InteractionVerbPrototype prot
         if (state.CurrentState == StandingState.Lying && MakeStanding)
             return stateSystem.Stand(args.Target);
         else if (state.CurrentState == StandingState.Standing && MakeLaying)
-            return stateSystem.Down(args.Target, setDrawDepth: true);
+            return stateSystem.Down(args.Target);
 
         return false;
     }
diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs
index 7c150141333..4483c6af68c 100644
--- a/Content.Server/IoC/ServerContentIoC.cs
+++ b/Content.Server/IoC/ServerContentIoC.cs
@@ -11,7 +11,6 @@
 using Content.Server.DiscordAuth;
 using Content.Server.EUI;
 using Content.Server.GhostKick;
-using Content.Server.Info;
 using Content.Server.Maps;
 using Content.Server.Players.JobWhitelist;
 using Content.Server.MoMMI;
@@ -49,7 +48,6 @@ public static void Register()
             IoCManager.Register<IPlayerLocator, PlayerLocator>();
             IoCManager.Register<IAfkManager, AfkManager>();
             IoCManager.Register<IGameMapManager, GameMapManager>();
-            IoCManager.Register<RulesManager, RulesManager>();
             IoCManager.Register<IBanManager, BanManager>();
             IoCManager.Register<ContentNetworkResourceManager>();
             IoCManager.Register<IAdminNotesManager, AdminNotesManager>();
diff --git a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs
index f9e1ae22416..40be61695fc 100644
--- a/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs
+++ b/Content.Server/Kitchen/EntitySystems/MicrowaveSystem.cs
@@ -99,9 +99,13 @@ private void OnCookStart(Entity<ActiveMicrowaveComponent> ent, ref ComponentStar
             if (!TryComp<MicrowaveComponent>(ent, out var microwaveComponent))
                 return;
             SetAppearance(ent.Owner, MicrowaveVisualState.Cooking, microwaveComponent);
+            
+            var audio = _audio.PlayPvs(microwaveComponent.LoopingSound, ent, AudioParams.Default.WithLoop(true).WithMaxDistance(5));
 
-            microwaveComponent.PlayingStream =
-                _audio.PlayPvs(microwaveComponent.LoopingSound, ent, AudioParams.Default.WithLoop(true).WithMaxDistance(5)).Value.Entity;
+            if (audio == null)
+                return;
+
+            microwaveComponent.PlayingStream = audio!.Value.Entity;
         }
 
         private void OnCookStop(Entity<ActiveMicrowaveComponent> ent, ref ComponentShutdown args)
diff --git a/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs b/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs
index 93a15f319d9..0d6fb3fb90c 100644
--- a/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs
+++ b/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs
@@ -323,9 +323,15 @@ private void DoWork(EntityUid uid, ReagentGrinderComponent reagentGrinder, Grind
             var active = AddComp<ActiveReagentGrinderComponent>(uid);
             active.EndTime = _timing.CurTime + reagentGrinder.WorkTime * reagentGrinder.WorkTimeMultiplier;
             active.Program = program;
+            
+            // slightly higher pitched
+            var audio = _audioSystem.PlayPvs(sound, uid,
+                AudioParams.Default.WithPitchScale(1 / reagentGrinder.WorkTimeMultiplier));
 
-            reagentGrinder.AudioStream = _audioSystem.PlayPvs(sound, uid,
-                AudioParams.Default.WithPitchScale(1 / reagentGrinder.WorkTimeMultiplier)).Value.Entity; //slightly higher pitched
+            if (audio == null)
+                return;
+
+            reagentGrinder.AudioStream = audio!.Value.Entity;
             _userInterfaceSystem.ServerSendUiMessage(uid, ReagentGrinderUiKey.Key,
                 new ReagentGrinderWorkStartedMessage(program));
         }
diff --git a/Content.Server/Light/EntitySystems/RotatingLightSystem.cs b/Content.Server/Light/EntitySystems/RotatingLightSystem.cs
index dd72b3a43e8..7ef1357dc31 100644
--- a/Content.Server/Light/EntitySystems/RotatingLightSystem.cs
+++ b/Content.Server/Light/EntitySystems/RotatingLightSystem.cs
@@ -19,6 +19,6 @@ private void OnLightToggle(EntityUid uid, RotatingLightComponent comp, PointLigh
             return;
 
         comp.Enabled = args.Enabled;
-        Dirty(comp);
+        Dirty(uid, comp);
     }
 }
diff --git a/Content.Server/Lightning/LightningSystem.cs b/Content.Server/Lightning/LightningSystem.cs
index 4f975a60fda..3c0da8e9149 100644
--- a/Content.Server/Lightning/LightningSystem.cs
+++ b/Content.Server/Lightning/LightningSystem.cs
@@ -20,6 +20,7 @@ public sealed class LightningSystem : SharedLightningSystem
     [Dependency] private readonly BeamSystem _beam = default!;
     [Dependency] private readonly IRobustRandom _random = default!;
     [Dependency] private readonly EntityLookupSystem _lookup = default!;
+    [Dependency] private readonly SharedTransformSystem _transform = default!;
 
     public override void Initialize()
     {
@@ -74,28 +75,33 @@ public void ShootRandomLightnings(EntityUid user, float range, int boltCount, st
         //To Do: This is still pretty bad for perf but better than before and at least it doesn't re-allocate
         // several hashsets every time
 
-        var targets = _lookup.GetComponentsInRange<LightningTargetComponent>(Transform(user).MapPosition, range).ToList();
+        var userCoords = _transform.GetMapCoordinates(user);
+        var targetEnts = _lookup.GetEntitiesInRange<LightningTargetComponent>(userCoords, range);
+        var targets = targetEnts.Select(x => x.Comp).ToList();
+
         _random.Shuffle(targets);
         targets.Sort((x, y) => y.Priority.CompareTo(x.Priority));
 
-        int shootedCount = 0;
+        int shotCount = 0;
         int count = -1;
-        while(shootedCount < boltCount)
+
+        while (shotCount < boltCount)
         {
             count++;
 
             if (count >= targets.Count) { break; }
-
             var curTarget = targets[count];
-            if (!_random.Prob(curTarget.HitProbability)) //Chance to ignore target
+
+            // Chance to ignore target
+            if (!_random.Prob(curTarget.HitProbability))
                 continue;
 
             ShootLightning(user, targets[count].Owner, lightningPrototype, triggerLightningEvents);
+
             if (arcDepth - targets[count].LightningResistance > 0)
-            {
                 ShootRandomLightnings(targets[count].Owner, range, 1, lightningPrototype, arcDepth - targets[count].LightningResistance, triggerLightningEvents);
-            }
-            shootedCount++;
+
+            shotCount++;
         }
     }
 }
diff --git a/Content.Server/Materials/MaterialReclaimerSystem.cs b/Content.Server/Materials/MaterialReclaimerSystem.cs
index de82f125985..3b23308758d 100644
--- a/Content.Server/Materials/MaterialReclaimerSystem.cs
+++ b/Content.Server/Materials/MaterialReclaimerSystem.cs
@@ -146,7 +146,7 @@ public override bool TryFinishProcessItem(EntityUid uid, MaterialReclaimerCompon
             return false;
 
         Container.Remove(item, active.ReclaimingContainer);
-        Dirty(component);
+        Dirty(uid, component);
 
         // scales the output if the process was interrupted.
         var completion = 1f - Math.Clamp((float) Math.Round((active.EndTime - Timing.CurTime) / active.Duration),
diff --git a/Content.Server/Mech/Equipment/EntitySystems/MechGrabberSystem.cs b/Content.Server/Mech/Equipment/EntitySystems/MechGrabberSystem.cs
index fa46792d2af..0ade8928f9f 100644
--- a/Content.Server/Mech/Equipment/EntitySystems/MechGrabberSystem.cs
+++ b/Content.Server/Mech/Equipment/EntitySystems/MechGrabberSystem.cs
@@ -83,9 +83,10 @@ public void RemoveItem(EntityUid uid, EntityUid mech, EntityUid toRemove, MechGr
         var xform = Transform(toRemove);
         _transform.AttachToGridOrMap(toRemove, xform);
         var (mechPos, mechRot) = _transform.GetWorldPositionRotation(mechxform);
+        var toRemoveWorldPos = _transform.GetWorldPosition(xform);
 
         var offset = mechPos + mechRot.RotateVec(component.DepositOffset);
-        _transform.SetWorldPositionRotation(xform, offset, Angle.Zero);
+        _transform.SetWorldPositionRotation(toRemove, toRemoveWorldPos + offset, Angle.Zero);
         _mech.UpdateUserInterface(mech);
     }
 
@@ -154,7 +155,12 @@ private void OnInteract(EntityUid uid, MechGrabberComponent component, InteractN
             return;
 
         args.Handled = true;
-        component.AudioStream = _audio.PlayPvs(component.GrabSound, uid).Value.Entity;
+        var audio = _audio.PlayPvs(component.GrabSound, uid);
+        
+        if (audio == null)
+            return;
+
+        component.AudioStream = audio!.Value.Entity;
         var doAfterArgs = new DoAfterArgs(EntityManager, args.User, component.GrabDelay, new GrabberDoAfterEvent(), uid, target: target, used: uid)
         {
             BreakOnTargetMove = true,
diff --git a/Content.Server/Mech/Systems/MechSystem.cs b/Content.Server/Mech/Systems/MechSystem.cs
index 36dce2c9bcc..a728ee7de5e 100644
--- a/Content.Server/Mech/Systems/MechSystem.cs
+++ b/Content.Server/Mech/Systems/MechSystem.cs
@@ -107,7 +107,7 @@ private void OnInsertBattery(EntityUid uid, MechComponent component, EntInserted
         component.Energy = battery.CurrentCharge;
         component.MaxEnergy = battery.MaxCharge;
 
-        Dirty(component);
+        Dirty(uid, component);
         _actionBlocker.UpdateCanMove(uid);
     }
 
@@ -137,7 +137,7 @@ private void OnMapInit(EntityUid uid, MechComponent component, MapInitEvent args
         component.Energy = component.MaxEnergy;
 
         _actionBlocker.UpdateCanMove(uid);
-        Dirty(component);
+        Dirty(uid, component);
     }
 
     private void OnRemoveEquipmentMessage(EntityUid uid, MechComponent component, MechEquipmentRemoveMessage args)
@@ -337,7 +337,7 @@ public override bool TryChangeEnergy(EntityUid uid, FixedPoint2 delta, MechCompo
         {
             Log.Debug($"Battery charge was not equal to mech charge. Battery {batteryComp.CurrentCharge}. Mech {component.Energy}");
             component.Energy = batteryComp.CurrentCharge;
-            Dirty(component);
+            Dirty(uid, component);
         }
         _actionBlocker.UpdateCanMove(uid);
         return true;
@@ -357,7 +357,7 @@ public void InsertBattery(EntityUid uid, EntityUid toInsert, MechComponent? comp
 
         _actionBlocker.UpdateCanMove(uid);
 
-        Dirty(component);
+        Dirty(uid, component);
         UpdateUserInterface(uid, component);
     }
 
@@ -372,7 +372,7 @@ public void RemoveBattery(EntityUid uid, MechComponent? component = null)
 
         _actionBlocker.UpdateCanMove(uid);
 
-        Dirty(component);
+        Dirty(uid, component);
         UpdateUserInterface(uid, component);
     }
 
diff --git a/Content.Server/Mood/MoodComponent.cs b/Content.Server/Mood/MoodComponent.cs
index 7fd4a7136f3..caa221fe18e 100644
--- a/Content.Server/Mood/MoodComponent.cs
+++ b/Content.Server/Mood/MoodComponent.cs
@@ -1,5 +1,6 @@
 using Content.Shared.Alert;
 using Content.Shared.FixedPoint;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Generic;
 
 namespace Content.Server.Mood;
@@ -50,6 +51,9 @@ public sealed partial class MoodComponent : Component
     [ViewVariables(VVAccess.ReadOnly)]
     public FixedPoint2 CritThresholdBeforeModify;
 
+    [DataField]
+    public ProtoId<AlertCategoryPrototype> MoodCategory = "Mood";
+
     [DataField(customTypeSerializer: typeof(DictionarySerializer<MoodThreshold, float>))]
     public Dictionary<MoodThreshold, float> MoodThresholds = new()
     {
@@ -65,20 +69,20 @@ public sealed partial class MoodComponent : Component
         { MoodThreshold.Dead, 0f }
     };
 
-    [DataField(customTypeSerializer: typeof(DictionarySerializer<MoodThreshold, AlertType>))]
-    public Dictionary<MoodThreshold, AlertType> MoodThresholdsAlerts = new()
+    [DataField(customTypeSerializer: typeof(DictionarySerializer<MoodThreshold, ProtoId<AlertPrototype>>))]
+    public Dictionary<MoodThreshold, ProtoId<AlertPrototype>> MoodThresholdsAlerts = new()
     {
-        { MoodThreshold.Dead, AlertType.MoodDead },
-        { MoodThreshold.Horrible, AlertType.Horrible },
-        { MoodThreshold.Terrible, AlertType.Terrible },
-        { MoodThreshold.Bad, AlertType.Bad },
-        { MoodThreshold.Meh, AlertType.Meh },
-        { MoodThreshold.Neutral, AlertType.Neutral },
-        { MoodThreshold.Good, AlertType.Good },
-        { MoodThreshold.Great, AlertType.Great },
-        { MoodThreshold.Exceptional, AlertType.Exceptional },
-        { MoodThreshold.Perfect, AlertType.Perfect },
-        { MoodThreshold.Insane, AlertType.Insane }
+        { MoodThreshold.Dead, "MoodDead" },
+        { MoodThreshold.Horrible, "Horrible" },
+        { MoodThreshold.Terrible, "Terrible" },
+        { MoodThreshold.Bad, "Bad" },
+        { MoodThreshold.Meh, "Meh" },
+        { MoodThreshold.Neutral, "Neutral" },
+        { MoodThreshold.Good, "Good" },
+        { MoodThreshold.Great, "Great" },
+        { MoodThreshold.Exceptional, "Exceptional" },
+        { MoodThreshold.Perfect, "Perfect" },
+        { MoodThreshold.Insane, "Insane" }
     };
 
     /// <summary>
diff --git a/Content.Server/Mood/MoodSystem.cs b/Content.Server/Mood/MoodSystem.cs
index 4ec4709ea7a..41baf9f5479 100644
--- a/Content.Server/Mood/MoodSystem.cs
+++ b/Content.Server/Mood/MoodSystem.cs
@@ -57,7 +57,7 @@ public override void Initialize()
 
     private void OnShutdown(EntityUid uid, MoodComponent component, ComponentShutdown args)
     {
-        _alerts.ClearAlertCategory(uid, AlertCategory.Mood);
+        _alerts.ClearAlertCategory(uid, component.MoodCategory);
     }
 
     private void OnRemoveEffect(EntityUid uid, MoodComponent component, MoodRemoveEffectEvent args)
@@ -331,7 +331,7 @@ private void DoMoodThresholdsEffects(EntityUid uid, MoodComponent? component = n
         if (component.MoodThresholdsAlerts.TryGetValue(component.CurrentMoodThreshold, out var alertId))
             _alerts.ShowAlert(uid, alertId);
         else
-            _alerts.ClearAlertCategory(uid, AlertCategory.Mood);
+            _alerts.ClearAlertCategory(uid, component.MoodCategory);
 
         component.LastThreshold = component.CurrentMoodThreshold;
     }
diff --git a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/UnbuckleOperator.cs b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/UnbuckleOperator.cs
index 207665d786f..116e8fe7c7f 100644
--- a/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/UnbuckleOperator.cs
+++ b/Content.Server/NPC/HTN/PrimitiveTasks/Operators/Combat/UnbuckleOperator.cs
@@ -1,11 +1,9 @@
 using Content.Server.Buckle.Systems;
-using Content.Shared.Buckle.Components;
 
 namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat;
 
 public sealed partial class UnbuckleOperator : HTNOperator
 {
-    [Dependency] private readonly IEntityManager _entManager = default!;
     private BuckleSystem _buckle = default!;
 
     [DataField("shutdownState")]
@@ -21,10 +19,7 @@ public override void Startup(NPCBlackboard blackboard)
     {
         base.Startup(blackboard);
         var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
-        if (!_entManager.TryGetComponent<BuckleComponent>(owner, out var buckle) || !buckle.Buckled)
-            return;
-
-        _buckle.TryUnbuckle(owner, owner, true, buckle);
+        _buckle.Unbuckle(owner, null);
     }
 
     public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)
diff --git a/Content.Server/NPC/Systems/NPCJukeSystem.cs b/Content.Server/NPC/Systems/NPCJukeSystem.cs
index 5a724762ef6..d55a0895d30 100644
--- a/Content.Server/NPC/Systems/NPCJukeSystem.cs
+++ b/Content.Server/NPC/Systems/NPCJukeSystem.cs
@@ -22,6 +22,7 @@ public sealed class NPCJukeSystem : EntitySystem
     [Dependency] private readonly EntityLookupSystem _lookup = default!;
     [Dependency] private readonly MeleeWeaponSystem _melee = default!;
     [Dependency] private readonly SharedTransformSystem _transform = default!;
+    [Dependency] private readonly SharedMapSystem _map = default!;
 
     private EntityQuery<NPCMeleeCombatComponent> _npcMeleeQuery;
     private EntityQuery<NPCRangedCombatComponent> _npcRangedQuery;
@@ -49,6 +50,9 @@ private void OnJukeSteering(EntityUid uid, NPCJukeComponent component, ref NPCSt
 
         if (component.JukeType == JukeType.AdjacentTile)
         {
+            if (args.Transform.GridUid == null)
+                return;
+
             if (_npcRangedQuery.TryGetComponent(uid, out var ranged)
                 && ranged.Status is CombatStatus.NotInSight
                 || !TryComp<MapGridComponent>(args.Transform.GridUid, out var grid))
@@ -57,7 +61,7 @@ private void OnJukeSteering(EntityUid uid, NPCJukeComponent component, ref NPCSt
                 return;
             }
 
-            var currentTile = grid.CoordinatesToTile(args.Transform.Coordinates);
+            var currentTile = _map.CoordinatesToTile((EntityUid) args.Transform.GridUid, grid, args.Transform.Coordinates);
 
             if (component.TargetTile == null)
             {
@@ -113,8 +117,8 @@ private void OnJukeSteering(EntityUid uid, NPCJukeComponent component, ref NPCSt
                 return;
             }
 
-            var targetCoords = grid.GridTileToWorld(component.TargetTile.Value);
-            var targetDir = (targetCoords.Position - args.WorldPosition);
+            var targetCoords = _map.GridTileToWorld((EntityUid) args.Transform.GridUid, grid, component.TargetTile.Value);
+            var targetDir = targetCoords.Position - args.WorldPosition;
             targetDir = args.OffsetRotation.RotateVec(targetDir);
             const float weight = 1f;
             var norm = targetDir.Normalized();
diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs
index 3bc4eae9e49..60408e9a4cb 100644
--- a/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs
+++ b/Content.Server/NPC/Systems/NPCSteeringSystem.Obstacles.cs
@@ -207,7 +207,7 @@ private void GetObstacleEntities(PathPoly poly, int mask, int layer, List<Entity
             return;
         }
 
-        foreach (var ent in grid.GetLocalAnchoredEntities(poly.Box))
+        foreach (var ent in _map.GetLocalAnchoredEntities(poly.GraphUid, grid, poly.Box))
         {
             if (!_physicsQuery.TryGetComponent(ent, out var body) ||
                 !body.Hard ||
diff --git a/Content.Server/NPC/Systems/NPCSteeringSystem.cs b/Content.Server/NPC/Systems/NPCSteeringSystem.cs
index 447792b6ff2..8729c9f7d8b 100644
--- a/Content.Server/NPC/Systems/NPCSteeringSystem.cs
+++ b/Content.Server/NPC/Systems/NPCSteeringSystem.cs
@@ -58,6 +58,7 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
     [Dependency] private readonly SharedPhysicsSystem _physics = default!;
     [Dependency] private readonly SharedTransformSystem _transform = default!;
     [Dependency] private readonly SharedCombatModeSystem _combat = default!;
+    [Dependency] private readonly SharedMapSystem _map = default!;
 
     private EntityQuery<FixturesComponent> _fixturesQuery;
     private EntityQuery<MovementSpeedModifierComponent> _modifierQuery;
diff --git a/Content.Server/NameIdentifier/NameIdentifierSystem.cs b/Content.Server/NameIdentifier/NameIdentifierSystem.cs
index 87953d518b3..eefd4357cb3 100644
--- a/Content.Server/NameIdentifier/NameIdentifierSystem.cs
+++ b/Content.Server/NameIdentifier/NameIdentifierSystem.cs
@@ -113,7 +113,7 @@ private void OnMapInit(EntityUid uid, NameIdentifierComponent component, MapInit
         _metaData.SetEntityName(uid, group.FullName
             ? uniqueName
             : $"{meta.EntityName} ({uniqueName})", meta);
-        Dirty(component);
+        Dirty(uid, component);
     }
 
     private void InitialSetupPrototypes()
diff --git a/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs b/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs
index 1dfaf4f3393..0c1e88653fa 100644
--- a/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs
+++ b/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs
@@ -102,20 +102,23 @@ private int Download(EntityUid uid, List<string> ids)
     /// </summary>
     public void SetSuitPowerAlert(EntityUid uid, SpaceNinjaComponent? comp = null)
     {
-        if (!Resolve(uid, ref comp, false) || comp.Deleted || comp.Suit == null)
+        if (!Resolve(uid, ref comp, false))
+            return;
+
+        if (comp.Deleted || comp.Suit == null)
         {
-            _alerts.ClearAlert(uid, AlertType.SuitPower);
+            _alerts.ClearAlert(uid, comp.SuitPowerAlert);
             return;
         }
 
         if (GetNinjaBattery(uid, out _, out var battery))
         {
             var severity = ContentHelpers.RoundToLevels(MathF.Max(0f, battery.CurrentCharge), battery.MaxCharge, 8);
-            _alerts.ShowAlert(uid, AlertType.SuitPower, (short) severity);
+            _alerts.ShowAlert(uid, comp.SuitPowerAlert, (short) severity);
         }
         else
         {
-            _alerts.ClearAlert(uid, AlertType.SuitPower);
+            _alerts.ClearAlert(uid, comp.SuitPowerAlert);
         }
     }
 
diff --git a/Content.Server/Nyanotrasen/Construction/Commands/TileWindowsCommand.cs b/Content.Server/Nyanotrasen/Construction/Commands/TileWindowsCommand.cs
index 9eef7292eaa..4e03ee059a0 100644
--- a/Content.Server/Nyanotrasen/Construction/Commands/TileWindowsCommand.cs
+++ b/Content.Server/Nyanotrasen/Construction/Commands/TileWindowsCommand.cs
@@ -5,7 +5,9 @@
 using Robust.Server.Player;
 using Robust.Shared.Console;
 using Robust.Shared.Map;
+using Robust.Shared.Map.Components;
 using Robust.Shared.Maths;
+using Robust.Shared.Physics;
 using Robust.Shared.Player;
 
 namespace Content.Server.Construction.Commands
@@ -24,8 +26,11 @@ sealed class TileWindowsCommand : IConsoleCommand
 
         public void Execute(IConsoleShell shell, string argStr, string[] args)
         {
-            var player = shell.Player as ICommonSession;
+            var player = shell.Player;
             var entityManager = IoCManager.Resolve<IEntityManager>();
+            var lookup = IoCManager.Resolve<EntityLookupSystem>();
+            var mapSystem = IoCManager.Resolve<SharedMapSystem>();
+
             EntityUid? gridId;
 
             switch (args.Length)
@@ -53,8 +58,7 @@ public void Execute(IConsoleShell shell, string argStr, string[] args)
                     return;
             }
 
-            var mapManager = IoCManager.Resolve<IMapManager>();
-            if (!mapManager.TryGetGrid(gridId, out var grid))
+            if (!entityManager.TryGetComponent<MapGridComponent>(gridId, out var grid))
             {
                 shell.WriteLine($"No grid exists with id {gridId}");
                 return;
@@ -70,8 +74,12 @@ public void Execute(IConsoleShell shell, string argStr, string[] args)
             var tagSystem = entityManager.EntitySysManager.GetEntitySystem<TagSystem>();
             var underplating = tileDefinitionManager[TilePrototypeId];
             var underplatingTile = new Tile(underplating.TileId);
+            var childEntities = new HashSet<Entity<TransformComponent>>();
             var changed = 0;
-            foreach (var child in entityManager.GetComponent<TransformComponent>(grid.Owner).ChildEntities)
+
+            lookup.GetChildEntities(grid.Owner, childEntities);
+
+            foreach (var child in childEntities)
             {
                 if (!entityManager.EntityExists(child))
                 {
@@ -95,7 +103,7 @@ public void Execute(IConsoleShell shell, string argStr, string[] args)
                     continue;
                 }
 
-                var tile = grid.GetTileRef(childTransform.Coordinates);
+                var tile = mapSystem.GetTileRef((EntityUid) gridId, grid, childTransform.Coordinates);
                 var tileDef = (ContentTileDefinition) tileDefinitionManager[tile.Tile.TypeId];
 
                 if (tileDef.ID == TilePrototypeId)
@@ -103,7 +111,7 @@ public void Execute(IConsoleShell shell, string argStr, string[] args)
                     continue;
                 }
 
-                grid.SetTile(childTransform.Coordinates, underplatingTile);
+                mapSystem.SetTile((EntityUid) gridId, grid, childTransform.Coordinates, underplatingTile);
                 changed++;
             }
 
diff --git a/Content.Server/Nyanotrasen/Players/PlayTimeTracking/PlayTimeTrackingManager.Whitelist.cs b/Content.Server/Nyanotrasen/Players/PlayTimeTracking/PlayTimeTrackingManager.Whitelist.cs
index ccbe8d8e7fe..53b0889aa64 100644
--- a/Content.Server/Nyanotrasen/Players/PlayTimeTracking/PlayTimeTrackingManager.Whitelist.cs
+++ b/Content.Server/Nyanotrasen/Players/PlayTimeTracking/PlayTimeTrackingManager.Whitelist.cs
@@ -15,7 +15,7 @@ private void SendWhitelistCached(ICommonSession playerSession)
             Whitelisted = whitelist
         };
 
-        _net.ServerSendMessage(msg, playerSession.ConnectedClient);
+        _net.ServerSendMessage(msg, playerSession.Channel);
     }
 
     /// <summary>
diff --git a/Content.Server/Nyanotrasen/StationEvents/Events/MidRoundAntagRule.cs b/Content.Server/Nyanotrasen/StationEvents/Events/MidRoundAntagRule.cs
index 94a488bd84b..c1bce269ff9 100644
--- a/Content.Server/Nyanotrasen/StationEvents/Events/MidRoundAntagRule.cs
+++ b/Content.Server/Nyanotrasen/StationEvents/Events/MidRoundAntagRule.cs
@@ -1,4 +1,4 @@
-using Content.Server.GameTicking.Components;
+using Content.Shared.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
 using Robust.Shared.Random;
diff --git a/Content.Server/Objectives/ObjectivesSystem.cs b/Content.Server/Objectives/ObjectivesSystem.cs
index 47fe4eb5f88..bf013bc0402 100644
--- a/Content.Server/Objectives/ObjectivesSystem.cs
+++ b/Content.Server/Objectives/ObjectivesSystem.cs
@@ -1,6 +1,7 @@
 using Content.Server.GameTicking;
 using Content.Server.Shuttles.Systems;
 using Content.Shared.Cuffs.Components;
+using Content.Shared.GameTicking.Components;
 using Content.Shared.Mind;
 using Content.Shared.Objectives.Components;
 using Content.Shared.Objectives.Systems;
@@ -9,7 +10,6 @@
 using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
 using System.Linq;
-using Content.Server.GameTicking.Components;
 using System.Text;
 using Robust.Server.Player;
 
diff --git a/Content.Server/OfferItem/OfferItemSystem.cs b/Content.Server/OfferItem/OfferItemSystem.cs
index 420df71ace7..e2eb65822e0 100644
--- a/Content.Server/OfferItem/OfferItemSystem.cs
+++ b/Content.Server/OfferItem/OfferItemSystem.cs
@@ -39,11 +39,11 @@ public override void Update(float frameTime)
 
             if (!offerItem.IsInReceiveMode)
             {
-                _alertsSystem.ClearAlert(uid, AlertType.Offer);
+                _alertsSystem.ClearAlert(uid, offerItem.OfferAlert);
                 continue;
             }
 
-            _alertsSystem.ShowAlert(uid, AlertType.Offer);
+            _alertsSystem.ShowAlert(uid, offerItem.OfferAlert);
         }
     }
 
diff --git a/Content.Server/Physics/Controllers/ConveyorController.cs b/Content.Server/Physics/Controllers/ConveyorController.cs
index 42279bb7496..b3508025cb9 100644
--- a/Content.Server/Physics/Controllers/ConveyorController.cs
+++ b/Content.Server/Physics/Controllers/ConveyorController.cs
@@ -67,7 +67,7 @@ private void OnPowerChanged(EntityUid uid, ConveyorComponent component, ref Powe
     {
         component.Powered = args.Powered;
         UpdateAppearance(uid, component);
-        Dirty(component);
+        Dirty(uid, component);
     }
 
     private void UpdateAppearance(EntityUid uid, ConveyorComponent component)
@@ -106,7 +106,7 @@ private void SetState(EntityUid uid, ConveyorState state, ConveyorComponent? com
         _materialReclaimer.SetReclaimerEnabled(uid, component.State != ConveyorState.Off);
 
         UpdateAppearance(uid, component);
-        Dirty(component);
+        Dirty(uid, component);
     }
 
     /// <summary>
diff --git a/Content.Server/Pinpointer/NavMapSystem.cs b/Content.Server/Pinpointer/NavMapSystem.cs
index 5881daa0689..7ed13b7b940 100644
--- a/Content.Server/Pinpointer/NavMapSystem.cs
+++ b/Content.Server/Pinpointer/NavMapSystem.cs
@@ -68,7 +68,7 @@ public override void Initialize()
     private void OnStationInit(StationGridAddedEvent ev)
     {
         var comp = EnsureComp<NavMapComponent>(ev.GridId);
-        RefreshGrid(comp, Comp<MapGridComponent>(ev.GridId));
+        RefreshGrid(ev.GridId, comp, Comp<MapGridComponent>(ev.GridId));
     }
 
     #region: Grid change event handling
@@ -81,10 +81,10 @@ private void OnNavMapSplit(ref GridSplitEvent args)
         foreach (var grid in args.NewGrids)
         {
             var newComp = EnsureComp<MapGridComponent>(grid);
-            RefreshGrid(comp, newComp);
+            RefreshGrid(args.Grid, comp, newComp);
         }
 
-        RefreshGrid(comp, _gridQuery.GetComponent(args.Grid));
+        RefreshGrid(args.Grid, comp, _gridQuery.GetComponent(args.Grid));
     }
 
     private NavMapChunk EnsureChunk(NavMapComponent component, Vector2i origin)
@@ -231,14 +231,14 @@ private void OnConfigurableExamined(Entity<ConfigurableNavMapBeaconComponent> en
 
     #region: Grid functions
 
-    private void RefreshGrid(NavMapComponent component, MapGridComponent mapGrid)
+    private void RefreshGrid(EntityUid uid, NavMapComponent component, MapGridComponent mapGrid)
     {
         // Clear stale data
         component.Chunks.Clear();
         component.Beacons.Clear();
 
         // Loop over all tiles
-        var tileRefs = _mapSystem.GetAllTiles(mapGrid.Owner, mapGrid);
+        var tileRefs = _mapSystem.GetAllTiles(uid, mapGrid);
 
         foreach (var tileRef in tileRefs)
         {
@@ -247,10 +247,10 @@ private void RefreshGrid(NavMapComponent component, MapGridComponent mapGrid)
 
             var chunk = EnsureChunk(component, chunkOrigin);
             chunk.LastUpdate = _gameTiming.CurTick;
-            RefreshTileEntityContents(mapGrid.Owner, component, mapGrid, chunkOrigin, tile, setFloor: true);
+            RefreshTileEntityContents(uid, component, mapGrid, chunkOrigin, tile, setFloor: true);
         }
 
-        Dirty(mapGrid.Owner, component);
+        Dirty(uid, component);
     }
 
     private (int NewVal, NavMapChunk Chunk) RefreshTileEntityContents(EntityUid uid,
diff --git a/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs b/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs
index 42c84b7f43b..35b17dc9584 100644
--- a/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs
+++ b/Content.Server/Power/EntitySystems/PowerMonitoringConsoleSystem.cs
@@ -6,6 +6,7 @@
 using Content.Server.Power.NodeGroups;
 using Content.Server.Station.Components;
 using Content.Server.StationEvents.Components;
+using Content.Shared.GameTicking.Components;
 using Content.Shared.Pinpointer;
 using Content.Shared.Power;
 using JetBrains.Annotations;
@@ -13,7 +14,6 @@
 using Robust.Shared.Map.Components;
 using Robust.Shared.Utility;
 using System.Linq;
-using Content.Server.GameTicking.Components;
 
 namespace Content.Server.Power.EntitySystems;
 
diff --git a/Content.Server/PowerCell/PowerCellSystem.Draw.cs b/Content.Server/PowerCell/PowerCellSystem.Draw.cs
index 8e960357b7a..4155a4f6bec 100644
--- a/Content.Server/PowerCell/PowerCellSystem.Draw.cs
+++ b/Content.Server/PowerCell/PowerCellSystem.Draw.cs
@@ -67,7 +67,7 @@ private void OnDrawChargeChanged(EntityUid uid, PowerCellDrawComponent component
         {
             component.CanDraw = canDraw;
             component.CanUse = canUse;
-            Dirty(component);
+            Dirty(uid, component);
         }
     }
 
@@ -80,7 +80,7 @@ private void OnDrawCellChanged(EntityUid uid, PowerCellDrawComponent component,
         {
             component.CanDraw = canDraw;
             component.CanUse = canUse;
-            Dirty(component);
+            Dirty(uid, component);
         }
     }
 }
diff --git a/Content.Server/Psionics/Dreams/DreamSystem.cs b/Content.Server/Psionics/Dreams/DreamSystem.cs
index d6067717c94..0729f5c59dd 100644
--- a/Content.Server/Psionics/Dreams/DreamSystem.cs
+++ b/Content.Server/Psionics/Dreams/DreamSystem.cs
@@ -51,7 +51,7 @@ public override void Update(float frameTime)
                     ("telepathicChannelName", Loc.GetString("chat-manager-telepathic-channel-name")), ("message", msg));
 
                 _chatManager.ChatMessageToOne(Shared.Chat.ChatChannel.Telepathic,
-                msg, messageWrap, sleeper.Owner, false, actor.PlayerSession.ConnectedClient, Color.PaleVioletRed);
+                msg, messageWrap, sleeper.Owner, false, actor.PlayerSession.Channel, Color.PaleVioletRed);
             }
         }
     }
diff --git a/Content.Server/Research/Systems/ResearchSystem.Server.cs b/Content.Server/Research/Systems/ResearchSystem.Server.cs
index 2a802a91a32..09ca7ed15c2 100644
--- a/Content.Server/Research/Systems/ResearchSystem.Server.cs
+++ b/Content.Server/Research/Systems/ResearchSystem.Server.cs
@@ -18,7 +18,7 @@ private void OnServerStartup(EntityUid uid, ResearchServerComponent component, C
         var unusedId = EntityQuery<ResearchServerComponent>(true)
             .Max(s => s.Id) + 1;
         component.Id = unusedId;
-        Dirty(component);
+        Dirty(uid, component);
     }
 
     private void OnServerShutdown(EntityUid uid, ResearchServerComponent component, ComponentShutdown args)
@@ -74,7 +74,7 @@ public void RegisterClient(EntityUid client, EntityUid server, ResearchClientCom
         SyncClientWithServer(client, clientComponent: clientComponent);
 
         if (dirtyServer)
-            Dirty(serverComponent);
+            Dirty(server, serverComponent);
 
         var ev = new ResearchRegistrationChangedEvent(server);
         RaiseLocalEvent(client, ref ev);
@@ -117,7 +117,7 @@ public void UnregisterClient(EntityUid client, EntityUid server, ResearchClientC
 
         if (dirtyServer)
         {
-            Dirty(serverComponent);
+            Dirty(server, serverComponent);
         }
 
         var ev = new ResearchRegistrationChangedEvent(null);
@@ -167,6 +167,6 @@ public void ModifyServerPoints(EntityUid uid, int points, ResearchServerComponen
         {
             RaiseLocalEvent(client, ref ev);
         }
-        Dirty(component);
+        Dirty(uid, component);
     }
 }
diff --git a/Content.Server/Research/Systems/ResearchSystem.Technology.cs b/Content.Server/Research/Systems/ResearchSystem.Technology.cs
index 107d51ccd8c..9bd71cf7c6e 100644
--- a/Content.Server/Research/Systems/ResearchSystem.Technology.cs
+++ b/Content.Server/Research/Systems/ResearchSystem.Technology.cs
@@ -21,7 +21,7 @@ public void Sync(EntityUid primaryUid, EntityUid otherUid, TechnologyDatabaseCom
         primaryDb.UnlockedTechnologies = otherDb.UnlockedTechnologies;
         primaryDb.UnlockedRecipes = otherDb.UnlockedRecipes;
 
-        Dirty(primaryDb);
+        Dirty(primaryUid, primaryDb);
 
         var ev = new TechnologyDatabaseModifiedEvent();
         RaiseLocalEvent(primaryUid, ref ev);
@@ -125,7 +125,7 @@ public void AddTechnology(EntityUid uid, TechnologyPrototype technology, Technol
                 continue;
             component.UnlockedRecipes.Add(unlock);
         }
-        Dirty(component);
+        Dirty(uid, component);
 
         var ev = new TechnologyDatabaseModifiedEvent();
         RaiseLocalEvent(uid, ref ev);
@@ -144,7 +144,7 @@ public void AddLatheRecipe(EntityUid uid, string recipe, TechnologyDatabaseCompo
             return;
 
         component.UnlockedRecipes.Add(recipe);
-        Dirty(component);
+        Dirty(uid, component);
 
         var ev = new TechnologyDatabaseModifiedEvent();
         RaiseLocalEvent(uid, ref ev);
@@ -185,6 +185,6 @@ private void OnDatabaseRegistrationChanged(EntityUid uid, TechnologyDatabaseComp
         component.SupportedDisciplines = new List<string>();
         component.UnlockedTechnologies = new List<string>();
         component.UnlockedRecipes = new List<string>();
-        Dirty(component);
+        Dirty(uid, component);
     }
 }
diff --git a/Content.Server/Revenant/EntitySystems/RevenantSystem.cs b/Content.Server/Revenant/EntitySystems/RevenantSystem.cs
index 428d1ecb75e..595de100e86 100644
--- a/Content.Server/Revenant/EntitySystems/RevenantSystem.cs
+++ b/Content.Server/Revenant/EntitySystems/RevenantSystem.cs
@@ -141,7 +141,7 @@ public bool ChangeEssenceAmount(EntityUid uid, FixedPoint2 amount, RevenantCompo
         if (TryComp<StoreComponent>(uid, out var store))
             _store.UpdateUserInterface(uid, uid, store);
 
-        _alerts.ShowAlert(uid, AlertType.Essence);
+        _alerts.ShowAlert(uid, component.EssenceAlert);
 
         if (component.Essence <= 0)
         {
diff --git a/Content.Server/Salvage/SalvageSystem.Runner.cs b/Content.Server/Salvage/SalvageSystem.Runner.cs
index 23607e2bdc5..9b15ee30adc 100644
--- a/Content.Server/Salvage/SalvageSystem.Runner.cs
+++ b/Content.Server/Salvage/SalvageSystem.Runner.cs
@@ -151,8 +151,12 @@ private void UpdateRunner()
             }
             else if (comp.Stream == null && remaining < audioLength)
             {
-                var audio = _audio.PlayPvs(comp.Sound, uid).Value;
-                comp.Stream = audio.Entity;
+                var audio = _audio.PlayPvs(comp.Sound, uid);
+
+                if (audio == null)
+                    continue;
+
+                comp.Stream = audio!.Value.Entity;
                 _audio.SetMapAudio(audio);
                 comp.Stage = ExpeditionStage.MusicCountdown;
                 Dirty(uid, comp);
diff --git a/Content.Server/Shadowkin/ShadowkinSystem.cs b/Content.Server/Shadowkin/ShadowkinSystem.cs
index 83461e7a7fe..96bd09db276 100644
--- a/Content.Server/Shadowkin/ShadowkinSystem.cs
+++ b/Content.Server/Shadowkin/ShadowkinSystem.cs
@@ -55,7 +55,7 @@ private void OnEyeColorChange(EntityUid uid, ShadowkinComponent component, EyeCo
 
         component.OldEyeColor = humanoid.EyeColor;
         humanoid.EyeColor = component.BlackEyeColor;
-        Dirty(humanoid);
+        Dirty(uid, humanoid);
     }
 
     private void OnExamined(EntityUid uid, ShadowkinComponent component, ExaminedEvent args)
@@ -89,10 +89,10 @@ public void UpdateShadowkinAlert(EntityUid uid, ShadowkinComponent component)
         if (TryComp<PsionicComponent>(uid, out var magic))
         {
             var severity = (short) ContentHelpers.RoundToLevels(magic.Mana, magic.MaxMana, 8);
-            _alerts.ShowAlert(uid, AlertType.ShadowkinPower, severity);
+            _alerts.ShowAlert(uid, component.ShadowkinPowerAlert, severity);
         }
         else
-            _alerts.ClearAlert(uid, AlertType.ShadowkinPower);
+            _alerts.ClearAlert(uid, component.ShadowkinPowerAlert);
     }
 
     private void OnAttemptPowerUse(EntityUid uid, ShadowkinComponent component, OnAttemptPowerUseEvent args)
@@ -115,7 +115,7 @@ private void OnManaUpdate(EntityUid uid, ShadowkinComponent component, ref OnMan
         if (magic.Mana <= component.BlackEyeMana)
             ApplyBlackEye(uid);
 
-        Dirty(magic); // Update Shadowkin Overlay.
+        Dirty(uid, magic); // Update Shadowkin Overlay.
         UpdateShadowkinAlert(uid, component);
     }
 
@@ -141,7 +141,7 @@ private void OnMindbreak(EntityUid uid, ShadowkinComponent component, ref OnMind
         {
             component.OldEyeColor = humanoid.EyeColor;
             humanoid.EyeColor = component.BlackEyeColor;
-            Dirty(humanoid);
+            Dirty(uid, humanoid);
         }
 
         if (component.BlackeyeSpawn)
@@ -162,7 +162,7 @@ private void OnRejuvenate(EntityUid uid, ShadowkinComponent component, Rejuvenat
         if (TryComp<HumanoidAppearanceComponent>(uid, out var humanoid))
         {
             humanoid.EyeColor = component.OldEyeColor;
-            Dirty(humanoid);
+            Dirty(uid, humanoid);
         }
 
         EnsureComp<PsionicComponent>(uid, out var magic);
diff --git a/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs b/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs
index 6f24208c3a6..5550201202f 100644
--- a/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs
+++ b/Content.Server/Shuttles/Systems/ShuttleConsoleSystem.cs
@@ -317,12 +317,12 @@ public void AddPilot(EntityUid uid, EntityUid entity, ShuttleConsoleComponent co
 
         component.SubscribedPilots.Add(entity);
 
-        _alertsSystem.ShowAlert(entity, AlertType.PilotingShuttle);
+        _alertsSystem.ShowAlert(entity, pilotComponent.PilotingAlert);
 
         pilotComponent.Console = uid;
         ActionBlockerSystem.UpdateCanMove(entity);
         pilotComponent.Position = EntityManager.GetComponent<TransformComponent>(entity).Coordinates;
-        Dirty(pilotComponent);
+        Dirty(entity, pilotComponent);
     }
 
     public void RemovePilot(EntityUid pilotUid, PilotComponent pilotComponent)
@@ -339,7 +339,7 @@ public void RemovePilot(EntityUid pilotUid, PilotComponent pilotComponent)
         if (!helm.SubscribedPilots.Remove(pilotUid))
             return;
 
-        _alertsSystem.ClearAlert(pilotUid, AlertType.PilotingShuttle);
+        _alertsSystem.ClearAlert(pilotUid, pilotComponent.PilotingAlert);
 
         _popup.PopupEntity(Loc.GetString("shuttle-pilot-end"), pilotUid, pilotUid);
 
diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs
index 11cc16e0cd0..fe8ec7baeed 100644
--- a/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs
+++ b/Content.Server/Shuttles/Systems/ShuttleSystem.FasterThanLight.cs
@@ -81,6 +81,8 @@ public sealed partial class ShuttleSystem
     private void InitializeFTL()
     {
         SubscribeLocalEvent<StationPostInitEvent>(OnStationPostInit);
+        SubscribeLocalEvent<FTLComponent, ComponentShutdown>(OnFtlShutdown);
+
         _bodyQuery = GetEntityQuery<BodyComponent>();
         _buckleQuery = GetEntityQuery<BuckleComponent>();
         _beaconQuery = GetEntityQuery<FTLBeaconComponent>();
@@ -97,6 +99,12 @@ private void InitializeFTL()
         _cfg.OnValueChanged(CCVars.HyperspaceKnockdownTime, time => _hyperspaceKnockdownTime = TimeSpan.FromSeconds(time), true);
     }
 
+    private void OnFtlShutdown(Entity<FTLComponent> ent, ref ComponentShutdown args)
+    {
+        Del(ent.Comp.VisualizerEntity);
+        ent.Comp.VisualizerEntity = null;
+    }
+
     private void OnStationPostInit(ref StationPostInitEvent ev)
     {
         // Add all grid maps as ftl destinations that anyone can FTL to.
@@ -343,7 +351,11 @@ private bool TrySetupFTL(EntityUid uid, ShuttleComponent shuttle, [NotNullWhen(t
         component = AddComp<FTLComponent>(uid);
         component.State = FTLState.Starting;
         var audio = _audio.PlayPvs(_startupSound, uid);
-        audio.Value.Component.Flags |= AudioFlags.GridAudio;
+
+        if (audio == null)
+            return false;
+
+        audio!.Value.Component.Flags |= AudioFlags.GridAudio;
 
         if (_physicsQuery.TryGetComponent(uid, out var gridPhysics))
         {
@@ -422,8 +434,16 @@ private void UpdateFTLTravelling(Entity<FTLComponent, ShuttleComponent> entity)
         var comp = entity.Comp1;
         comp.StateTime = StartEndTime.FromCurTime(_gameTiming, DefaultArrivalTime);
         comp.State = FTLState.Arriving;
-        // TODO: Arrival effects
-        // For now we'll just use the ss13 bubbles but we can do fancier.
+
+        if (entity.Comp1.VisualizerProto != null)
+        {
+            comp.VisualizerEntity = SpawnAtPosition(entity.Comp1.VisualizerProto, entity.Comp1.TargetCoordinates);
+            var visuals = Comp<FtlVisualizerComponent>(comp.VisualizerEntity.Value);
+            visuals.Grid = entity.Owner;
+            Dirty(comp.VisualizerEntity.Value, visuals);
+            _transform.SetLocalRotation(comp.VisualizerEntity.Value, entity.Comp1.TargetAngle);
+            _pvs.AddGlobalOverride(comp.VisualizerEntity.Value);
+        }
 
         _thruster.DisableLinearThrusters(shuttle);
         _thruster.EnableLinearThrustDirection(shuttle, DirectionFlag.South);
@@ -509,7 +529,11 @@ private void UpdateFTLArriving(Entity<FTLComponent, ShuttleComponent> entity)
 
         comp.TravelStream = _audio.Stop(comp.TravelStream);
         var audio = _audio.PlayPvs(_arrivalSound, uid);
-        audio.Value.Component.Flags |= AudioFlags.GridAudio;
+
+        if (audio == null)
+            return;
+
+        audio!.Value.Component.Flags |= AudioFlags.GridAudio;
         // TODO: Shitcode til engine fix
 
         if (_physicsQuery.TryGetComponent(uid, out var gridPhysics))
diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.cs
index 6fe2324d51e..a10d0d56144 100644
--- a/Content.Server/Shuttles/Systems/ShuttleSystem.cs
+++ b/Content.Server/Shuttles/Systems/ShuttleSystem.cs
@@ -11,6 +11,7 @@
 using Content.Shared.Throwing;
 using JetBrains.Annotations;
 using Robust.Server.GameObjects;
+using Robust.Server.GameStates;
 using Robust.Shared.Audio;
 using Robust.Shared.Audio.Systems;
 using Robust.Shared.Configuration;
@@ -40,6 +41,7 @@ public sealed partial class ShuttleSystem : SharedShuttleSystem
     [Dependency] private readonly FixtureSystem _fixtures = default!;
     [Dependency] private readonly MapLoaderSystem _loader = default!;
     [Dependency] private readonly MetaDataSystem _metadata = default!;
+    [Dependency] private readonly PvsOverrideSystem _pvs = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
     [Dependency] private readonly SharedPhysicsSystem _physics = default!;
     [Dependency] private readonly SharedTransformSystem _transform = default!;
diff --git a/Content.Server/Silicon/Charge/Systems/SiliconChargeSystem.cs b/Content.Server/Silicon/Charge/Systems/SiliconChargeSystem.cs
index d8b034a69f5..444b65530b2 100644
--- a/Content.Server/Silicon/Charge/Systems/SiliconChargeSystem.cs
+++ b/Content.Server/Silicon/Charge/Systems/SiliconChargeSystem.cs
@@ -91,10 +91,10 @@ public override void Update(float frameTime)
             if (!TryGetSiliconBattery(silicon, out var batteryComp))
             {
                 UpdateChargeState(silicon, 0, siliconComp);
-                if (_alerts.IsShowingAlert(silicon, AlertType.BorgBattery))
+                if (_alerts.IsShowingAlert(silicon, siliconComp.BatteryAlert))
                 {
-                    _alerts.ClearAlert(silicon, AlertType.BorgBattery);
-                    _alerts.ShowAlert(silicon, AlertType.BorgBatteryNone);
+                    _alerts.ClearAlert(silicon, siliconComp.BatteryAlert);
+                    _alerts.ShowAlert(silicon, siliconComp.NoBatteryAlert);
                 }
                 continue;
             }
@@ -142,10 +142,10 @@ public void UpdateChargeState(EntityUid uid, short chargePercent, SiliconCompone
         _moveMod.RefreshMovementSpeedModifiers(uid);
 
         // If the battery was replaced and the no battery indicator is showing, replace the indicator
-        if (_alerts.IsShowingAlert(uid, AlertType.BorgBatteryNone) && chargePercent != 0)
+        if (_alerts.IsShowingAlert(uid, component.NoBatteryAlert) && chargePercent != 0)
         {
-            _alerts.ClearAlert(uid, AlertType.BorgBatteryNone);
-            _alerts.ShowAlert(uid, AlertType.BorgBattery, chargePercent);
+            _alerts.ClearAlert(uid, component.NoBatteryAlert);
+            _alerts.ShowAlert(uid, component.BatteryAlert, chargePercent);
         }
     }
 
diff --git a/Content.Server/Silicons/Borgs/BorgSystem.cs b/Content.Server/Silicons/Borgs/BorgSystem.cs
index 75f25a3a922..97adfd00eb4 100644
--- a/Content.Server/Silicons/Borgs/BorgSystem.cs
+++ b/Content.Server/Silicons/Borgs/BorgSystem.cs
@@ -84,7 +84,7 @@ public override void Initialize()
 
     private void OnMapInit(EntityUid uid, BorgChassisComponent component, MapInitEvent args)
     {
-        UpdateBatteryAlert(uid);
+        UpdateBatteryAlert((uid, component));
         _movementSpeedModifier.RefreshMovementSpeedModifiers(uid);
     }
 
@@ -183,7 +183,7 @@ private void OnMobStateChanged(EntityUid uid, BorgChassisComponent component, Mo
 
     private void OnPowerCellChanged(EntityUid uid, BorgChassisComponent component, PowerCellChangedEvent args)
     {
-        UpdateBatteryAlert(uid);
+        UpdateBatteryAlert((uid, component));
 
         if (!TryComp<PowerCellDrawComponent>(uid, out var draw))
             return;
@@ -256,12 +256,12 @@ private void OnBrainPointAttempt(EntityUid uid, BorgBrainComponent component, Po
         args.Cancel();
     }
 
-    private void UpdateBatteryAlert(EntityUid uid, PowerCellSlotComponent? slotComponent = null)
+    private void UpdateBatteryAlert(Entity<BorgChassisComponent> ent, PowerCellSlotComponent? slotComponent = null)
     {
-        if (!_powerCell.TryGetBatteryFromSlot(uid, out var battery, slotComponent))
+        if (!_powerCell.TryGetBatteryFromSlot(ent, out var battery, slotComponent))
         {
-            _alerts.ClearAlert(uid, AlertType.BorgBattery);
-            _alerts.ShowAlert(uid, AlertType.BorgBatteryNone);
+            _alerts.ClearAlert(ent, ent.Comp.BatteryAlert);
+            _alerts.ShowAlert(ent, ent.Comp.NoBatteryAlert);
             return;
         }
 
@@ -269,13 +269,13 @@ private void UpdateBatteryAlert(EntityUid uid, PowerCellSlotComponent? slotCompo
 
         // we make sure 0 only shows if they have absolutely no battery.
         // also account for floating point imprecision
-        if (chargePercent == 0 && _powerCell.HasDrawCharge(uid, cell: slotComponent))
+        if (chargePercent == 0 && _powerCell.HasDrawCharge(ent, cell: slotComponent))
         {
             chargePercent = 1;
         }
 
-        _alerts.ClearAlert(uid, AlertType.BorgBatteryNone);
-        _alerts.ShowAlert(uid, AlertType.BorgBattery, chargePercent);
+        _alerts.ClearAlert(ent, ent.Comp.NoBatteryAlert);
+        _alerts.ShowAlert(ent, ent.Comp.BatteryAlert, chargePercent);
     }
 
     /// <summary>
@@ -288,7 +288,7 @@ public void EnableBorgAbilities(EntityUid uid, BorgChassisComponent component, P
 
         component.Activated = true;
         InstallAllModules(uid, component);
-        Dirty(component);
+        Dirty(uid, component);
         _movementSpeedModifier.RefreshMovementSpeedModifiers(uid);
     }
 
@@ -302,7 +302,7 @@ public void DisableBorgAbilities(EntityUid uid, BorgChassisComponent component)
 
         component.Activated = false;
         DisableAllModules(uid, component);
-        Dirty(component);
+        Dirty(uid, component);
         _movementSpeedModifier.RefreshMovementSpeedModifiers(uid);
     }
 
diff --git a/Content.Server/Spawners/Components/EntityTableSpawnerComponent.cs b/Content.Server/Spawners/Components/EntityTableSpawnerComponent.cs
new file mode 100644
index 00000000000..d122eb098b6
--- /dev/null
+++ b/Content.Server/Spawners/Components/EntityTableSpawnerComponent.cs
@@ -0,0 +1,30 @@
+using Content.Server.Spawners.EntitySystems;
+using Content.Shared.EntityTable.EntitySelectors;
+using Robust.Shared.Prototypes;
+
+namespace Content.Server.Spawners.Components;
+
+[RegisterComponent, EntityCategory("Spawner"), Access(typeof(ConditionalSpawnerSystem))]
+public sealed partial class EntityTableSpawnerComponent : Component
+{
+    /// <summary>
+    /// Table that determines what gets spawned.
+    /// </summary>
+    [DataField(required: true)]
+    public EntityTableSelector Table = default!;
+
+    /// <summary>
+    /// Scatter of entity spawn coordinates
+    /// </summary>
+    [DataField]
+    public float Offset = 0.2f;
+
+    /// <summary>
+    /// A variable meaning whether the spawn will
+    /// be able to be used again or whether
+    /// it will be destroyed after the first use
+    /// </summary>
+    [DataField]
+    public bool DeleteSpawnerAfterSpawn = true;
+}
+
diff --git a/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs b/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs
index 75f86187989..f176f1b1358 100644
--- a/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs
+++ b/Content.Server/Spawners/EntitySystems/ConditionalSpawnerSystem.cs
@@ -1,9 +1,11 @@
 using System.Numerics;
 using Content.Server.GameTicking;
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Spawners.Components;
+using Content.Shared.EntityTable;
+using Content.Shared.GameTicking.Components;
 using JetBrains.Annotations;
+using Robust.Shared.Map;
 using Robust.Shared.Random;
 
 namespace Content.Server.Spawners.EntitySystems
@@ -13,6 +15,7 @@ public sealed class ConditionalSpawnerSystem : EntitySystem
     {
         [Dependency] private readonly IRobustRandom _robustRandom = default!;
         [Dependency] private readonly GameTicker _ticker = default!;
+        [Dependency] private readonly EntityTableSystem _entityTable = default!;
 
         public override void Initialize()
         {
@@ -21,6 +24,7 @@ public override void Initialize()
             SubscribeLocalEvent<GameRuleStartedEvent>(OnRuleStarted);
             SubscribeLocalEvent<ConditionalSpawnerComponent, MapInitEvent>(OnCondSpawnMapInit);
             SubscribeLocalEvent<RandomSpawnerComponent, MapInitEvent>(OnRandSpawnMapInit);
+            SubscribeLocalEvent<EntityTableSpawnerComponent, MapInitEvent>(OnEntityTableSpawnMapInit);
         }
 
         private void OnCondSpawnMapInit(EntityUid uid, ConditionalSpawnerComponent component, MapInitEvent args)
@@ -35,6 +39,13 @@ private void OnRandSpawnMapInit(EntityUid uid, RandomSpawnerComponent component,
                 QueueDel(uid);
         }
 
+        private void OnEntityTableSpawnMapInit(Entity<EntityTableSpawnerComponent> ent, ref MapInitEvent args)
+        {
+            Spawn(ent);
+            if (ent.Comp.DeleteSpawnerAfterSpawn && !TerminatingOrDeleted(ent) && Exists(ent))
+                QueueDel(ent);
+        }
+
         private void OnRuleStarted(ref GameRuleStartedEvent args)
         {
             var query = EntityQueryEnumerator<ConditionalSpawnerComponent>();
@@ -110,5 +121,23 @@ private void Spawn(EntityUid uid, RandomSpawnerComponent component)
 
             EntityManager.SpawnEntity(_robustRandom.Pick(component.Prototypes), coordinates);
         }
+
+        private void Spawn(Entity<EntityTableSpawnerComponent> ent)
+        {
+            if (TerminatingOrDeleted(ent) || !Exists(ent))
+                return;
+
+            var coords = Transform(ent).Coordinates;
+
+            var spawns = _entityTable.GetSpawns(ent.Comp.Table);
+            foreach (var proto in spawns)
+            {
+                var xOffset = _robustRandom.NextFloat(-ent.Comp.Offset, ent.Comp.Offset);
+                var yOffset = _robustRandom.NextFloat(-ent.Comp.Offset, ent.Comp.Offset);
+                var trueCoords = coords.Offset(new Vector2(xOffset, yOffset));
+
+                Spawn(proto, trueCoords);
+            }
+        }
     }
 }
diff --git a/Content.Server/Sprite/RandomSpriteSystem.cs b/Content.Server/Sprite/RandomSpriteSystem.cs
index 5d04dd2f5a6..7f81f4bdd45 100644
--- a/Content.Server/Sprite/RandomSpriteSystem.cs
+++ b/Content.Server/Sprite/RandomSpriteSystem.cs
@@ -63,7 +63,7 @@ private void OnMapInit(EntityUid uid, RandomSpriteComponent component, MapInitEv
             }
         }
 
-        Dirty(component);
+        Dirty(uid, component);
     }
 
     private void OnGetState(EntityUid uid, RandomSpriteComponent component, ref ComponentGetState args)
diff --git a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs
index b9eb3b7b09d..f2704d53f4c 100644
--- a/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs
+++ b/Content.Server/StationEvents/BasicStationEventSchedulerSystem.cs
@@ -1,11 +1,11 @@
 using System.Linq;
 using Content.Server.Administration;
-using Content.Server.GameTicking.Components;
+using Content.Server.GameTicking;
 using Content.Server.GameTicking.Rules;
-using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
 using Content.Shared.Administration;
 using Content.Shared.CCVar;
+using Content.Shared.GameTicking.Components;
 using JetBrains.Annotations;
 using Robust.Shared.Configuration;
 using Robust.Shared.Random;
diff --git a/Content.Server/StationEvents/Events/AlertLevelInterceptionRule.cs b/Content.Server/StationEvents/Events/AlertLevelInterceptionRule.cs
index a78a542d3b3..916d7d16883 100644
--- a/Content.Server/StationEvents/Events/AlertLevelInterceptionRule.cs
+++ b/Content.Server/StationEvents/Events/AlertLevelInterceptionRule.cs
@@ -1,6 +1,6 @@
-using Content.Server.GameTicking.Components;
 using Content.Server.StationEvents.Components;
 using Content.Server.AlertLevel;
+using Content.Shared.GameTicking.Components;
 
 namespace Content.Server.StationEvents.Events;
 
@@ -20,4 +20,4 @@ protected override void Started(EntityUid uid, AlertLevelInterceptionRuleCompone
 
         _alertLevelSystem.SetLevel(chosenStation.Value, component.AlertLevel, true, true, true);
     }
-}
\ No newline at end of file
+}
diff --git a/Content.Server/StationEvents/Events/AnomalySpawnRule.cs b/Content.Server/StationEvents/Events/AnomalySpawnRule.cs
index 98d5aa76a6a..aa2fa74c483 100644
--- a/Content.Server/StationEvents/Events/AnomalySpawnRule.cs
+++ b/Content.Server/StationEvents/Events/AnomalySpawnRule.cs
@@ -1,9 +1,8 @@
 using Content.Server.Anomaly;
-using Content.Server.GameTicking.Components;
-using Content.Server.GameTicking.Rules.Components;
+using Content.Server.Announcements.Systems;
 using Content.Server.Station.Components;
 using Content.Server.StationEvents.Components;
-using Content.Server.Announcements.Systems;
+using Content.Shared.GameTicking.Components;
 using Robust.Shared.Player;
 
 namespace Content.Server.StationEvents.Events;
diff --git a/Content.Server/StationEvents/Events/BluespaceArtifactRule.cs b/Content.Server/StationEvents/Events/BluespaceArtifactRule.cs
index 29c18976576..3983981ff46 100644
--- a/Content.Server/StationEvents/Events/BluespaceArtifactRule.cs
+++ b/Content.Server/StationEvents/Events/BluespaceArtifactRule.cs
@@ -1,6 +1,5 @@
-using Content.Server.GameTicking.Components;
-using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
+using Content.Shared.GameTicking.Components;
 using Robust.Shared.Random;
 using Content.Server.Announcements.Systems;
 using Robust.Shared.Player;
diff --git a/Content.Server/StationEvents/Events/BluespaceLockerRule.cs b/Content.Server/StationEvents/Events/BluespaceLockerRule.cs
index eef9850e739..b19485bc31e 100644
--- a/Content.Server/StationEvents/Events/BluespaceLockerRule.cs
+++ b/Content.Server/StationEvents/Events/BluespaceLockerRule.cs
@@ -1,4 +1,3 @@
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Resist;
 using Content.Server.Station.Components;
@@ -6,6 +5,7 @@
 using Content.Server.Storage.Components;
 using Content.Server.Storage.EntitySystems;
 using Content.Shared.Access.Components;
+using Content.Shared.GameTicking.Components;
 using Content.Shared.Coordinates;
 
 namespace Content.Server.StationEvents.Events;
diff --git a/Content.Server/StationEvents/Events/BreakerFlipRule.cs b/Content.Server/StationEvents/Events/BreakerFlipRule.cs
index 3b2368556be..bc657657f66 100644
--- a/Content.Server/StationEvents/Events/BreakerFlipRule.cs
+++ b/Content.Server/StationEvents/Events/BreakerFlipRule.cs
@@ -1,9 +1,8 @@
-using Content.Server.GameTicking.Components;
-using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Power.Components;
 using Content.Server.Power.EntitySystems;
 using Content.Server.Station.Components;
 using Content.Server.StationEvents.Components;
+using Content.Shared.GameTicking.Components;
 using JetBrains.Annotations;
 using Content.Server.Announcements.Systems;
 using Robust.Shared.Player;
diff --git a/Content.Server/StationEvents/Events/BureaucraticErrorRule.cs b/Content.Server/StationEvents/Events/BureaucraticErrorRule.cs
index 282e28e4991..6600b0623fd 100644
--- a/Content.Server/StationEvents/Events/BureaucraticErrorRule.cs
+++ b/Content.Server/StationEvents/Events/BureaucraticErrorRule.cs
@@ -1,9 +1,9 @@
 using System.Linq;
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Station.Components;
 using Content.Server.Station.Systems;
 using Content.Server.StationEvents.Components;
+using Content.Shared.GameTicking.Components;
 using JetBrains.Annotations;
 using Robust.Shared.Random;
 
diff --git a/Content.Server/StationEvents/Events/CargoGiftsRule.cs b/Content.Server/StationEvents/Events/CargoGiftsRule.cs
index 550f799b27e..b2170e06625 100644
--- a/Content.Server/StationEvents/Events/CargoGiftsRule.cs
+++ b/Content.Server/StationEvents/Events/CargoGiftsRule.cs
@@ -2,10 +2,9 @@
 using Content.Server.Cargo.Components;
 using Content.Server.Cargo.Systems;
 using Content.Server.GameTicking;
-using Content.Server.GameTicking.Components;
-using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Station.Components;
 using Content.Server.StationEvents.Components;
+using Content.Shared.GameTicking.Components;
 using Robust.Shared.Prototypes;
 using Content.Server.Announcements.Systems;
 using Robust.Shared.Player;
diff --git a/Content.Server/StationEvents/Events/ClericalErrorRule.cs b/Content.Server/StationEvents/Events/ClericalErrorRule.cs
index 854ee685b33..e52c2c05aaf 100644
--- a/Content.Server/StationEvents/Events/ClericalErrorRule.cs
+++ b/Content.Server/StationEvents/Events/ClericalErrorRule.cs
@@ -1,9 +1,9 @@
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
 using Content.Server.StationRecords;
 using Content.Server.StationRecords.Systems;
 using Content.Shared.StationRecords;
+using Content.Shared.GameTicking.Components;
 using Robust.Shared.Random;
 
 namespace Content.Server.StationEvents.Events;
diff --git a/Content.Server/StationEvents/Events/FalseAlarmRule.cs b/Content.Server/StationEvents/Events/FalseAlarmRule.cs
index 2d129b35584..8c8bf0aadc9 100644
--- a/Content.Server/StationEvents/Events/FalseAlarmRule.cs
+++ b/Content.Server/StationEvents/Events/FalseAlarmRule.cs
@@ -1,7 +1,6 @@
 using System.Linq;
-using Content.Server.GameTicking.Components;
-using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
+using Content.Shared.GameTicking.Components;
 using JetBrains.Annotations;
 using Robust.Shared.Player;
 using Robust.Shared.Random;
diff --git a/Content.Server/StationEvents/Events/FreeProberRule.cs b/Content.Server/StationEvents/Events/FreeProberRule.cs
index a5dfdd6b6ea..04795bdf4e9 100644
--- a/Content.Server/StationEvents/Events/FreeProberRule.cs
+++ b/Content.Server/StationEvents/Events/FreeProberRule.cs
@@ -1,4 +1,4 @@
-using Content.Server.GameTicking.Components;
+using Content.Shared.GameTicking.Components;
 using Robust.Shared.Map;
 using Robust.Shared.Random;
 using Content.Server.GameTicking.Rules.Components;
@@ -8,13 +8,14 @@
 using Content.Server.Psionics.Glimmer;
 using Content.Shared.Construction.EntitySystems;
 using Content.Shared.Psionics.Glimmer;
+using Robust.Shared.Map.Components;
 
 namespace Content.Server.StationEvents.Events;
 
 internal sealed class FreeProberRule : StationEventSystem<FreeProberRuleComponent>
 {
     [Dependency] private readonly IRobustRandom _robustRandom = default!;
-    [Dependency] private readonly IMapManager _mapManager = default!;
+    [Dependency] private readonly SharedMapSystem _sharedMapSystem = default!;
     [Dependency] private readonly AnchorableSystem _anchorable = default!;
     [Dependency] private readonly GlimmerSystem _glimmerSystem = default!;
     [Dependency] private readonly StationSystem _stationSystem = default!;
@@ -59,10 +60,14 @@ protected override void Started(EntityUid uid, FreeProberRuleComponent component
 
                 var coordinates = xform.Coordinates;
                 var gridUid = xform.GridUid;
-                if (!_mapManager.TryGetGrid(gridUid, out var grid))
+
+                if (gridUid == null)
+                    continue;
+
+                if (!TryComp<MapGridComponent>(gridUid, out var grid))
                     continue;
 
-                var tileIndices = grid.TileIndicesFor(coordinates);
+                var tileIndices = _sharedMapSystem.TileIndicesFor((EntityUid) gridUid, grid, coordinates);
 
                 for (var i = 0; i < SpawnDirections; i++)
                 {
@@ -73,7 +78,7 @@ protected override void Started(EntityUid uid, FreeProberRuleComponent component
                     if (!_anchorable.TileFree(grid, offsetIndices))
                         continue;
 
-                    Spawn(ProberPrototype, grid.GridTileToLocal(offsetIndices));
+                    Spawn(ProberPrototype, _sharedMapSystem.GridTileToLocal((EntityUid) gridUid, grid, offsetIndices));
                     return;
                 }
             }
diff --git a/Content.Server/StationEvents/Events/GasLeakRule.cs b/Content.Server/StationEvents/Events/GasLeakRule.cs
index 1221612171d..391c407bacd 100644
--- a/Content.Server/StationEvents/Events/GasLeakRule.cs
+++ b/Content.Server/StationEvents/Events/GasLeakRule.cs
@@ -1,7 +1,7 @@
 using Content.Server.Atmos.EntitySystems;
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
+using Content.Shared.GameTicking.Components;
 using Robust.Shared.Audio;
 using Robust.Shared.Random;
 using Robust.Shared.Timing;
diff --git a/Content.Server/StationEvents/Events/GlimmerEventSystem.cs b/Content.Server/StationEvents/Events/GlimmerEventSystem.cs
index 3e0762c8346..37eb0410fbd 100644
--- a/Content.Server/StationEvents/Events/GlimmerEventSystem.cs
+++ b/Content.Server/StationEvents/Events/GlimmerEventSystem.cs
@@ -1,4 +1,4 @@
-using Content.Server.GameTicking.Components;
+using Content.Shared.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Psionics.Glimmer;
 using Content.Shared.Psionics.Glimmer;
diff --git a/Content.Server/StationEvents/Events/GlimmerMobSpawnRule.cs b/Content.Server/StationEvents/Events/GlimmerMobSpawnRule.cs
index 702147842c6..f80bc83a1e6 100644
--- a/Content.Server/StationEvents/Events/GlimmerMobSpawnRule.cs
+++ b/Content.Server/StationEvents/Events/GlimmerMobSpawnRule.cs
@@ -1,5 +1,5 @@
 using System.Linq;
-using Content.Server.GameTicking.Components;
+using Content.Shared.GameTicking.Components;
 using Robust.Shared.Random;
 using Content.Server.GameTicking;
 using Content.Server.NPC.Components;
diff --git a/Content.Server/StationEvents/Events/GlimmerRandomSentienceRule.cs b/Content.Server/StationEvents/Events/GlimmerRandomSentienceRule.cs
index a288710356a..ee0de6fe01a 100644
--- a/Content.Server/StationEvents/Events/GlimmerRandomSentienceRule.cs
+++ b/Content.Server/StationEvents/Events/GlimmerRandomSentienceRule.cs
@@ -1,5 +1,5 @@
 using System.Linq;
-using Content.Server.GameTicking.Components;
+using Content.Shared.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Ghost.Roles.Components;
 using Content.Shared.Abilities.Psionics;
diff --git a/Content.Server/StationEvents/Events/GlimmerRevenantSpawnRule.cs b/Content.Server/StationEvents/Events/GlimmerRevenantSpawnRule.cs
index 152d6d9fe59..dfb7653303b 100644
--- a/Content.Server/StationEvents/Events/GlimmerRevenantSpawnRule.cs
+++ b/Content.Server/StationEvents/Events/GlimmerRevenantSpawnRule.cs
@@ -1,4 +1,4 @@
-using Content.Server.GameTicking.Components;
+using Content.Shared.GameTicking.Components;
 using Robust.Shared.Random;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Psionics.Glimmer;
diff --git a/Content.Server/StationEvents/Events/ImmovableRodRule.cs b/Content.Server/StationEvents/Events/ImmovableRodRule.cs
index 45d6c18189c..37f912773c5 100644
--- a/Content.Server/StationEvents/Events/ImmovableRodRule.cs
+++ b/Content.Server/StationEvents/Events/ImmovableRodRule.cs
@@ -1,10 +1,10 @@
 using System.Numerics;
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.ImmovableRod;
 using Content.Server.StationEvents.Components;
 using Content.Server.Weapons.Ranged.Systems;
-using Robust.Shared.Spawners;
+using Content.Shared.GameTicking.Components;
+using Content.Shared.Storage;
 using Robust.Shared.Prototypes;
 using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent;
 
diff --git a/Content.Server/StationEvents/Events/IonStormRule.cs b/Content.Server/StationEvents/Events/IonStormRule.cs
index 8361cc6048a..926ecc2db92 100644
--- a/Content.Server/StationEvents/Events/IonStormRule.cs
+++ b/Content.Server/StationEvents/Events/IonStormRule.cs
@@ -1,4 +1,3 @@
-using Content.Server.GameTicking.Components;
 using System.Linq;
 using Content.Server.Silicons.Laws;
 using Content.Server.Station.Components;
@@ -7,6 +6,7 @@
 using Content.Shared.Database;
 using Content.Shared.Dataset;
 using Content.Shared.FixedPoint;
+using Content.Shared.GameTicking.Components;
 using Content.Shared.Random;
 using Content.Shared.Random.Helpers;
 using Content.Shared.Silicons.Laws;
diff --git a/Content.Server/StationEvents/Events/KudzuGrowthRule.cs b/Content.Server/StationEvents/Events/KudzuGrowthRule.cs
index 5b56e03846f..42c57ffcaae 100644
--- a/Content.Server/StationEvents/Events/KudzuGrowthRule.cs
+++ b/Content.Server/StationEvents/Events/KudzuGrowthRule.cs
@@ -1,6 +1,6 @@
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
+using Content.Shared.GameTicking.Components;
 
 namespace Content.Server.StationEvents.Events;
 
diff --git a/Content.Server/StationEvents/Events/MassHallucinationsRule.cs b/Content.Server/StationEvents/Events/MassHallucinationsRule.cs
index 2239db7f701..c35ba94727f 100644
--- a/Content.Server/StationEvents/Events/MassHallucinationsRule.cs
+++ b/Content.Server/StationEvents/Events/MassHallucinationsRule.cs
@@ -1,7 +1,7 @@
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
 using Content.Server.Traits.Assorted;
+using Content.Shared.GameTicking.Components;
 using Content.Shared.Mind.Components;
 using Content.Shared.Traits.Assorted.Components;
 
diff --git a/Content.Server/StationEvents/Events/MassMindSwapRule.cs b/Content.Server/StationEvents/Events/MassMindSwapRule.cs
index beb08eb8a79..0839b217293 100644
--- a/Content.Server/StationEvents/Events/MassMindSwapRule.cs
+++ b/Content.Server/StationEvents/Events/MassMindSwapRule.cs
@@ -1,7 +1,7 @@
 using Robust.Server.GameObjects;
 using Robust.Shared.Random;
 using Content.Server.Abilities.Psionics;
-using Content.Server.GameTicking.Components;
+using Content.Shared.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Psionics;
 using Content.Server.StationEvents.Components;
diff --git a/Content.Server/StationEvents/Events/MeteorSwarmRule.cs b/Content.Server/StationEvents/Events/MeteorSwarmRule.cs
index 455011259dc..b97cde86a02 100644
--- a/Content.Server/StationEvents/Events/MeteorSwarmRule.cs
+++ b/Content.Server/StationEvents/Events/MeteorSwarmRule.cs
@@ -1,7 +1,7 @@
 using System.Numerics;
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
+using Content.Shared.GameTicking.Components;
 using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
 using Robust.Shared.Physics.Components;
diff --git a/Content.Server/StationEvents/Events/NinjaSpawnRule.cs b/Content.Server/StationEvents/Events/NinjaSpawnRule.cs
index d9d68a386cf..9cbc193ce61 100644
--- a/Content.Server/StationEvents/Events/NinjaSpawnRule.cs
+++ b/Content.Server/StationEvents/Events/NinjaSpawnRule.cs
@@ -1,8 +1,8 @@
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Ninja.Systems;
 using Content.Server.Station.Components;
 using Content.Server.StationEvents.Components;
+using Content.Shared.GameTicking.Components;
 using Robust.Shared.Map;
 using Robust.Shared.Map.Components;
 
diff --git a/Content.Server/StationEvents/Events/NoosphericFryRule.cs b/Content.Server/StationEvents/Events/NoosphericFryRule.cs
index 85f98d6f4be..7e025f61e17 100644
--- a/Content.Server/StationEvents/Events/NoosphericFryRule.cs
+++ b/Content.Server/StationEvents/Events/NoosphericFryRule.cs
@@ -3,7 +3,7 @@
 using Robust.Shared.Player;
 using Content.Server.Atmos.Components;
 using Content.Server.Atmos.EntitySystems;
-using Content.Server.GameTicking.Components;
+using Content.Shared.GameTicking.Components;
 using Content.Shared.Construction.EntitySystems;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Popups;
@@ -18,6 +18,7 @@
 using Content.Shared.Mobs.Systems;
 using Content.Shared.Psionics.Glimmer;
 using Robust.Shared.Audio.Systems;
+using Robust.Shared.Map.Components;
 
 namespace Content.Server.StationEvents.Events;
 
@@ -26,7 +27,7 @@ namespace Content.Server.StationEvents.Events;
 /// </summary>
 internal sealed class NoosphericFryRule : StationEventSystem<NoosphericFryRuleComponent>
 {
-    [Dependency] private readonly IMapManager _mapManager = default!;
+    [Dependency] private readonly SharedMapSystem _sharedMapSystem = default!;
     [Dependency] private readonly MobStateSystem _mobStateSystem = default!;
     [Dependency] private readonly InventorySystem _inventorySystem = default!;
     [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
@@ -111,10 +112,14 @@ protected override void Started(EntityUid uid, NoosphericFryRuleComponent compon
             {
                 var coordinates = xform.Coordinates;
                 var gridUid = xform.GridUid;
-                if (!_mapManager.TryGetGrid(gridUid, out var grid))
+
+                if (gridUid == null)
+                    continue;
+
+                if (!TryComp<MapGridComponent>(gridUid, out var grid))
                     continue;
 
-                var tileIndices = grid.TileIndicesFor(coordinates);
+                var tileIndices = _sharedMapSystem.TileIndicesFor((EntityUid) gridUid, grid, coordinates);
 
                 if (_anchorableSystem.TileFree(grid, tileIndices, physics.CollisionLayer, physics.CollisionMask))
                     _transformSystem.AnchorEntity(reactive, xform);
diff --git a/Content.Server/StationEvents/Events/NoosphericStormRule.cs b/Content.Server/StationEvents/Events/NoosphericStormRule.cs
index 1de8bad89b5..b2777346ae0 100644
--- a/Content.Server/StationEvents/Events/NoosphericStormRule.cs
+++ b/Content.Server/StationEvents/Events/NoosphericStormRule.cs
@@ -1,6 +1,6 @@
 using Robust.Shared.Random;
 using Content.Server.Abilities.Psionics;
-using Content.Server.GameTicking.Components;
+using Content.Shared.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
 using Content.Server.Psionics;
diff --git a/Content.Server/StationEvents/Events/NoosphericZapRule.cs b/Content.Server/StationEvents/Events/NoosphericZapRule.cs
index 96c33612036..3819d1203a0 100644
--- a/Content.Server/StationEvents/Events/NoosphericZapRule.cs
+++ b/Content.Server/StationEvents/Events/NoosphericZapRule.cs
@@ -1,4 +1,4 @@
-using Content.Server.GameTicking.Components;
+using Content.Shared.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Popups;
 using Content.Server.Psionics;
diff --git a/Content.Server/StationEvents/Events/PirateRadioSpawnRule.cs b/Content.Server/StationEvents/Events/PirateRadioSpawnRule.cs
index e6d36839f92..51a4438583f 100644
--- a/Content.Server/StationEvents/Events/PirateRadioSpawnRule.cs
+++ b/Content.Server/StationEvents/Events/PirateRadioSpawnRule.cs
@@ -8,7 +8,7 @@
 using Content.Shared.Salvage;
 using Content.Shared.Random.Helpers;
 using System.Linq;
-using Content.Server.GameTicking.Components;
+using Content.Shared.GameTicking.Components;
 using Content.Shared.CCVar;
 using Robust.Shared.Serialization.Manager;
 using Content.Shared.Parallax.Biomes;
diff --git a/Content.Server/StationEvents/Events/PowerGridCheckRule.cs b/Content.Server/StationEvents/Events/PowerGridCheckRule.cs
index b0a0bbc9fe0..e09c88673fb 100644
--- a/Content.Server/StationEvents/Events/PowerGridCheckRule.cs
+++ b/Content.Server/StationEvents/Events/PowerGridCheckRule.cs
@@ -1,10 +1,10 @@
 using System.Threading;
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Power.Components;
 using Content.Server.Power.EntitySystems;
 using Content.Server.Station.Components;
 using Content.Server.StationEvents.Components;
+using Content.Shared.GameTicking.Components;
 using JetBrains.Annotations;
 using Robust.Shared.Audio;
 using Robust.Shared.Player;
diff --git a/Content.Server/StationEvents/Events/PsionicCatGotYourTongueRule.cs b/Content.Server/StationEvents/Events/PsionicCatGotYourTongueRule.cs
index b92097b28d0..f39d87d4e9d 100644
--- a/Content.Server/StationEvents/Events/PsionicCatGotYourTongueRule.cs
+++ b/Content.Server/StationEvents/Events/PsionicCatGotYourTongueRule.cs
@@ -1,4 +1,4 @@
-using Content.Server.GameTicking.Components;
+using Content.Shared.GameTicking.Components;
 using Robust.Shared.Random;
 using Robust.Shared.Player;
 using Content.Server.GameTicking.Rules.Components;
diff --git a/Content.Server/StationEvents/Events/RandomEntityStorageSpawnRule.cs b/Content.Server/StationEvents/Events/RandomEntityStorageSpawnRule.cs
index 87d50fc8b2a..a9f27938180 100644
--- a/Content.Server/StationEvents/Events/RandomEntityStorageSpawnRule.cs
+++ b/Content.Server/StationEvents/Events/RandomEntityStorageSpawnRule.cs
@@ -1,8 +1,8 @@
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
 using Content.Server.Storage.Components;
 using Content.Server.Storage.EntitySystems;
+using Content.Shared.GameTicking.Components;
 using Robust.Shared.Map;
 using Robust.Shared.Random;
 
diff --git a/Content.Server/StationEvents/Events/RandomSentienceRule.cs b/Content.Server/StationEvents/Events/RandomSentienceRule.cs
index 7b9173241f7..2fb733e1a67 100644
--- a/Content.Server/StationEvents/Events/RandomSentienceRule.cs
+++ b/Content.Server/StationEvents/Events/RandomSentienceRule.cs
@@ -1,10 +1,9 @@
 using System.Linq;
-using Content.Server.GameTicking.Components;
-using Content.Server.GameTicking.Rules.Components;
-using Content.Server.Ghost.Roles.Components;
-using Content.Server.StationEvents.Components;
 using Content.Server.Announcements.Systems;
+using Content.Server.Ghost.Roles.Components;
 using Content.Server.Station.Components;
+using Content.Server.StationEvents.Components;
+using Content.Shared.GameTicking.Components;
 
 namespace Content.Server.StationEvents.Events;
 
diff --git a/Content.Server/StationEvents/Events/RandomSpawnRule.cs b/Content.Server/StationEvents/Events/RandomSpawnRule.cs
index 77744d44e46..e904c24ba60 100644
--- a/Content.Server/StationEvents/Events/RandomSpawnRule.cs
+++ b/Content.Server/StationEvents/Events/RandomSpawnRule.cs
@@ -1,6 +1,6 @@
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
+using Content.Shared.GameTicking.Components;
 
 namespace Content.Server.StationEvents.Events;
 
diff --git a/Content.Server/StationEvents/Events/SolarFlareRule.cs b/Content.Server/StationEvents/Events/SolarFlareRule.cs
index 0370b4ee61d..19f6e393d20 100644
--- a/Content.Server/StationEvents/Events/SolarFlareRule.cs
+++ b/Content.Server/StationEvents/Events/SolarFlareRule.cs
@@ -1,4 +1,3 @@
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Radio;
 using Robust.Shared.Random;
@@ -8,6 +7,7 @@
 using Content.Shared.Radio.Components;
 using Content.Shared.Doors.Components;
 using Content.Shared.Doors.Systems;
+using Content.Shared.GameTicking.Components;
 
 namespace Content.Server.StationEvents.Events;
 
diff --git a/Content.Server/StationEvents/Events/StationEventSystem.cs b/Content.Server/StationEvents/Events/StationEventSystem.cs
index 040ebad2260..a88258b9ad9 100644
--- a/Content.Server/StationEvents/Events/StationEventSystem.cs
+++ b/Content.Server/StationEvents/Events/StationEventSystem.cs
@@ -1,12 +1,12 @@
 using System.Linq;
 using Content.Server.Administration.Logs;
 using Content.Server.Chat.Systems;
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Station.Systems;
 using Content.Server.StationEvents.Components;
 using Content.Shared.Database;
+using Content.Shared.GameTicking.Components;
 using Robust.Shared.Audio.Systems;
 using Robust.Shared.Map;
 using Robust.Shared.Player;
diff --git a/Content.Server/StationEvents/Events/VentClogRule.cs b/Content.Server/StationEvents/Events/VentClogRule.cs
index 867f41dcccf..043ea0375aa 100644
--- a/Content.Server/StationEvents/Events/VentClogRule.cs
+++ b/Content.Server/StationEvents/Events/VentClogRule.cs
@@ -6,9 +6,9 @@
 using Robust.Shared.Random;
 using System.Linq;
 using Content.Server.Fluids.EntitySystems;
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
+using Content.Shared.GameTicking.Components;
 
 namespace Content.Server.StationEvents.Events;
 
diff --git a/Content.Server/StationEvents/Events/VentCrittersRule.cs b/Content.Server/StationEvents/Events/VentCrittersRule.cs
index c2605039bce..fba9cfa6a60 100644
--- a/Content.Server/StationEvents/Events/VentCrittersRule.cs
+++ b/Content.Server/StationEvents/Events/VentCrittersRule.cs
@@ -1,7 +1,7 @@
-using Content.Server.GameTicking.Components;
 using Content.Server.StationEvents.Components;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.Station.Components;
+using Content.Shared.GameTicking.Components;
 using Content.Shared.Storage;
 using Robust.Shared.Map;
 using Robust.Shared.Random;
diff --git a/Content.Server/StationEvents/OscillatingStationEventScheduler.cs b/Content.Server/StationEvents/OscillatingStationEventScheduler.cs
index 1e6dbd14a18..a9b9de586ee 100644
--- a/Content.Server/StationEvents/OscillatingStationEventScheduler.cs
+++ b/Content.Server/StationEvents/OscillatingStationEventScheduler.cs
@@ -2,7 +2,7 @@
 using System.Diagnostics.CodeAnalysis;
 using Content.Server.GameTicking;
 using Content.Server.GameTicking.Rules;
-using Content.Server.GameTicking.Components;
+using Content.Shared.GameTicking.Components;
 using Content.Server.StationEvents.Components;
 using Content.Shared.CCVar;
 using Robust.Shared.Configuration;
diff --git a/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs b/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs
index a6c38ef765f..545064b39fe 100644
--- a/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs
+++ b/Content.Server/StationEvents/RampingStationEventSchedulerSystem.cs
@@ -1,10 +1,10 @@
 using Content.Server.GameTicking;
-using Content.Server.GameTicking.Components;
 using Content.Server.GameTicking.Rules;
 using Content.Server.GameTicking.Rules.Components;
 using Content.Server.StationEvents.Components;
 using Content.Server.StationEvents.Events;
 using Content.Shared.CCVar;
+using Content.Shared.GameTicking.Components;
 using Robust.Shared.Configuration;
 using Robust.Shared.Random;
 
diff --git a/Content.Server/Temperature/Components/TemperatureComponent.cs b/Content.Server/Temperature/Components/TemperatureComponent.cs
index ec00a570f96..3bfa12f2693 100644
--- a/Content.Server/Temperature/Components/TemperatureComponent.cs
+++ b/Content.Server/Temperature/Components/TemperatureComponent.cs
@@ -1,7 +1,9 @@
 using Content.Server.Temperature.Systems;
+using Content.Shared.Alert;
 using Content.Shared.Atmos;
 using Content.Shared.Damage;
 using Content.Shared.FixedPoint;
+using Robust.Shared.Prototypes;
 
 namespace Content.Server.Temperature.Components;
 
@@ -78,4 +80,10 @@ public float HeatCapacity
     /// </summary>
     [DataField]
     public bool TakingDamage = false;
+
+    [DataField]
+    public ProtoId<AlertPrototype> HotAlert = "Hot";
+
+    [DataField]
+    public ProtoId<AlertPrototype> ColdAlert = "Cold";
 }
diff --git a/Content.Server/Temperature/Systems/TemperatureSystem.cs b/Content.Server/Temperature/Systems/TemperatureSystem.cs
index 0f57da4b881..2f4497bdbbc 100644
--- a/Content.Server/Temperature/Systems/TemperatureSystem.cs
+++ b/Content.Server/Temperature/Systems/TemperatureSystem.cs
@@ -12,6 +12,7 @@
 using Content.Shared.Rejuvenate;
 using Content.Shared.Temperature;
 using Robust.Shared.Physics.Components;
+using Robust.Shared.Prototypes;
 
 namespace Content.Server.Temperature.Systems;
 
@@ -33,6 +34,9 @@ public sealed class TemperatureSystem : EntitySystem
 
     private float _accumulatedFrametime;
 
+    [ValidatePrototypeId<AlertCategoryPrototype>]
+    public const string TemperatureAlertCategory = "Temperature";
+
     public override void Initialize()
     {
         SubscribeLocalEvent<TemperatureComponent, OnTemperatureChangeEvent>(EnqueueDamage);
@@ -181,13 +185,13 @@ private void OnRejuvenate(EntityUid uid, TemperatureComponent comp, RejuvenateEv
 
     private void ServerAlert(EntityUid uid, AlertsComponent status, OnTemperatureChangeEvent args)
     {
-        AlertType type;
+        ProtoId<AlertPrototype> type;
         float threshold;
         float idealTemp;
 
         if (!TryComp<TemperatureComponent>(uid, out var temperature))
         {
-            _alerts.ClearAlertCategory(uid, AlertCategory.Temperature);
+            _alerts.ClearAlertCategory(uid, TemperatureAlertCategory);
             return;
         }
 
@@ -204,12 +208,12 @@ private void ServerAlert(EntityUid uid, AlertsComponent status, OnTemperatureCha
 
         if (args.CurrentTemperature <= idealTemp)
         {
-            type = AlertType.Cold;
+            type = temperature.ColdAlert;
             threshold = temperature.ColdDamageThreshold;
         }
         else
         {
-            type = AlertType.Hot;
+            type = temperature.HotAlert;
             threshold = temperature.HeatDamageThreshold;
         }
 
@@ -231,7 +235,7 @@ private void ServerAlert(EntityUid uid, AlertsComponent status, OnTemperatureCha
                 break;
 
             case > 0.66f:
-                _alerts.ClearAlertCategory(uid, AlertCategory.Temperature);
+                _alerts.ClearAlertCategory(uid, TemperatureAlertCategory);
                 break;
         }
     }
diff --git a/Content.Server/Traits/Assorted/ForeignerTraitComponent.cs b/Content.Server/Traits/Assorted/ForeignerTraitComponent.cs
index a35972f1c86..18d1842a87d 100644
--- a/Content.Server/Traits/Assorted/ForeignerTraitComponent.cs
+++ b/Content.Server/Traits/Assorted/ForeignerTraitComponent.cs
@@ -34,5 +34,5 @@ public sealed partial class ForeignerTraitComponent : Component
     ///     The base translator prototype to use when creating a translator for the entity.
     /// </summary>
     [DataField(required: true)]
-    public ProtoId<EntityPrototype> BaseTranslator = default!;
+    public EntProtoId BaseTranslator = default!;
 }
diff --git a/Content.Server/Traits/Assorted/ParacusiaSystem.cs b/Content.Server/Traits/Assorted/ParacusiaSystem.cs
index cf08e09e90e..d92e200a4fa 100644
--- a/Content.Server/Traits/Assorted/ParacusiaSystem.cs
+++ b/Content.Server/Traits/Assorted/ParacusiaSystem.cs
@@ -14,7 +14,7 @@ public void SetSounds(EntityUid uid, SoundSpecifier sounds, ParacusiaComponent?
             return;
         }
         component.Sounds = sounds;
-        Dirty(component);
+        Dirty(uid, component);
     }
 
     public void SetTime(EntityUid uid, float minTime, float maxTime, ParacusiaComponent? component = null)
@@ -25,7 +25,7 @@ public void SetTime(EntityUid uid, float minTime, float maxTime, ParacusiaCompon
         }
         component.MinTimeBetweenIncidents = minTime;
         component.MaxTimeBetweenIncidents = maxTime;
-        Dirty(component);
+        Dirty(uid, component);
     }
 
     public void SetDistance(EntityUid uid, float maxSoundDistance, ParacusiaComponent? component = null)
@@ -35,6 +35,6 @@ public void SetDistance(EntityUid uid, float maxSoundDistance, ParacusiaComponen
             return;
         }
         component.MaxSoundDistance = maxSoundDistance;
-        Dirty(component);
+        Dirty(uid, component);
     }
 }
diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.Battery.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.Battery.cs
index 25010b22333..0dcd92f9417 100644
--- a/Content.Server/Weapons/Ranged/Systems/GunSystem.Battery.cs
+++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.Battery.cs
@@ -51,7 +51,7 @@ private void UpdateShots(EntityUid uid, BatteryAmmoProviderComponent component,
 
         if (component.Shots != shots || component.Capacity != maxShots)
         {
-            Dirty(component);
+            Dirty(uid, component);
         }
 
         component.Shots = shots;
diff --git a/Content.Server/Weapons/Ranged/Systems/GunSystem.Revolver.cs b/Content.Server/Weapons/Ranged/Systems/GunSystem.Revolver.cs
index 6ff47507299..59e53f1f72a 100644
--- a/Content.Server/Weapons/Ranged/Systems/GunSystem.Revolver.cs
+++ b/Content.Server/Weapons/Ranged/Systems/GunSystem.Revolver.cs
@@ -13,6 +13,6 @@ protected override void SpinRevolver(EntityUid revolverUid, RevolverAmmoProvider
             return;
 
         component.CurrentIndex = index;
-        Dirty(component);
+        Dirty(revolverUid, component);
     }
 }
diff --git a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/RandomInstrumentArtifactSystem.cs b/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/RandomInstrumentArtifactSystem.cs
index 8945b867954..118bc396a72 100644
--- a/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/RandomInstrumentArtifactSystem.cs
+++ b/Content.Server/Xenoarchaeology/XenoArtifacts/Effects/Systems/RandomInstrumentArtifactSystem.cs
@@ -18,6 +18,6 @@ public override void Initialize()
     private void OnStartup(EntityUid uid, RandomInstrumentArtifactComponent component, ComponentStartup args)
     {
         var instrument = EnsureComp<InstrumentComponent>(uid);
-        _instrument.SetInstrumentProgram(instrument, (byte) _random.Next(0, 127), 0);
+        _instrument.SetInstrumentProgram(uid, instrument, (byte) _random.Next(0, 127), 0);
     }
 }
diff --git a/Content.Server/Zombies/ZombieSystem.Transform.cs b/Content.Server/Zombies/ZombieSystem.Transform.cs
index a906652765c..d90ceab0dca 100644
--- a/Content.Server/Zombies/ZombieSystem.Transform.cs
+++ b/Content.Server/Zombies/ZombieSystem.Transform.cs
@@ -191,7 +191,7 @@ public void ZombifyEntity(EntityUid target, MobStateComponent? mobState = null)
                 Dirty(target, pryComp);
             }
 
-            Dirty(melee);
+            Dirty(target, melee);
 
             //The zombie gets the assigned damage weaknesses and strengths
             _damageable.SetDamageModifierSetId(target, "Zombie");
diff --git a/Content.Shared/Actions/Events/FabricateActionEvent.cs b/Content.Shared/Actions/Events/FabricateActionEvent.cs
index 7483cb04d98..33ab826cc8e 100644
--- a/Content.Shared/Actions/Events/FabricateActionEvent.cs
+++ b/Content.Shared/Actions/Events/FabricateActionEvent.cs
@@ -5,5 +5,5 @@ namespace Content.Shared.Actions.Events;
 public sealed partial class FabricateActionEvent : InstantActionEvent
 {
     [DataField(required: true)]
-    public ProtoId<EntityPrototype> Fabrication;
+    public EntProtoId Fabrication;
 }
diff --git a/Content.Shared/Alert/AlertCategory.cs b/Content.Shared/Alert/AlertCategory.cs
deleted file mode 100644
index 57a3e40f70e..00000000000
--- a/Content.Shared/Alert/AlertCategory.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-namespace Content.Shared.Alert;
-
-/// <summary>
-/// Every category of alert. Corresponds to category field in alert prototypes defined in YML
-/// </summary>
-public enum AlertCategory
-{
-    Pressure,
-    Temperature,
-    Breathing,
-    Buckled,
-    Health,
-    Mood,
-    Internals,
-    Stamina,
-    Piloting,
-    Hunger,
-    Thirst,
-    Toxins,
-    Battery
-}
diff --git a/Content.Shared/Alert/AlertCategoryPrototype.cs b/Content.Shared/Alert/AlertCategoryPrototype.cs
new file mode 100644
index 00000000000..7c7d0475214
--- /dev/null
+++ b/Content.Shared/Alert/AlertCategoryPrototype.cs
@@ -0,0 +1,14 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Alert;
+
+/// <summary>
+/// This is a prototype for a category for marking alerts as mutually exclusive.
+/// </summary>
+[Prototype]
+public sealed partial class AlertCategoryPrototype : IPrototype
+{
+    /// <inheritdoc/>
+    [IdDataField]
+    public string ID { get; } = default!;
+}
diff --git a/Content.Shared/Alert/AlertKey.cs b/Content.Shared/Alert/AlertKey.cs
index c784af4cd48..c5c3a7643ec 100644
--- a/Content.Shared/Alert/AlertKey.cs
+++ b/Content.Shared/Alert/AlertKey.cs
@@ -1,5 +1,5 @@
-using Robust.Shared.Serialization;
-using Robust.Shared.Serialization.Manager;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
 
 namespace Content.Shared.Alert;
 
@@ -11,13 +11,13 @@ namespace Content.Shared.Alert;
 [Serializable, NetSerializable]
 public struct AlertKey
 {
-    public AlertType? AlertType { get; private set; } = Alert.AlertType.Error;
-    public readonly AlertCategory? AlertCategory;
+    public ProtoId<AlertPrototype>? AlertType { get; private set; } = default!;
+    public readonly ProtoId<AlertCategoryPrototype>? AlertCategory;
 
     /// NOTE: if the alert has a category you must pass the category for this to work
     /// properly as a key. I.e. if the alert has a category and you pass only the alert type, and you
     /// compare this to another AlertKey that has both the category and the same alert type, it will not consider them equal.
-    public AlertKey(AlertType? alertType, AlertCategory? alertCategory)
+    public AlertKey(ProtoId<AlertPrototype>? alertType, ProtoId<AlertCategoryPrototype>? alertCategory)
     {
         AlertCategory = alertCategory;
         AlertType = alertType;
@@ -49,7 +49,7 @@ public override int GetHashCode()
     /// <param name="category">alert category, must not be null</param>
     /// <returns>An alert key for the provided alert category. This must only be used for
     /// queries and never storage, as it is lacking an alert type.</returns>
-    public static AlertKey ForCategory(AlertCategory category)
+    public static AlertKey ForCategory(ProtoId<AlertCategoryPrototype> category)
     {
         return new(null, category);
     }
diff --git a/Content.Shared/Alert/AlertOrderPrototype.cs b/Content.Shared/Alert/AlertOrderPrototype.cs
index 8279d592b4b..af4241a27e7 100644
--- a/Content.Shared/Alert/AlertOrderPrototype.cs
+++ b/Content.Shared/Alert/AlertOrderPrototype.cs
@@ -7,7 +7,7 @@ namespace Content.Shared.Alert
     /// <summary>
     /// Defines the order of alerts so they show up in a consistent order.
     /// </summary>
-    [Prototype("alertOrder")]
+    [Prototype]
     [DataDefinition]
     public sealed partial class AlertOrderPrototype : IPrototype, IComparer<AlertPrototype>
     {
@@ -15,7 +15,7 @@ public sealed partial class AlertOrderPrototype : IPrototype, IComparer<AlertPro
         [IdDataField]
         public string ID { get; private set; } = default!;
 
-        [DataField("order")]
+        [DataField]
         private (string type, string alert)[] Order
         {
             // why would paul do this to me.
@@ -46,10 +46,10 @@ public sealed partial class AlertOrderPrototype : IPrototype, IComparer<AlertPro
                     switch (type)
                     {
                         case "alertType":
-                            _typeToIdx[Enum.Parse<AlertType>(alert)] = i++;
+                            _typeToIdx[alert] = i++;
                             break;
                         case "category":
-                            _categoryToIdx[Enum.Parse<AlertCategory>(alert)] = i++;
+                            _categoryToIdx[alert] = i++;
                             break;
                         default:
                             throw new ArgumentException();
@@ -58,17 +58,17 @@ public sealed partial class AlertOrderPrototype : IPrototype, IComparer<AlertPro
             }
         }
 
-        private readonly Dictionary<AlertType, int> _typeToIdx = new();
-        private readonly Dictionary<AlertCategory, int> _categoryToIdx = new();
+        private readonly Dictionary<ProtoId<AlertPrototype>, int> _typeToIdx = new();
+        private readonly Dictionary<ProtoId<AlertCategoryPrototype>, int> _categoryToIdx = new();
 
         private int GetOrderIndex(AlertPrototype alert)
         {
-            if (_typeToIdx.TryGetValue(alert.AlertType, out var idx))
+            if (_typeToIdx.TryGetValue(alert.ID, out var idx))
             {
                 return idx;
             }
             if (alert.Category != null &&
-                _categoryToIdx.TryGetValue((AlertCategory) alert.Category, out idx))
+                _categoryToIdx.TryGetValue(alert.Category.Value, out idx))
             {
                 return idx;
             }
@@ -78,20 +78,25 @@ private int GetOrderIndex(AlertPrototype alert)
 
         public int Compare(AlertPrototype? x, AlertPrototype? y)
         {
-            if ((x == null) && (y == null)) return 0;
-            if (x == null) return 1;
-            if (y == null) return -1;
+            if (x == null && y == null)
+                return 0;
+            if (x == null)
+                return 1;
+            if (y == null)
+                return -1;
             var idx = GetOrderIndex(x);
             var idy = GetOrderIndex(y);
             if (idx == -1 && idy == -1)
             {
                 // break ties by type value
                 // Must cast to int to avoid integer overflow when subtracting (enum's unsigned)
-                return (int)x.AlertType - (int)y.AlertType;
+                return string.Compare(x.ID, y.ID, StringComparison.InvariantCulture);
             }
 
-            if (idx == -1) return 1;
-            if (idy == -1) return -1;
+            if (idx == -1)
+                return 1;
+            if (idy == -1)
+                return -1;
             var result = idx - idy;
             // not strictly necessary (we don't care about ones that go at the same index)
             // but it makes the sort stable
@@ -99,7 +104,7 @@ public int Compare(AlertPrototype? x, AlertPrototype? y)
             {
                 // break ties by type value
                 // Must cast to int to avoid integer overflow when subtracting (enum's unsigned)
-                return (int)x.AlertType - (int)y.AlertType;
+                return string.Compare(x.ID, y.ID, StringComparison.InvariantCulture);
             }
 
             return result;
diff --git a/Content.Shared/Alert/AlertPrototype.cs b/Content.Shared/Alert/AlertPrototype.cs
index 248cc00ba40..f53da27c0de 100644
--- a/Content.Shared/Alert/AlertPrototype.cs
+++ b/Content.Shared/Alert/AlertPrototype.cs
@@ -1,120 +1,116 @@
 using Robust.Shared.Prototypes;
 using Robust.Shared.Utility;
 
-namespace Content.Shared.Alert
+namespace Content.Shared.Alert;
+
+/// <summary>
+/// An alert popup with associated icon, tooltip, and other data.
+/// </summary>
+[Prototype]
+public sealed partial class AlertPrototype : IPrototype
 {
     /// <summary>
-    /// An alert popup with associated icon, tooltip, and other data.
+    /// Type of alert, no 2 alert prototypes should have the same one.
     /// </summary>
-    [Prototype("alert")]
-    public sealed partial class AlertPrototype : IPrototype
-    {
-        [ViewVariables]
-        string IPrototype.ID => AlertType.ToString();
-
-        /// <summary>
-        /// Type of alert, no 2 alert prototypes should have the same one.
-        /// </summary>
-        [IdDataField]
-        public AlertType AlertType { get; private set; }
-
-        /// <summary>
-        /// List of icons to use for this alert. Each entry corresponds to a different severity level, starting from the
-        /// minimum and incrementing upwards. If severities are not supported, the first entry is used.
-        /// </summary>
-        [DataField("icons", required: true)]
-        public List<SpriteSpecifier> Icons = new();
-
-        /// <summary>
-        /// An entity used for displaying the <see cref="Icons"/> in the UI control.
-        /// </summary>
-        [DataField]
-        public EntProtoId AlertViewEntity = "AlertSpriteView";
-
-        /// <summary>
-        /// Name to show in tooltip window. Accepts formatting.
-        /// </summary>
-        [DataField("name")]
-        public string Name { get; private set; } = "";
-
-        /// <summary>
-        /// Description to show in tooltip window. Accepts formatting.
-        /// </summary>
-        [DataField("description")]
-        public string Description { get; private set; } = "";
-
-        /// <summary>
-        /// Category the alert belongs to. Only one alert of a given category
-        /// can be shown at a time. If one is shown while another is already being shown,
-        /// it will be replaced. This can be useful for categories of alerts which should naturally
-        /// replace each other and are mutually exclusive, for example lowpressure / highpressure,
-        /// hot / cold. If left unspecified, the alert will not replace or be replaced by any other alerts.
-        /// </summary>
-        [DataField("category")]
-        public AlertCategory? Category { get; private set; }
-
-        /// <summary>
-        /// Key which is unique w.r.t category semantics (alerts with same category have equal keys,
-        /// alerts with no category have different keys).
-        /// </summary>
-        public AlertKey AlertKey => new(AlertType, Category);
-
-        /// <summary>
-        /// -1 (no effect) unless MaxSeverity is specified. Defaults to 1. Minimum severity level supported by this state.
-        /// </summary>
-        public short MinSeverity => MaxSeverity == -1 ? (short) -1 : _minSeverity;
-
-        [DataField("minSeverity")] private short _minSeverity = 1;
-
-        /// <summary>
-        /// Maximum severity level supported by this state. -1 (default) indicates
-        /// no severity levels are supported by the state.
-        /// </summary>
-        [DataField("maxSeverity")]
-        public short MaxSeverity = -1;
-
-        /// <summary>
-        /// Indicates whether this state support severity levels
-        /// </summary>
-        public bool SupportsSeverity => MaxSeverity != -1;
-
-        /// <summary>
-        /// Defines what to do when the alert is clicked.
-        /// This will always be null on clientside.
-        /// </summary>
-        [DataField("onClick", serverOnly: true)]
-        public IAlertClick? OnClick { get; private set; }
-
-        /// <param name="severity">severity level, if supported by this alert</param>
-        /// <returns>the icon path to the texture for the provided severity level</returns>
-        public SpriteSpecifier GetIcon(short? severity = null)
-        {
-            var minIcons = SupportsSeverity
-                ? MaxSeverity - MinSeverity
-                : 1;
+    [IdDataField]
+    public string ID { get; private set; } = default!;
 
-            if (Icons.Count < minIcons)
-                throw new InvalidOperationException($"Insufficient number of icons given for alert {AlertType}");
+    /// <summary>
+    /// List of icons to use for this alert. Each entry corresponds to a different severity level, starting from the
+    /// minimum and incrementing upwards. If severities are not supported, the first entry is used.
+    /// </summary>
+    [DataField(required: true)]
+    public List<SpriteSpecifier> Icons = new();
 
-            if (!SupportsSeverity)
-                return Icons[0];
+    /// <summary>
+    /// An entity used for displaying the <see cref="Icons"/> in the UI control.
+    /// </summary>
+    [DataField]
+    public EntProtoId AlertViewEntity = "AlertSpriteView";
 
-            if (severity == null)
-            {
-                throw new ArgumentException($"No severity specified but this alert ({AlertKey}) has severity.", nameof(severity));
-            }
+    /// <summary>
+    /// Name to show in tooltip window. Accepts formatting.
+    /// </summary>
+    [DataField]
+    public string Name { get; private set; } = string.Empty;
 
-            if (severity < MinSeverity)
-            {
-                throw new ArgumentOutOfRangeException(nameof(severity), $"Severity below minimum severity in {AlertKey}.");
-            }
+    /// <summary>
+    /// Description to show in tooltip window. Accepts formatting.
+    /// </summary>
+    [DataField]
+    public string Description { get; private set; } = string.Empty;
 
-            if (severity > MaxSeverity)
-            {
-                throw new ArgumentOutOfRangeException(nameof(severity), $"Severity above maximum severity in {AlertKey}.");
-            }
+    /// <summary>
+    /// Category the alert belongs to. Only one alert of a given category
+    /// can be shown at a time. If one is shown while another is already being shown,
+    /// it will be replaced. This can be useful for categories of alerts which should naturally
+    /// replace each other and are mutually exclusive, for example lowpressure / highpressure,
+    /// hot / cold. If left unspecified, the alert will not replace or be replaced by any other alerts.
+    /// </summary>
+    [DataField]
+    public ProtoId<AlertCategoryPrototype>? Category { get; private set; }
+
+    /// <summary>
+    /// Key which is unique w.r.t category semantics (alerts with same category have equal keys,
+    /// alerts with no category have different keys).
+    /// </summary>
+    public AlertKey AlertKey => new(ID, Category);
 
-            return Icons[severity.Value - _minSeverity];
+    /// <summary>
+    /// -1 (no effect) unless MaxSeverity is specified. Defaults to 1. Minimum severity level supported by this state.
+    /// </summary>
+    public short MinSeverity => MaxSeverity == -1 ? (short) -1 : _minSeverity;
+
+    [DataField("minSeverity")] private short _minSeverity = 1;
+
+    /// <summary>
+    /// Maximum severity level supported by this state. -1 (default) indicates
+    /// no severity levels are supported by the state.
+    /// </summary>
+    [DataField]
+    public short MaxSeverity = -1;
+
+    /// <summary>
+    /// Indicates whether this state support severity levels
+    /// </summary>
+    public bool SupportsSeverity => MaxSeverity != -1;
+
+    /// <summary>
+    /// Defines what to do when the alert is clicked.
+    /// This will always be null on clientside.
+    /// </summary>
+    [DataField(serverOnly: true)]
+    public IAlertClick? OnClick { get; private set; }
+
+    /// <param name="severity">severity level, if supported by this alert</param>
+    /// <returns>the icon path to the texture for the provided severity level</returns>
+    public SpriteSpecifier GetIcon(short? severity = null)
+    {
+        var minIcons = SupportsSeverity
+            ? MaxSeverity - MinSeverity
+            : 1;
+
+        if (Icons.Count < minIcons)
+            throw new InvalidOperationException($"Insufficient number of icons given for alert {ID}");
+
+        if (!SupportsSeverity)
+            return Icons[0];
+
+        if (severity == null)
+        {
+            throw new ArgumentException($"No severity specified but this alert ({AlertKey}) has severity.", nameof(severity));
+        }
+
+        if (severity < MinSeverity)
+        {
+            throw new ArgumentOutOfRangeException(nameof(severity), $"Severity below minimum severity in {AlertKey}.");
         }
+
+        if (severity > MaxSeverity)
+        {
+            throw new ArgumentOutOfRangeException(nameof(severity), $"Severity above maximum severity in {AlertKey}.");
+        }
+
+        return Icons[severity.Value - _minSeverity];
     }
 }
diff --git a/Content.Shared/Alert/AlertState.cs b/Content.Shared/Alert/AlertState.cs
index effd9522036..d6309f6b426 100644
--- a/Content.Shared/Alert/AlertState.cs
+++ b/Content.Shared/Alert/AlertState.cs
@@ -1,3 +1,4 @@
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
 
 namespace Content.Shared.Alert;
@@ -9,5 +10,5 @@ public struct AlertState
     public (TimeSpan, TimeSpan)? Cooldown;
     public bool AutoRemove;
     public bool ShowCooldown;
-    public AlertType Type;
+    public ProtoId<AlertPrototype> Type;
 }
diff --git a/Content.Shared/Alert/AlertType.cs b/Content.Shared/Alert/AlertType.cs
deleted file mode 100644
index bd8c1dbe257..00000000000
--- a/Content.Shared/Alert/AlertType.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-namespace Content.Shared.Alert
-{
-    /// <summary>
-    /// Every kind of alert. Corresponds to alertType field in alert prototypes defined in YML
-    /// NOTE: Using byte for a compact encoding when sending this in messages, can upgrade
-    /// to ushort
-    /// </summary>
-    public enum AlertType : byte
-    {
-        Error,
-        LowOxygen,
-        LowNitrogen,
-        LowPressure,
-        HighPressure,
-        Fire,
-        Cold,
-        Hot,
-        Weightless,
-        Stun,
-        Handcuffed,
-        Ensnared,
-        Buckled,
-        HumanCrit,
-        HumanDead,
-        HumanHealth,
-        BorgBattery,
-        BorgBatteryNone,
-
-        // Mood
-        Bleeding,
-        Insane,
-        Horrible,
-        Terrible,
-        Bad,
-        Meh,
-        Neutral,
-        Good,
-        Great,
-        Exceptional,
-        Perfect,
-        MoodDead,
-        CultBuffed,
-
-        PilotingShuttle,
-        Peckish,
-        Starving,
-        Thirsty,
-        Parched,
-        Stamina,
-        Pulled,
-        Pulling,
-        Magboots,
-        Internals,
-        Toxins,
-        Muted,
-        Walking,
-        VowOfSilence,
-        VowBroken,
-        Essence,
-        Corporeal,
-        Bleed,
-        Pacified,
-        Debug1,
-        Debug2,
-        Debug3,
-        Debug4,
-        Debug5,
-        Debug6,
-        SuitPower,
-        BorgHealth,
-        BorgCrit,
-        BorgDead,
-        Offer,
-        ShadowkinPower,
-        Deflecting,
-    }
-
-}
diff --git a/Content.Shared/Alert/AlertsSystem.cs b/Content.Shared/Alert/AlertsSystem.cs
index 5b888e30c4c..83c6fcd0dd7 100644
--- a/Content.Shared/Alert/AlertsSystem.cs
+++ b/Content.Shared/Alert/AlertsSystem.cs
@@ -11,7 +11,7 @@ public abstract class AlertsSystem : EntitySystem
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
     [Dependency] private readonly IGameTiming _timing = default!;
 
-    private FrozenDictionary<AlertType, AlertPrototype> _typeToAlert = default!;
+    private FrozenDictionary<ProtoId<AlertPrototype>, AlertPrototype> _typeToAlert = default!;
 
     public IReadOnlyDictionary<AlertKey, AlertState>? GetActiveAlerts(EntityUid euid)
     {
@@ -20,23 +20,23 @@ public abstract class AlertsSystem : EntitySystem
             : null;
     }
 
-    public short GetSeverityRange(AlertType alertType)
+    public short GetSeverityRange(ProtoId<AlertPrototype> alertType)
     {
         var minSeverity = _typeToAlert[alertType].MinSeverity;
         return (short)MathF.Max(minSeverity,_typeToAlert[alertType].MaxSeverity - minSeverity);
     }
 
-    public short GetMaxSeverity(AlertType alertType)
+    public short GetMaxSeverity(ProtoId<AlertPrototype> alertType)
     {
         return _typeToAlert[alertType].MaxSeverity;
     }
 
-    public short GetMinSeverity(AlertType alertType)
+    public short GetMinSeverity(ProtoId<AlertPrototype> alertType)
     {
         return _typeToAlert[alertType].MinSeverity;
     }
 
-    public bool IsShowingAlert(EntityUid euid, AlertType alertType)
+    public bool IsShowingAlert(EntityUid euid, ProtoId<AlertPrototype> alertType)
     {
         if (!EntityManager.TryGetComponent(euid, out AlertsComponent? alertsComponent))
             return false;
@@ -51,7 +51,7 @@ public bool IsShowingAlert(EntityUid euid, AlertType alertType)
     }
 
     /// <returns>true iff an alert of the indicated alert category is currently showing</returns>
-    public bool IsShowingAlertCategory(EntityUid euid, AlertCategory alertCategory)
+    public bool IsShowingAlertCategory(EntityUid euid, ProtoId<AlertCategoryPrototype> alertCategory)
     {
         return EntityManager.TryGetComponent(euid, out AlertsComponent? alertsComponent)
                && alertsComponent.Alerts.ContainsKey(AlertKey.ForCategory(alertCategory));
@@ -78,7 +78,7 @@ public bool TryGetAlertState(EntityUid euid, AlertKey key, out AlertState alertS
     ///     be erased if there is currently a cooldown for the alert)</param>
     /// <param name="autoRemove">if true, the alert will be removed at the end of the cooldown</param>
     /// <param name="showCooldown">if true, the cooldown will be visibly shown over the alert icon</param>
-    public void ShowAlert(EntityUid euid, AlertType alertType, short? severity = null, (TimeSpan, TimeSpan)? cooldown = null, bool autoRemove = false, bool showCooldown = true )
+    public void ShowAlert(EntityUid euid, ProtoId<AlertPrototype> alertType, short? severity = null, (TimeSpan, TimeSpan)? cooldown = null, bool autoRemove = false, bool showCooldown = true )
     {
         // This should be handled as part of networking.
         if (_timing.ApplyingState)
@@ -131,7 +131,7 @@ public void ShowAlert(EntityUid euid, AlertType alertType, short? severity = nul
     /// <summary>
     /// Clear the alert with the given category, if one is currently showing.
     /// </summary>
-    public void ClearAlertCategory(EntityUid euid, AlertCategory category)
+    public void ClearAlertCategory(EntityUid euid, ProtoId<AlertCategoryPrototype> category)
     {
         if(!TryComp(euid, out AlertsComponent? alertsComponent))
             return;
@@ -150,7 +150,7 @@ public void ClearAlertCategory(EntityUid euid, AlertCategory category)
     /// <summary>
     /// Clear the alert of the given type if it is currently showing.
     /// </summary>
-    public void ClearAlert(EntityUid euid, AlertType alertType)
+    public void ClearAlert(EntityUid euid, ProtoId<AlertPrototype> alertType)
     {
         if (_timing.ApplyingState)
             return;
@@ -286,13 +286,13 @@ private void HandlePrototypesReloaded(PrototypesReloadedEventArgs obj)
 
     protected virtual void LoadPrototypes()
     {
-        var dict = new Dictionary<AlertType, AlertPrototype>();
+        var dict = new Dictionary<ProtoId<AlertPrototype>, AlertPrototype>();
         foreach (var alert in _prototypeManager.EnumeratePrototypes<AlertPrototype>())
         {
-            if (!dict.TryAdd(alert.AlertType, alert))
+            if (!dict.TryAdd(alert.ID, alert))
             {
                 Log.Error("Found alert with duplicate alertType {0} - all alerts must have" +
-                          " a unique alertType, this one will be skipped", alert.AlertType);
+                          " a unique alertType, this one will be skipped", alert.ID);
             }
         }
 
@@ -303,7 +303,7 @@ protected virtual void LoadPrototypes()
     /// Tries to get the alert of the indicated type
     /// </summary>
     /// <returns>true if found</returns>
-    public bool TryGet(AlertType alertType, [NotNullWhen(true)] out AlertPrototype? alert)
+    public bool TryGet(ProtoId<AlertPrototype> alertType, [NotNullWhen(true)] out AlertPrototype? alert)
     {
         return _typeToAlert.TryGetValue(alertType, out alert);
     }
diff --git a/Content.Shared/Alert/ClickAlertEvent.cs b/Content.Shared/Alert/ClickAlertEvent.cs
index fe7ca97e4c2..43dd086b562 100644
--- a/Content.Shared/Alert/ClickAlertEvent.cs
+++ b/Content.Shared/Alert/ClickAlertEvent.cs
@@ -1,4 +1,5 @@
-using Robust.Shared.Serialization;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
 
 namespace Content.Shared.Alert;
 
@@ -8,9 +9,9 @@ namespace Content.Shared.Alert;
 [Serializable, NetSerializable]
 public sealed class ClickAlertEvent : EntityEventArgs
 {
-    public readonly AlertType Type;
+    public readonly ProtoId<AlertPrototype> Type;
 
-    public ClickAlertEvent(AlertType alertType)
+    public ClickAlertEvent(ProtoId<AlertPrototype> alertType)
     {
         Type = alertType;
     }
diff --git a/Content.Shared/Buckle/Components/BuckleComponent.cs b/Content.Shared/Buckle/Components/BuckleComponent.cs
index cf28b56d51f..ee86e6d4de0 100644
--- a/Content.Shared/Buckle/Components/BuckleComponent.cs
+++ b/Content.Shared/Buckle/Components/BuckleComponent.cs
@@ -1,10 +1,15 @@
+using System.Diagnostics.CodeAnalysis;
 using Content.Shared.Interaction;
 using Robust.Shared.GameStates;
 using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
 namespace Content.Shared.Buckle.Components;
 
-[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
+/// <summary>
+/// This component allows an entity to be buckled to an entity with a <see cref="StrapComponent"/>.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
 [Access(typeof(SharedBuckleSystem))]
 public sealed partial class BuckleComponent : Component
 {
@@ -14,31 +19,23 @@ public sealed partial class BuckleComponent : Component
     /// across a table two tiles away" problem.
     /// </summary>
     [DataField]
-    [ViewVariables(VVAccess.ReadWrite)]
     public float Range = SharedInteractionSystem.InteractionRange / 1.4f;
 
     /// <summary>
     /// True if the entity is buckled, false otherwise.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    [AutoNetworkedField]
-    public bool Buckled;
-
-    [ViewVariables]
-    [AutoNetworkedField]
-    public EntityUid? LastEntityBuckledTo;
+    [MemberNotNullWhen(true, nameof(BuckledTo))]
+    public bool Buckled => BuckledTo != null;
 
     /// <summary>
     /// Whether or not collisions should be possible with the entity we are strapped to
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
-    [DataField, AutoNetworkedField]
+    [DataField]
     public bool DontCollide;
 
     /// <summary>
     /// Whether or not we should be allowed to pull the entity we are strapped to
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite)]
     [DataField]
     public bool PullStrap;
 
@@ -47,20 +44,18 @@ public sealed partial class BuckleComponent : Component
     /// be able to unbuckle after recently buckling.
     /// </summary>
     [DataField]
-    [ViewVariables(VVAccess.ReadWrite)]
     public TimeSpan Delay = TimeSpan.FromSeconds(0.25f);
 
     /// <summary>
     /// The time that this entity buckled at.
     /// </summary>
-    [ViewVariables]
-    public TimeSpan BuckleTime;
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
+    public TimeSpan? BuckleTime;
 
     /// <summary>
     /// The strap that this component is buckled to.
     /// </summary>
-    [ViewVariables]
-    [AutoNetworkedField]
+    [DataField]
     public EntityUid? BuckledTo;
 
     /// <summary>
@@ -68,7 +63,6 @@ public sealed partial class BuckleComponent : Component
     /// <see cref="StrapComponent"/>.
     /// </summary>
     [DataField]
-    [ViewVariables(VVAccess.ReadWrite)]
     public int Size = 100;
 
     /// <summary>
@@ -77,11 +71,90 @@ public sealed partial class BuckleComponent : Component
     [ViewVariables] public int? OriginalDrawDepth;
 }
 
+[Serializable, NetSerializable]
+public sealed class BuckleState(NetEntity? buckledTo, bool dontCollide, TimeSpan? buckleTime) : ComponentState
+{
+    public readonly NetEntity? BuckledTo = buckledTo;
+    public readonly bool DontCollide = dontCollide;
+    public readonly TimeSpan? BuckleTime = buckleTime;
+}
+
+
+/// <summary>
+/// Event raised directed at a strap entity before some entity gets buckled to it.
+/// </summary>
+[ByRefEvent]
+public record struct StrapAttemptEvent(
+    Entity<StrapComponent> Strap,
+    Entity<BuckleComponent> Buckle,
+    EntityUid? User,
+    bool Popup)
+{
+    public bool Cancelled;
+}
+
+/// <summary>
+/// Event raised directed at a buckle entity before it gets buckled to some strap entity.
+/// </summary>
+[ByRefEvent]
+public record struct BuckleAttemptEvent(
+    Entity<StrapComponent> Strap,
+    Entity<BuckleComponent> Buckle,
+    EntityUid? User,
+    bool Popup)
+{
+    public bool Cancelled;
+}
+
+/// <summary>
+/// Event raised directed at a strap entity before some entity gets unbuckled from it.
+/// </summary>
+[ByRefEvent]
+public record struct UnstrapAttemptEvent(
+    Entity<StrapComponent> Strap,
+    Entity<BuckleComponent> Buckle,
+    EntityUid? User,
+    bool Popup)
+{
+    public bool Cancelled;
+}
+
+/// <summary>
+/// Event raised directed at a buckle entity before it gets unbuckled.
+/// </summary>
+[ByRefEvent]
+public record struct UnbuckleAttemptEvent(
+    Entity<StrapComponent> Strap,
+    Entity<BuckleComponent> Buckle,
+    EntityUid? User,
+    bool Popup)
+{
+    public bool Cancelled;
+}
+
+/// <summary>
+/// Event raised directed at a strap entity after something has been buckled to it.
+/// </summary>
+[ByRefEvent]
+public readonly record struct StrappedEvent(Entity<StrapComponent> Strap, Entity<BuckleComponent> Buckle);
+
+/// <summary>
+/// Event raised directed at a buckle entity after it has been buckled.
+/// </summary>
+[ByRefEvent]
+public readonly record struct BuckledEvent(Entity<StrapComponent> Strap, Entity<BuckleComponent> Buckle);
+
+/// <summary>
+/// Event raised directed at a strap entity after something has been unbuckled from it.
+/// </summary>
 [ByRefEvent]
-public record struct BuckleAttemptEvent(EntityUid StrapEntity, EntityUid BuckledEntity, EntityUid UserEntity, bool Buckling, bool Cancelled = false);
+public readonly record struct UnstrappedEvent(Entity<StrapComponent> Strap, Entity<BuckleComponent> Buckle);
 
+/// <summary>
+/// Event raised directed at a buckle entity after it has been unbuckled from some strap entity.
+/// </summary>
 [ByRefEvent]
-public readonly record struct BuckleChangeEvent(EntityUid StrapEntity, EntityUid BuckledEntity, bool Buckling);
+public readonly record struct UnbuckledEvent(Entity<StrapComponent> Strap, Entity<BuckleComponent> Buckle);
 
 [Serializable, NetSerializable]
 public enum BuckleVisuals
diff --git a/Content.Shared/Buckle/Components/StrapComponent.cs b/Content.Shared/Buckle/Components/StrapComponent.cs
index 72c92ebf84b..0fbdae693de 100644
--- a/Content.Shared/Buckle/Components/StrapComponent.cs
+++ b/Content.Shared/Buckle/Components/StrapComponent.cs
@@ -3,6 +3,7 @@
 using Content.Shared.Whitelist;
 using Robust.Shared.Audio;
 using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
 
 namespace Content.Shared.Buckle.Components;
@@ -12,117 +13,83 @@ namespace Content.Shared.Buckle.Components;
 public sealed partial class StrapComponent : Component
 {
     /// <summary>
-    /// The entities that are currently buckled
+    /// The entities that are currently buckled to this strap.
     /// </summary>
-    [AutoNetworkedField]
-    [ViewVariables] // TODO serialization
+    [ViewVariables]
     public HashSet<EntityUid> BuckledEntities = new();
 
     /// <summary>
     /// Entities that this strap accepts and can buckle
     /// If null it accepts any entity
     /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public EntityWhitelist? Whitelist;
 
     /// <summary>
     /// Entities that this strap does not accept and cannot buckle.
     /// </summary>
-    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    [DataField]
     public EntityWhitelist? Blacklist;
 
     /// <summary>
     /// The change in position to the strapped mob
     /// </summary>
     [DataField, AutoNetworkedField]
-    [ViewVariables(VVAccess.ReadWrite)]
     public StrapPosition Position = StrapPosition.None;
 
-    /// <summary>
-    /// The distance above which a buckled entity will be automatically unbuckled.
-    /// Don't change it unless you really have to
-    /// </summary>
-    /// <remarks>
-    /// Dont set this below 0.2 because that causes audio issues with <see cref="SharedBuckleSystem.OnBuckleMove"/>
-    /// My guess after testing is that the client sets BuckledTo to the strap in *some* ticks for some reason
-    /// whereas the server doesnt, thus the client tries to unbuckle like 15 times because it passes the strap null check
-    /// This is why this needs to be above 0.1 to make the InRange check fail in both client and server.
-    /// </remarks>
-    [DataField, AutoNetworkedField]
-    [ViewVariables(VVAccess.ReadWrite)]
-    public float MaxBuckleDistance = 0.2f;
-
-    /// <summary>
-    /// Gets and clamps the buckle offset to MaxBuckleDistance
-    /// </summary>
-    [ViewVariables]
-    public Vector2 BuckleOffsetClamped => Vector2.Clamp(
-        BuckleOffset,
-        Vector2.One * -MaxBuckleDistance,
-        Vector2.One * MaxBuckleDistance);
-
     /// <summary>
     /// The buckled entity will be offset by this amount from the center of the strap object.
-    /// If this offset it too big, it will be clamped to <see cref="MaxBuckleDistance"/>
     /// </summary>
     [DataField, AutoNetworkedField]
-    [ViewVariables(VVAccess.ReadWrite)]
     public Vector2 BuckleOffset = Vector2.Zero;
 
     /// <summary>
     /// The angle to rotate the player by when they get strapped
     /// </summary>
     [DataField]
-    [ViewVariables(VVAccess.ReadWrite)]
     public Angle Rotation;
 
     /// <summary>
     /// The size of the strap which is compared against when buckling entities
     /// </summary>
     [DataField]
-    [ViewVariables(VVAccess.ReadWrite)]
     public int Size = 100;
 
     /// <summary>
     /// If disabled, nothing can be buckled on this object, and it will unbuckle anything that's already buckled
     /// </summary>
-    [ViewVariables]
+    [DataField, AutoNetworkedField]
     public bool Enabled = true;
 
     /// <summary>
     /// You can specify the offset the entity will have after unbuckling.
     /// </summary>
     [DataField]
-    [ViewVariables(VVAccess.ReadWrite)]
     public Vector2 UnbuckleOffset = Vector2.Zero;
 
     /// <summary>
     /// The sound to be played when a mob is buckled
     /// </summary>
     [DataField]
-    [ViewVariables(VVAccess.ReadWrite)]
     public SoundSpecifier BuckleSound  = new SoundPathSpecifier("/Audio/Effects/buckle.ogg");
 
     /// <summary>
     /// The sound to be played when a mob is unbuckled
     /// </summary>
     [DataField]
-    [ViewVariables(VVAccess.ReadWrite)]
     public SoundSpecifier UnbuckleSound = new SoundPathSpecifier("/Audio/Effects/unbuckle.ogg");
 
     /// <summary>
     /// ID of the alert to show when buckled
     /// </summary>
     [DataField]
-    [ViewVariables(VVAccess.ReadWrite)]
-    public AlertType BuckledAlertType = AlertType.Buckled;
+    public ProtoId<AlertPrototype> BuckledAlertType = "Buckled";
 
     /// <summary>
-    /// The sum of the sizes of all the buckled entities in this strap
+    /// Whether InteractHand will buckle the user to the strap.
     /// </summary>
-    [AutoNetworkedField]
-    [ViewVariables]
-    public int OccupiedSize;
+    [DataField]
+    public bool BuckleOnInteractHand = true;
 }
 
 public enum StrapPosition
diff --git a/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs b/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs
index c07d90b3a2b..ed1b3c19906 100644
--- a/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs
+++ b/Content.Shared/Buckle/SharedBuckleSystem.Buckle.cs
@@ -1,36 +1,49 @@
 using System.Diagnostics.CodeAnalysis;
 using System.Numerics;
 using Content.Shared.Alert;
-using Content.Shared.Bed.Sleep;
 using Content.Shared.Buckle.Components;
+using Content.Shared.Cuffs.Components;
 using Content.Shared.Database;
+using Content.Shared.DoAfter;
 using Content.Shared.Hands.Components;
 using Content.Shared.IdentityManagement;
-using Content.Shared.Interaction;
-using Content.Shared.Mobs.Components;
 using Content.Shared.Movement.Events;
+using Content.Shared.Movement.Pulling.Events;
 using Content.Shared.Popups;
+using Content.Shared.Pulling.Events;
+using Content.Shared.Rotation;
 using Content.Shared.Standing;
 using Content.Shared.Storage.Components;
 using Content.Shared.Stunnable;
 using Content.Shared.Throwing;
-using Content.Shared.Verbs;
+using Content.Shared.Whitelist;
+using Robust.Shared.Containers;
+using Robust.Shared.GameStates;
+using Robust.Shared.Map;
 using Robust.Shared.Physics.Components;
 using Robust.Shared.Physics.Events;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Utility;
-using PullableComponent = Content.Shared.Movement.Pulling.Components.PullableComponent;
 
 namespace Content.Shared.Buckle;
 
 public abstract partial class SharedBuckleSystem
 {
+    public static ProtoId<AlertCategoryPrototype> BuckledAlertCategory = "Buckled";
+
+    [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
+
     private void InitializeBuckle()
     {
-        SubscribeLocalEvent<BuckleComponent, ComponentStartup>(OnBuckleComponentStartup);
         SubscribeLocalEvent<BuckleComponent, ComponentShutdown>(OnBuckleComponentShutdown);
         SubscribeLocalEvent<BuckleComponent, MoveEvent>(OnBuckleMove);
-        SubscribeLocalEvent<BuckleComponent, InteractHandEvent>(OnBuckleInteractHand);
-        SubscribeLocalEvent<BuckleComponent, GetVerbsEvent<InteractionVerb>>(AddUnbuckleVerb);
+        SubscribeLocalEvent<BuckleComponent, EntParentChangedMessage>(OnParentChanged);
+        SubscribeLocalEvent<BuckleComponent, EntGotInsertedIntoContainerMessage>(OnInserted);
+
+        SubscribeLocalEvent<BuckleComponent, StartPullAttemptEvent>(OnPullAttempt);
+        SubscribeLocalEvent<BuckleComponent, BeingPulledAttemptEvent>(OnBeingPulledAttempt);
+        SubscribeLocalEvent<BuckleComponent, PullStartedMessage>(OnPullStarted);
+
         SubscribeLocalEvent<BuckleComponent, InsertIntoEntityStorageAttemptEvent>(OnBuckleInsertIntoEntityStorageAttempt);
 
         SubscribeLocalEvent<BuckleComponent, PreventCollideEvent>(OnBucklePreventCollide);
@@ -40,64 +53,84 @@ private void InitializeBuckle()
         SubscribeLocalEvent<BuckleComponent, UpdateCanMoveEvent>(OnBuckleUpdateCanMove);
     }
 
-    private void OnBuckleComponentStartup(EntityUid uid, BuckleComponent component, ComponentStartup args)
+    private void OnBuckleComponentShutdown(Entity<BuckleComponent> ent, ref ComponentShutdown args)
     {
-        UpdateBuckleStatus(uid, component);
+        Unbuckle(ent!, null);
     }
 
-    private void OnBuckleComponentShutdown(EntityUid uid, BuckleComponent component, ComponentShutdown args)
-    {
-        TryUnbuckle(uid, uid, true, component);
+    #region Pulling
 
-        component.BuckleTime = default;
+    private void OnPullAttempt(Entity<BuckleComponent> ent, ref StartPullAttemptEvent args)
+    {
+        // Prevent people pulling the chair they're on, etc.
+        if (ent.Comp.BuckledTo == args.Pulled && !ent.Comp.PullStrap)
+            args.Cancel();
     }
 
-    private void OnBuckleMove(EntityUid uid, BuckleComponent component, ref MoveEvent ev)
+    private void OnBeingPulledAttempt(Entity<BuckleComponent> ent, ref BeingPulledAttemptEvent args)
     {
-        if (component.BuckledTo is not {} strapUid)
+        if (args.Cancelled || !ent.Comp.Buckled)
             return;
 
-        if (!TryComp<StrapComponent>(strapUid, out var strapComp))
-            return;
+        if (!CanUnbuckle(ent!, args.Puller, false))
+            args.Cancel();
+    }
 
-        var strapPosition = Transform(strapUid).Coordinates;
-        if (ev.NewPosition.EntityId.IsValid() && ev.NewPosition.InRange(EntityManager, _transform, strapPosition, strapComp.MaxBuckleDistance))
-            return;
+    private void OnPullStarted(Entity<BuckleComponent> ent, ref PullStartedMessage args)
+    {
+        Unbuckle(ent!, args.PullerUid);
+    }
+
+    #endregion
 
-        TryUnbuckle(uid, uid, true, component);
+    #region Transform
+
+    private void OnParentChanged(Entity<BuckleComponent> ent, ref EntParentChangedMessage args)
+    {
+        BuckleTransformCheck(ent, args.Transform);
     }
 
-    private void OnBuckleInteractHand(EntityUid uid, BuckleComponent component, InteractHandEvent args)
+    private void OnInserted(Entity<BuckleComponent> ent, ref EntGotInsertedIntoContainerMessage args)
     {
-        if (!component.Buckled)
-            return;
+        BuckleTransformCheck(ent, Transform(ent));
+    }
 
-        if (TryUnbuckle(uid, args.User, buckleComp: component))
-            args.Handled = true;
+    private void OnBuckleMove(Entity<BuckleComponent> ent, ref MoveEvent ev)
+    {
+        BuckleTransformCheck(ent, ev.Component);
     }
 
-    private void AddUnbuckleVerb(EntityUid uid, BuckleComponent component, GetVerbsEvent<InteractionVerb> args)
+    /// <summary>
+    /// Check if the entity should get unbuckled as a result of transform or container changes.
+    /// </summary>
+    private void BuckleTransformCheck(Entity<BuckleComponent> buckle, TransformComponent xform)
     {
-        if (!args.CanAccess || !args.CanInteract || !component.Buckled)
+        if (_gameTiming.ApplyingState)
+            return;
+
+        if (buckle.Comp.BuckledTo is not { } strapUid)
             return;
 
-        InteractionVerb verb = new()
+        if (!TryComp<StrapComponent>(strapUid, out var strapComp))
         {
-            Act = () => TryUnbuckle(uid, args.User, buckleComp: component),
-            Text = Loc.GetString("verb-categories-unbuckle"),
-            Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/unbuckle.svg.192dpi.png"))
-        };
+            Log.Error($"Encountered buckle entity {ToPrettyString(buckle)} without a valid strap entity {ToPrettyString(strapUid)}");
+            SetBuckledTo(buckle, null);
+            return;
+        }
 
-        if (args.Target == args.User && args.Using == null)
+        if (xform.ParentUid != strapUid || _container.IsEntityInContainer(buckle))
         {
-            // A user is left clicking themselves with an empty hand, while buckled.
-            // It is very likely they are trying to unbuckle themselves.
-            verb.Priority = 1;
+            Unbuckle(buckle, (strapUid, strapComp), null);
+            return;
         }
 
-        args.Verbs.Add(verb);
+        var delta = (xform.LocalPosition - strapComp.BuckleOffset).LengthSquared();
+        if (delta > 1e-5)
+            Unbuckle(buckle, (strapUid, strapComp), null);
     }
 
+    #endregion
+
     private void OnBuckleInsertIntoEntityStorageAttempt(EntityUid uid, BuckleComponent component, ref InsertIntoEntityStorageAttemptEvent args)
     {
         if (component.Buckled)
@@ -106,10 +139,7 @@ private void OnBuckleInsertIntoEntityStorageAttempt(EntityUid uid, BuckleCompone
 
     private void OnBucklePreventCollide(EntityUid uid, BuckleComponent component, ref PreventCollideEvent args)
     {
-        if (args.OtherEntity != component.BuckledTo)
-            return;
-
-        if (component.Buckled || component.DontCollide)
+        if (args.OtherEntity == component.BuckledTo && component.DontCollide)
             args.Cancelled = true;
     }
 
@@ -133,10 +163,7 @@ private void OnBuckleThrowPushbackAttempt(EntityUid uid, BuckleComponent compone
 
     private void OnBuckleUpdateCanMove(EntityUid uid, BuckleComponent component, UpdateCanMoveEvent args)
     {
-        if (component.LifeStage > ComponentLifeStage.Running)
-            return;
-
-        if (component.Buckled) // buckle shitcode
+        if (component.Buckled)
             args.Cancel();
     }
 
@@ -145,162 +172,146 @@ public bool IsBuckled(EntityUid uid, BuckleComponent? component = null)
         return Resolve(uid, ref component, false) && component.Buckled;
     }
 
-    /// <summary>
-    /// Shows or hides the buckled status effect depending on if the
-    /// entity is buckled or not.
-    /// </summary>
-    /// <param name="uid"> Entity that we want to show the alert </param>
-    /// <param name="buckleComp"> buckle component of the entity </param>
-    /// <param name="strapComp"> strap component of the thing we are strapping to </param>
-    private void UpdateBuckleStatus(EntityUid uid, BuckleComponent buckleComp, StrapComponent? strapComp = null)
+    protected void SetBuckledTo(Entity<BuckleComponent> buckle, Entity<StrapComponent?>? strap)
     {
-        Appearance.SetData(uid, StrapVisuals.State, buckleComp.Buckled);
-        if (buckleComp.BuckledTo != null)
+        if (TryComp(buckle.Comp.BuckledTo, out StrapComponent? old))
         {
-            if (!Resolve(buckleComp.BuckledTo.Value, ref strapComp))
-                return;
-
-            var alertType = strapComp.BuckledAlertType;
-            _alerts.ShowAlert(uid, alertType);
+            old.BuckledEntities.Remove(buckle);
+            Dirty(buckle.Comp.BuckledTo.Value, old);
         }
-        else
-        {
-            _alerts.ClearAlertCategory(uid, AlertCategory.Buckled);
-        }
-    }
 
-    /// <summary>
-    /// Sets the <see cref="BuckleComponent.BuckledTo"/> field in the component to a value
-    /// </summary>
-    /// <param name="strapUid"> Value tat with be assigned to the field </param>
-    private void SetBuckledTo(EntityUid buckleUid, EntityUid? strapUid, StrapComponent? strapComp, BuckleComponent buckleComp)
-    {
-        buckleComp.BuckledTo = strapUid;
-
-        if (strapUid == null)
+        if (strap is {} strapEnt && Resolve(strapEnt.Owner, ref strapEnt.Comp))
         {
-            buckleComp.Buckled = false;
+            strapEnt.Comp.BuckledEntities.Add(buckle);
+            Dirty(strapEnt);
+            _alerts.ShowAlert(buckle, strapEnt.Comp.BuckledAlertType);
         }
         else
         {
-            buckleComp.LastEntityBuckledTo = strapUid;
-            buckleComp.DontCollide = true;
-            buckleComp.Buckled = true;
-            buckleComp.BuckleTime = _gameTiming.CurTime;
+            _alerts.ClearAlertCategory(buckle, BuckledAlertCategory);
         }
 
-        ActionBlocker.UpdateCanMove(buckleUid);
-        UpdateBuckleStatus(buckleUid, buckleComp, strapComp);
-        Dirty(buckleComp);
+        buckle.Comp.BuckledTo = strap;
+        buckle.Comp.BuckleTime = _gameTiming.CurTime;
+        ActionBlocker.UpdateCanMove(buckle);
+        Appearance.SetData(buckle, StrapVisuals.State, buckle.Comp.Buckled);
+        Dirty(buckle);
     }
 
     /// <summary>
     /// Checks whether or not buckling is possible
     /// </summary>
     /// <param name="buckleUid"> Uid of the owner of BuckleComponent </param>
-    /// <param name="userUid">
-    /// Uid of a third party entity,
-    /// i.e, the uid of someone else you are dragging to a chair.
-    /// Can equal buckleUid sometimes
+    /// <param name="user">
+    ///     Uid of a third party entity,
+    ///     i.e, the uid of someone else you are dragging to a chair.
+    ///     Can equal buckleUid sometimes
     /// </param>
     /// <param name="strapUid"> Uid of the owner of strap component </param>
-    private bool CanBuckle(
-        EntityUid buckleUid,
-        EntityUid userUid,
+    /// <param name="strapComp"></param>
+    /// <param name="buckleComp"></param>
+    private bool CanBuckle(EntityUid buckleUid,
+        EntityUid? user,
         EntityUid strapUid,
+        bool popup,
         [NotNullWhen(true)] out StrapComponent? strapComp,
-        BuckleComponent? buckleComp = null)
+        BuckleComponent buckleComp)
     {
         strapComp = null;
-
-        if (userUid == strapUid ||
-            !Resolve(buckleUid, ref buckleComp, false) ||
-            !Resolve(strapUid, ref strapComp, false))
-        {
+        if (!Resolve(strapUid, ref strapComp, false))
             return false;
-        }
 
         // Does it pass the Whitelist
         if (strapComp.Whitelist != null &&
-            !strapComp.Whitelist.IsValid(buckleUid, EntityManager) || strapComp.Blacklist?.IsValid(buckleUid, EntityManager) == true)
+            !_whitelistSystem.IsValid(strapComp.Whitelist, buckleUid) || strapComp.Blacklist != null && _whitelistSystem.IsValid(strapComp.Blacklist, buckleUid))
         {
-            if (_netManager.IsServer)
-                _popup.PopupEntity(Loc.GetString("buckle-component-cannot-fit-message"), userUid, buckleUid, PopupType.Medium);
+            if (popup)
+                _popup.PopupClient(Loc.GetString("buckle-component-cannot-fit-message"), user, PopupType.Medium);
+
             return false;
         }
 
-        // Is it within range
-        bool Ignored(EntityUid entity) => entity == buckleUid || entity == userUid || entity == strapUid;
-
-        if (!_interaction.InRangeUnobstructed(buckleUid, strapUid, buckleComp.Range, predicate: Ignored,
+        if (!_interaction.InRangeUnobstructed(buckleUid,
+                strapUid,
+                buckleComp.Range,
+                predicate: entity => entity == buckleUid || entity == user || entity == strapUid,
                 popup: true))
         {
             return false;
         }
 
-        // If in a container
-        if (_container.TryGetContainingContainer(buckleUid, out var ownerContainer))
-        {
-            // And not in the same container as the strap
-            if (!_container.TryGetContainingContainer(strapUid, out var strapContainer) ||
-                ownerContainer != strapContainer)
-            {
-                return false;
-            }
-        }
+        if (!_container.IsInSameOrNoContainer((buckleUid, null, null), (strapUid, null, null)))
+            return false;
 
-        if (!HasComp<HandsComponent>(userUid))
+        if (user != null && !HasComp<HandsComponent>(user))
         {
-            // PopupPredicted when
-            if (_netManager.IsServer)
-                _popup.PopupEntity(Loc.GetString("buckle-component-no-hands-message"), userUid, userUid);
+            if (popup)
+                _popup.PopupClient(Loc.GetString("buckle-component-no-hands-message"), user);
+
             return false;
         }
 
-        if (buckleComp.Buckled)
+        if (buckleComp.Buckled && !TryUnbuckle(buckleUid, user, buckleComp))
         {
-            var message = Loc.GetString(buckleUid == userUid
+            if (popup)
+            {
+                var message = Loc.GetString(buckleUid == user
                     ? "buckle-component-already-buckled-message"
                     : "buckle-component-other-already-buckled-message",
                 ("owner", Identity.Entity(buckleUid, EntityManager)));
-            if (_netManager.IsServer)
-                _popup.PopupEntity(message, userUid, userUid);
+
+                _popup.PopupClient(message, user);
+            }
 
             return false;
         }
 
+        // Check whether someone is attempting to buckle something to their own child
         var parent = Transform(strapUid).ParentUid;
         while (parent.IsValid())
         {
-            if (parent == userUid)
+            if (parent != buckleUid)
+            {
+                parent = Transform(parent).ParentUid;
+                continue;
+            }
+
+            if (popup)
             {
-                var message = Loc.GetString(buckleUid == userUid
+                var message = Loc.GetString(buckleUid == user
                     ? "buckle-component-cannot-buckle-message"
-                    : "buckle-component-other-cannot-buckle-message", ("owner", Identity.Entity(buckleUid, EntityManager)));
-                if (_netManager.IsServer)
-                    _popup.PopupEntity(message, userUid, userUid);
+                    : "buckle-component-other-cannot-buckle-message",
+                ("owner", Identity.Entity(buckleUid, EntityManager)));
 
-                return false;
+                _popup.PopupClient(message, user);
             }
 
-            parent = Transform(parent).ParentUid;
+            return false;
         }
 
         if (!StrapHasSpace(strapUid, buckleComp, strapComp))
         {
-            var message = Loc.GetString(buckleUid == userUid
-                ? "buckle-component-cannot-fit-message"
-                : "buckle-component-other-cannot-fit-message", ("owner", Identity.Entity(buckleUid, EntityManager)));
-            if (_netManager.IsServer)
-                _popup.PopupEntity(message, userUid, userUid);
+            if (popup)
+            {
+                var message = Loc.GetString(buckleUid == user
+                    ? "buckle-component-cannot-buckle-message"
+                    : "buckle-component-other-cannot-buckle-message",
+                ("owner", Identity.Entity(buckleUid, EntityManager)));
+
+                _popup.PopupClient(message, user);
+            }
 
             return false;
         }
 
-        var attemptEvent = new BuckleAttemptEvent(strapUid, buckleUid, userUid, true);
-        RaiseLocalEvent(attemptEvent.BuckledEntity, ref attemptEvent);
-        RaiseLocalEvent(attemptEvent.StrapEntity, ref attemptEvent);
-        if (attemptEvent.Cancelled)
+        var buckleAttempt = new BuckleAttemptEvent((strapUid, strapComp), (buckleUid, buckleComp), user, popup);
+        RaiseLocalEvent(buckleUid, ref buckleAttempt);
+        if (buckleAttempt.Cancelled)
+            return false;
+
+        var strapAttempt = new StrapAttemptEvent((strapUid, strapComp), (buckleUid, buckleComp), user, popup);
+        RaiseLocalEvent(strapUid, ref strapAttempt);
+        if (strapAttempt.Cancelled)
             return false;
 
         return true;
@@ -309,217 +320,199 @@ private bool CanBuckle(
     /// <summary>
     /// Attempts to buckle an entity to a strap
     /// </summary>
-    /// <param name="buckleUid"> Uid of the owner of BuckleComponent </param>
-    /// <param name="userUid">
+    /// <param name="buckle"> Uid of the owner of BuckleComponent </param>
+    /// <param name="user">
     /// Uid of a third party entity,
     /// i.e, the uid of someone else you are dragging to a chair.
     /// Can equal buckleUid sometimes
     /// </param>
-    /// <param name="strapUid"> Uid of the owner of strap component </param>
-    public bool TryBuckle(EntityUid buckleUid, EntityUid userUid, EntityUid strapUid, BuckleComponent? buckleComp = null)
+    /// <param name="strap"> Uid of the owner of strap component </param>
+    public bool TryBuckle(EntityUid buckle, EntityUid? user, EntityUid strap, BuckleComponent? buckleComp = null, bool popup = true)
     {
-        if (!Resolve(buckleUid, ref buckleComp, false))
+        if (!Resolve(buckle, ref buckleComp, false))
             return false;
 
-        if (!CanBuckle(buckleUid, userUid, strapUid, out var strapComp, buckleComp))
+        if (!CanBuckle(buckle, user, strap, popup, out var strapComp, buckleComp))
             return false;
 
-        if (!StrapTryAdd(strapUid, buckleUid, buckleComp, false, strapComp))
-        {
-            var message = Loc.GetString(buckleUid == userUid
-                ? "buckle-component-cannot-buckle-message"
-                : "buckle-component-other-cannot-buckle-message", ("owner", Identity.Entity(buckleUid, EntityManager)));
-            if (_netManager.IsServer)
-                _popup.PopupEntity(message, userUid, userUid);
-            return false;
-        }
+        Buckle((buckle, buckleComp), (strap, strapComp), user);
+        return true;
+    }
 
-        if (TryComp<AppearanceComponent>(buckleUid, out var appearance))
-            Appearance.SetData(buckleUid, BuckleVisuals.Buckled, true, appearance);
+    private void Buckle(Entity<BuckleComponent> buckle, Entity<StrapComponent> strap, EntityUid? user)
+    {
+        if (user == buckle.Owner)
+            _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):player} buckled themselves to {ToPrettyString(strap)}");
+        else if (user != null)
+            _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):player} buckled {ToPrettyString(buckle)} to {ToPrettyString(strap)}");
 
-        _rotationVisuals.SetHorizontalAngle(buckleUid, strapComp.Rotation);
+        _audio.PlayPredicted(strap.Comp.BuckleSound, strap, user);
 
-        ReAttach(buckleUid, strapUid, buckleComp, strapComp);
-        SetBuckledTo(buckleUid, strapUid, strapComp, buckleComp);
-        // TODO user is currently set to null because if it isn't the sound fails to play in some situations, fix that
-        _audio.PlayPredicted(strapComp.BuckleSound, strapUid, userUid);
+        SetBuckledTo(buckle, strap!);
+        Appearance.SetData(strap, StrapVisuals.State, true);
+        Appearance.SetData(buckle, BuckleVisuals.Buckled, true);
 
-        var ev = new BuckleChangeEvent(strapUid, buckleUid, true);
-        RaiseLocalEvent(ev.BuckledEntity, ref ev);
-        RaiseLocalEvent(ev.StrapEntity, ref ev);
+        _rotationVisuals.SetHorizontalAngle(buckle.Owner, strap.Comp.Rotation);
 
-        if (TryComp<PullableComponent>(buckleUid, out var ownerPullable))
-        {
-            if (ownerPullable.Puller != null)
-            {
-                _pulling.TryStopPull(buckleUid, ownerPullable);
-            }
-        }
+        var xform = Transform(buckle);
+        var coords = new EntityCoordinates(strap, strap.Comp.BuckleOffset);
+        _transform.SetCoordinates(buckle, xform, coords, rotation: Angle.Zero);
 
-        if (TryComp<PhysicsComponent>(buckleUid, out var physics))
-        {
-            _physics.ResetDynamics(buckleUid, physics);
-        }
+        _joints.SetRelay(buckle, strap);
 
-        if (!buckleComp.PullStrap && TryComp<PullableComponent>(strapUid, out var toPullable))
+        switch (strap.Comp.Position)
         {
-            if (toPullable.Puller == buckleUid)
-            {
-                // can't pull it and buckle to it at the same time
-                _pulling.TryStopPull(strapUid, toPullable);
-            }
+            case StrapPosition.Stand:
+                _standing.Stand(buckle);
+                break;
+            case StrapPosition.Down:
+                _standing.Down(buckle, false, false);
+                break;
         }
 
-        // Logging
-        if (userUid != buckleUid)
-            _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} buckled {ToPrettyString(buckleUid)} to {ToPrettyString(strapUid)}");
-        else
-            _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} buckled themselves to {ToPrettyString(strapUid)}");
+        var ev = new StrappedEvent(strap, buckle);
+        RaiseLocalEvent(strap, ref ev);
 
-        return true;
+        var gotEv = new BuckledEvent(strap, buckle);
+        RaiseLocalEvent(buckle, ref gotEv);
+
+        if (TryComp<PhysicsComponent>(buckle, out var physics))
+            _physics.ResetDynamics(buckle, physics);
+
+        DebugTools.AssertEqual(xform.ParentUid, strap.Owner);
     }
 
     /// <summary>
     /// Tries to unbuckle the Owner of this component from its current strap.
     /// </summary>
     /// <param name="buckleUid">The entity to unbuckle.</param>
-    /// <param name="userUid">The entity doing the unbuckling.</param>
-    /// <param name="force">
-    /// Whether to force the unbuckling or not. Does not guarantee true to
-    /// be returned, but guarantees the owner to be unbuckled afterwards.
-    /// </param>
+    /// <param name="user">The entity doing the unbuckling.</param>
     /// <param name="buckleComp">The buckle component of the entity to unbuckle.</param>
     /// <returns>
     ///     true if the owner was unbuckled, otherwise false even if the owner
     ///     was previously already unbuckled.
     /// </returns>
-    public bool TryUnbuckle(EntityUid buckleUid, EntityUid userUid, bool force = false, BuckleComponent? buckleComp = null)
+    public bool TryUnbuckle(EntityUid buckleUid,
+        EntityUid? user,
+        BuckleComponent? buckleComp = null,
+        bool popup = true)
     {
-        if (!Resolve(buckleUid, ref buckleComp, false) ||
-            buckleComp.BuckledTo is not { } strapUid)
+        return TryUnbuckle((buckleUid, buckleComp), user, popup);
+    }
+
+    public bool TryUnbuckle(Entity<BuckleComponent?> buckle, EntityUid? user, bool popup)
+    {
+        if (!Resolve(buckle.Owner, ref buckle.Comp))
             return false;
 
-        if (!force)
-        {
-            var attemptEvent = new BuckleAttemptEvent(strapUid, buckleUid, userUid, false);
-            RaiseLocalEvent(attemptEvent.BuckledEntity, ref attemptEvent);
-            RaiseLocalEvent(attemptEvent.StrapEntity, ref attemptEvent);
-            if (attemptEvent.Cancelled)
-                return false;
+        if (!CanUnbuckle(buckle, user, popup, out var strap))
+            return false;
 
-            if (_gameTiming.CurTime < buckleComp.BuckleTime + buckleComp.Delay)
-                return false;
+        Unbuckle(buckle!, strap, user);
+        return true;
+    }
 
-            if (!_interaction.InRangeUnobstructed(userUid, strapUid, buckleComp.Range, popup: true))
-                return false;
+    public void Unbuckle(Entity<BuckleComponent?> buckle, EntityUid? user)
+    {
+        if (!Resolve(buckle.Owner, ref buckle.Comp, false))
+            return;
 
-            if (HasComp<SleepingComponent>(buckleUid) && buckleUid == userUid)
-                return false;
+        if (buckle.Comp.BuckledTo is not { } strap)
+            return;
 
-            // If the person is crit or dead in any kind of strap, return. This prevents people from unbuckling themselves while incapacitated.
-            if (_mobState.IsIncapacitated(buckleUid) && userUid == buckleUid)
-                return false;
+        if (!TryComp(strap, out StrapComponent? strapComp))
+        {
+            Log.Error($"Encountered buckle {ToPrettyString(buckle.Owner)} with invalid strap entity {ToPrettyString(strap)}");
+            SetBuckledTo(buckle!, null);
+            return;
         }
 
-        // Logging
-        if (userUid != buckleUid)
-            _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} unbuckled {ToPrettyString(buckleUid)} from {ToPrettyString(strapUid)}");
-        else
-            _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} unbuckled themselves from {ToPrettyString(strapUid)}");
+        Unbuckle(buckle!, (strap, strapComp), user);
+    }
 
-        SetBuckledTo(buckleUid, null, null, buckleComp);
+    private void Unbuckle(Entity<BuckleComponent> buckle, Entity<StrapComponent> strap, EntityUid? user)
+    {
+        if (user == buckle.Owner)
+            _adminLogger.Add(LogType.Action, LogImpact.Low, $"{user} unbuckled themselves from {strap}");
+        else if (user != null)
+            _adminLogger.Add(LogType.Action, LogImpact.Low, $"{user} unbuckled {buckle} from {strap}");
 
-        if (!TryComp<StrapComponent>(strapUid, out var strapComp))
-            return false;
+        _audio.PlayPredicted(strap.Comp.UnbuckleSound, strap, user);
 
-        var buckleXform = Transform(buckleUid);
-        var oldBuckledXform = Transform(strapUid);
+        SetBuckledTo(buckle, null);
 
-        if (buckleXform.ParentUid == strapUid && !Terminating(buckleXform.ParentUid))
+        var buckleXform = Transform(buckle);
+        var oldBuckledXform = Transform(strap);
+
+        if (buckleXform.ParentUid == strap.Owner && !Terminating(buckleXform.ParentUid))
         {
-            _container.AttachParentToContainerOrGrid((buckleUid, buckleXform));
+            _transform.PlaceNextTo((buckle, buckleXform), (strap.Owner, oldBuckledXform));
+            buckleXform.ActivelyLerping = false;
 
-            var oldBuckledToWorldRot = _transform.GetWorldRotation(strapUid);
-            _transform.SetWorldRotation(buckleXform, oldBuckledToWorldRot);
+            var oldBuckledToWorldRot = _transform.GetWorldRotation(strap);
+            _transform.SetWorldRotationNoLerp((buckle, buckleXform), oldBuckledToWorldRot);
 
-            if (strapComp.UnbuckleOffset != Vector2.Zero)
-                buckleXform.Coordinates = oldBuckledXform.Coordinates.Offset(strapComp.UnbuckleOffset);
+            // TODO: This is doing 4 moveevents this is why I left the warning in, if you're going to remove it make it only do 1 moveevent.
+            if (strap.Comp.BuckleOffset != Vector2.Zero)
+            {
+                buckleXform.Coordinates = oldBuckledXform.Coordinates.Offset(strap.Comp.BuckleOffset);
+            }
         }
 
-        if (TryComp(buckleUid, out AppearanceComponent? appearance))
-            Appearance.SetData(buckleUid, BuckleVisuals.Buckled, false, appearance);
-        _rotationVisuals.ResetHorizontalAngle(buckleUid);
+        _rotationVisuals.ResetHorizontalAngle(buckle.Owner);
+        Appearance.SetData(strap, StrapVisuals.State, strap.Comp.BuckledEntities.Count != 0);
+        Appearance.SetData(buckle, BuckleVisuals.Buckled, false);
 
-        if (TryComp<MobStateComponent>(buckleUid, out var mobState)
-            && _mobState.IsIncapacitated(buckleUid, mobState)
-            || HasComp<KnockedDownComponent>(buckleUid))
-        {
-            _standing.Down(buckleUid);
-        }
+        if (HasComp<KnockedDownComponent>(buckle) || _mobState.IsIncapacitated(buckle))
+            _standing.Down(buckle, playSound: false);
         else
-        {
-            _standing.Stand(buckleUid);
-        }
+            _standing.Stand(buckle);
 
-        if (_mobState.IsIncapacitated(buckleUid, mobState))
-        {
-            _standing.Down(buckleUid);
-        }
-        if (strapComp.BuckledEntities.Remove(buckleUid))
-        {
-            strapComp.OccupiedSize -= buckleComp.Size;
-            //Dirty(strapUid);
-            Dirty(strapComp);
-        }
+        _joints.RefreshRelay(buckle);
 
-        _joints.RefreshRelay(buckleUid);
-        Appearance.SetData(strapUid, StrapVisuals.State, strapComp.BuckledEntities.Count != 0);
+        var buckleEv = new UnbuckledEvent(strap, buckle);
+        RaiseLocalEvent(buckle, ref buckleEv);
 
-        // TODO: Buckle listening to moveevents is sussy anyway.
-        if (!TerminatingOrDeleted(strapUid))
-            _audio.PlayPredicted(strapComp.UnbuckleSound, strapUid, userUid);
-
-        var ev = new BuckleChangeEvent(strapUid, buckleUid, false);
-        RaiseLocalEvent(buckleUid, ref ev);
-        RaiseLocalEvent(strapUid, ref ev);
+        var strapEv = new UnstrappedEvent(strap, buckle);
+        RaiseLocalEvent(strap, ref strapEv);
+    }
 
-        return true;
+    public bool CanUnbuckle(Entity<BuckleComponent?> buckle, EntityUid user, bool popup)
+    {
+        return CanUnbuckle(buckle, user, popup, out _);
     }
 
-    /// <summary>
-    /// Makes an entity toggle the buckling status of the owner to a
-    /// specific entity.
-    /// </summary>
-    /// <param name="buckleUid">The entity to buckle/unbuckle from <see cref="to"/>.</param>
-    /// <param name="userUid">The entity doing the buckling/unbuckling.</param>
-    /// <param name="strapUid">
-    /// The entity to toggle the buckle status of the owner to.
-    /// </param>
-    /// <param name="force">
-    /// Whether to force the unbuckling or not, if it happens. Does not
-    /// guarantee true to be returned, but guarantees the owner to be
-    /// unbuckled afterwards.
-    /// </param>
-    /// <param name="buckle">The buckle component of the entity to buckle/unbuckle from <see cref="to"/>.</param>
-    /// <returns>true if the buckling status was changed, false otherwise.</returns>
-    public bool ToggleBuckle(
-        EntityUid buckleUid,
-        EntityUid userUid,
-        EntityUid strapUid,
-        bool force = false,
-        BuckleComponent? buckle = null)
+    private bool CanUnbuckle(Entity<BuckleComponent?> buckle, EntityUid? user, bool popup, out Entity<StrapComponent> strap)
     {
-        if (!Resolve(buckleUid, ref buckle, false))
+        strap = default;
+        if (!Resolve(buckle.Owner, ref buckle.Comp))
             return false;
 
-        if (!buckle.Buckled)
-        {
-            return TryBuckle(buckleUid, userUid, strapUid, buckle);
-        }
-        else
+        if (buckle.Comp.BuckledTo is not { } strapUid)
+            return false;
+
+        if (!TryComp(strapUid, out StrapComponent? strapComp))
         {
-            return TryUnbuckle(buckleUid, userUid, force, buckle);
+            Log.Error($"Encountered buckle {ToPrettyString(buckle.Owner)} with invalid strap entity {ToPrettyString(strap)}");
+            SetBuckledTo(buckle!, null);
+            return false;
         }
 
+        strap = (strapUid, strapComp);
+        if (_gameTiming.CurTime < buckle.Comp.BuckleTime + buckle.Comp.Delay)
+            return false;
+
+        if (user != null && !_interaction.InRangeUnobstructed(user.Value, strap.Owner, buckle.Comp.Range, popup: popup))
+            return false;
+
+        var unbuckleAttempt = new UnbuckleAttemptEvent(strap, buckle!, user, popup);
+        RaiseLocalEvent(buckle, ref unbuckleAttempt);
+        if (unbuckleAttempt.Cancelled)
+            return false;
+
+        var unstrapAttempt = new UnstrapAttemptEvent(strap, buckle!, user, popup);
+        RaiseLocalEvent(strap, ref unstrapAttempt);
+        return !unstrapAttempt.Cancelled;
     }
-}
+
+}
\ No newline at end of file
diff --git a/Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs b/Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs
new file mode 100644
index 00000000000..59eff1f8c87
--- /dev/null
+++ b/Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs
@@ -0,0 +1,200 @@
+using System.Linq;
+using Content.Shared.Buckle.Components;
+using Content.Shared.DoAfter;
+using Content.Shared.DragDrop;
+using Content.Shared.IdentityManagement;
+using Content.Shared.Interaction;
+using Content.Shared.Verbs;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Buckle;
+
+// Partial class containing interaction & verb event handlers
+public abstract partial class SharedBuckleSystem
+{
+    private void InitializeInteraction()
+    {
+        SubscribeLocalEvent<StrapComponent, GetVerbsEvent<InteractionVerb>>(AddStrapVerbs);
+        SubscribeLocalEvent<StrapComponent, InteractHandEvent>(OnStrapInteractHand);
+        SubscribeLocalEvent<StrapComponent, DragDropTargetEvent>(OnStrapDragDropTarget);
+        SubscribeLocalEvent<StrapComponent, CanDropTargetEvent>(OnCanDropTarget);
+
+        SubscribeLocalEvent<BuckleComponent, GetVerbsEvent<InteractionVerb>>(AddUnbuckleVerb);
+    }
+
+    private void OnCanDropTarget(EntityUid uid, StrapComponent component, ref CanDropTargetEvent args)
+    {
+        args.CanDrop = StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component);
+        args.Handled = true;
+    }
+
+    private void OnStrapDragDropTarget(EntityUid uid, StrapComponent component, ref DragDropTargetEvent args)
+    {
+        if (!StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component))
+            return;
+
+        args.Handled = TryBuckle(args.Dragged, args.User, uid, popup: false);
+    }
+
+    private bool StrapCanDragDropOn(
+        EntityUid strapUid,
+        EntityUid userUid,
+        EntityUid targetUid,
+        EntityUid buckleUid,
+        StrapComponent? strapComp = null,
+        BuckleComponent? buckleComp = null)
+    {
+        if (!Resolve(strapUid, ref strapComp, false) ||
+            !Resolve(buckleUid, ref buckleComp, false))
+        {
+            return false;
+        }
+
+        bool Ignored(EntityUid entity) => entity == userUid || entity == buckleUid || entity == targetUid;
+
+        return _interaction.InRangeUnobstructed(targetUid, buckleUid, buckleComp.Range, predicate: Ignored);
+    }
+
+    private void OnStrapInteractHand(EntityUid uid, StrapComponent component, InteractHandEvent args)
+    {
+        if (args.Handled)
+            return;
+
+        if (!TryComp(args.User, out BuckleComponent? buckle))
+            return;
+
+        // Buckle self
+        if (buckle.BuckledTo == null && component.BuckleOnInteractHand && StrapHasSpace(uid, buckle, component))
+        {
+            TryBuckle(args.User, args.User, uid, buckle, popup: true);
+            args.Handled = true;
+            return;
+        }
+
+        // Unbuckle self
+        if (buckle.BuckledTo == uid && TryUnbuckle(args.User, args.User, buckle, popup: true))
+        {
+            args.Handled = true;
+            return;
+        }
+
+        // Unbuckle others
+        if (component.BuckledEntities.TryFirstOrNull(out var buckled) && TryUnbuckle(buckled.Value, args.User))
+        {
+            args.Handled = true;
+            return;
+        }
+
+        // TODO BUCKLE add out bool for whether a pop-up was generated or not.
+    }
+
+    private void OnBuckleInteractHand(Entity<BuckleComponent> ent, ref InteractHandEvent args)
+    {
+        if (args.Handled)
+            return;
+
+        if (ent.Comp.BuckledTo != null)
+            TryUnbuckle(ent!, args.User, popup: true);
+
+        // TODO BUCKLE add out bool for whether a pop-up was generated or not.
+        args.Handled = true;
+    }
+
+    private void AddStrapVerbs(EntityUid uid, StrapComponent component, GetVerbsEvent<InteractionVerb> args)
+    {
+        if (args.Hands == null || !args.CanAccess || !args.CanInteract || !component.Enabled)
+            return;
+
+        // Note that for whatever bloody reason, buckle component has its own interaction range. Additionally, this
+        // range can be set per-component, so we have to check a modified InRangeUnobstructed for every verb.
+
+        // Add unstrap verbs for every strapped entity.
+        foreach (var entity in component.BuckledEntities)
+        {
+            var buckledComp = Comp<BuckleComponent>(entity);
+
+            if (!_interaction.InRangeUnobstructed(args.User, args.Target, range: buckledComp.Range))
+                continue;
+
+            var verb = new InteractionVerb()
+            {
+                Act = () => TryUnbuckle(entity, args.User, buckleComp: buckledComp),
+                Category = VerbCategory.Unbuckle,
+                Text = entity == args.User
+                    ? Loc.GetString("verb-self-target-pronoun")
+                    : Identity.Name(entity, EntityManager)
+            };
+
+            // In the event that you have more than once entity with the same name strapped to the same object,
+            // these two verbs will be identical according to Verb.CompareTo, and only one with actually be added to
+            // the verb list. However this should rarely ever be a problem. If it ever is, it could be fixed by
+            // appending an integer to verb.Text to distinguish the verbs.
+
+            args.Verbs.Add(verb);
+        }
+
+        // Add a verb to buckle the user.
+        if (TryComp<BuckleComponent>(args.User, out var buckle) &&
+            buckle.BuckledTo != uid &&
+            args.User != uid &&
+            StrapHasSpace(uid, buckle, component) &&
+            _interaction.InRangeUnobstructed(args.User, args.Target, range: buckle.Range))
+        {
+            InteractionVerb verb = new()
+            {
+                Act = () => TryBuckle(args.User, args.User, args.Target, buckle),
+                Category = VerbCategory.Buckle,
+                Text = Loc.GetString("verb-self-target-pronoun")
+            };
+            args.Verbs.Add(verb);
+        }
+
+        // If the user is currently holding/pulling an entity that can be buckled, add a verb for that.
+        if (args.Using is { Valid: true } @using &&
+            TryComp<BuckleComponent>(@using, out var usingBuckle) &&
+            StrapHasSpace(uid, usingBuckle, component) &&
+            _interaction.InRangeUnobstructed(@using, args.Target, range: usingBuckle.Range))
+        {
+            // Check that the entity is unobstructed from the target (ignoring the user).
+            bool Ignored(EntityUid entity) => entity == args.User || entity == args.Target || entity == @using;
+            if (!_interaction.InRangeUnobstructed(@using, args.Target, usingBuckle.Range, predicate: Ignored))
+                return;
+
+            var isPlayer = _playerManager.TryGetSessionByEntity(@using, out var _);
+            InteractionVerb verb = new()
+            {
+                Act = () => TryBuckle(@using, args.User, args.Target, usingBuckle),
+                Category = VerbCategory.Buckle,
+                Text = Identity.Name(@using, EntityManager),
+                // just a held object, the user is probably just trying to sit down.
+                // If the used entity is a person being pulled, prioritize this verb. Conversely, if it is
+                Priority = isPlayer ? 1 : -1
+            };
+
+            args.Verbs.Add(verb);
+        }
+    }
+
+    private void AddUnbuckleVerb(EntityUid uid, BuckleComponent component, GetVerbsEvent<InteractionVerb> args)
+    {
+        if (!args.CanAccess || !args.CanInteract || !component.Buckled)
+            return;
+
+        InteractionVerb verb = new()
+        {
+            Act = () => TryUnbuckle(uid, args.User, buckleComp: component),
+            Text = Loc.GetString("verb-categories-unbuckle"),
+            Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/unbuckle.svg.192dpi.png"))
+        };
+
+        if (args.Target == args.User && args.Using == null)
+        {
+            // A user is left clicking themselves with an empty hand, while buckled.
+            // It is very likely they are trying to unbuckle themselves.
+            verb.Priority = 1;
+        }
+
+        args.Verbs.Add(verb);
+    }
+
+}
diff --git a/Content.Shared/Buckle/SharedBuckleSystem.Strap.cs b/Content.Shared/Buckle/SharedBuckleSystem.Strap.cs
index 7be54360741..eb23aa973b4 100644
--- a/Content.Shared/Buckle/SharedBuckleSystem.Strap.cs
+++ b/Content.Shared/Buckle/SharedBuckleSystem.Strap.cs
@@ -2,40 +2,26 @@
 using Content.Shared.Buckle.Components;
 using Content.Shared.Construction;
 using Content.Shared.Destructible;
-using Content.Shared.DragDrop;
 using Content.Shared.Foldable;
-using Content.Shared.Interaction;
-using Content.Shared.Rotation;
 using Content.Shared.Storage;
-using Content.Shared.Verbs;
 using Robust.Shared.Containers;
 
 namespace Content.Shared.Buckle;
 
 public abstract partial class SharedBuckleSystem
 {
-    [Dependency] private readonly SharedRotationVisualsSystem _rotationVisuals = default!;
-
     private void InitializeStrap()
     {
         SubscribeLocalEvent<StrapComponent, ComponentStartup>(OnStrapStartup);
         SubscribeLocalEvent<StrapComponent, ComponentShutdown>(OnStrapShutdown);
-        SubscribeLocalEvent<StrapComponent, ComponentRemove>((_, c, _) => StrapRemoveAll(c));
+        SubscribeLocalEvent<StrapComponent, ComponentRemove>((e, c, _) => StrapRemoveAll(e, c));
 
-        SubscribeLocalEvent<StrapComponent, EntInsertedIntoContainerMessage>(OnStrapEntModifiedFromContainer);
-        SubscribeLocalEvent<StrapComponent, EntRemovedFromContainerMessage>(OnStrapEntModifiedFromContainer);
-        SubscribeLocalEvent<StrapComponent, GetVerbsEvent<InteractionVerb>>(AddStrapVerbs);
         SubscribeLocalEvent<StrapComponent, ContainerGettingInsertedAttemptEvent>(OnStrapContainerGettingInsertedAttempt);
-        SubscribeLocalEvent<StrapComponent, InteractHandEvent>(OnStrapInteractHand);
-        SubscribeLocalEvent<StrapComponent, DestructionEventArgs>((_,c,_) => StrapRemoveAll(c));
-        SubscribeLocalEvent<StrapComponent, BreakageEventArgs>((_, c, _) => StrapRemoveAll(c));
+        SubscribeLocalEvent<StrapComponent, DestructionEventArgs>((e, c, _) => StrapRemoveAll(e, c));
+        SubscribeLocalEvent<StrapComponent, BreakageEventArgs>((e, c, _) => StrapRemoveAll(e, c));
 
-        SubscribeLocalEvent<StrapComponent, DragDropTargetEvent>(OnStrapDragDropTarget);
-        SubscribeLocalEvent<StrapComponent, CanDropTargetEvent>(OnCanDropTarget);
         SubscribeLocalEvent<StrapComponent, FoldAttemptEvent>(OnAttemptFold);
-
-        SubscribeLocalEvent<StrapComponent, MoveEvent>(OnStrapMoveEvent);
-        SubscribeLocalEvent<StrapComponent, MachineDeconstructedEvent>((_, c, _) => StrapRemoveAll(c));
+        SubscribeLocalEvent<StrapComponent, MachineDeconstructedEvent>((e, c, _) => StrapRemoveAll(e, c));
     }
 
     private void OnStrapStartup(EntityUid uid, StrapComponent component, ComponentStartup args)
@@ -45,145 +31,17 @@ private void OnStrapStartup(EntityUid uid, StrapComponent component, ComponentSt
 
     private void OnStrapShutdown(EntityUid uid, StrapComponent component, ComponentShutdown args)
     {
-        if (LifeStage(uid) > EntityLifeStage.MapInitialized)
-            return;
-
-        StrapRemoveAll(component);
-    }
-
-    private void OnStrapEntModifiedFromContainer(EntityUid uid, StrapComponent component, ContainerModifiedMessage message)
-    {
-        if (_gameTiming.ApplyingState)
-            return;
-
-        foreach (var buckledEntity in component.BuckledEntities)
-        {
-            if (!TryComp<BuckleComponent>(buckledEntity, out var buckleComp))
-            {
-                continue;
-            }
-
-            ContainerModifiedReAttach(buckledEntity, uid, buckleComp, component);
-        }
-    }
-
-    private void ContainerModifiedReAttach(EntityUid buckleUid, EntityUid strapUid, BuckleComponent? buckleComp = null, StrapComponent? strapComp = null)
-    {
-        if (!Resolve(buckleUid, ref buckleComp, false) ||
-            !Resolve(strapUid, ref strapComp, false))
-            return;
-
-        var contained = _container.TryGetContainingContainer(buckleUid, out var ownContainer);
-        var strapContained = _container.TryGetContainingContainer(strapUid, out var strapContainer);
-
-        if (contained != strapContained || ownContainer != strapContainer)
-        {
-            TryUnbuckle(buckleUid, buckleUid, true, buckleComp);
-            return;
-        }
-
-        if (!contained)
-        {
-            ReAttach(buckleUid, strapUid, buckleComp, strapComp);
-        }
+        if (!TerminatingOrDeleted(uid))
+            StrapRemoveAll(uid, component);
     }
 
     private void OnStrapContainerGettingInsertedAttempt(EntityUid uid, StrapComponent component, ContainerGettingInsertedAttemptEvent args)
     {
         // If someone is attempting to put this item inside of a backpack, ensure that it has no entities strapped to it.
-        if (HasComp<StorageComponent>(args.Container.Owner) && component.BuckledEntities.Count != 0)
+        if (args.Container.ID == StorageComponent.ContainerId && component.BuckledEntities.Count != 0)
             args.Cancel();
     }
 
-    private void OnStrapInteractHand(EntityUid uid, StrapComponent component, InteractHandEvent args)
-    {
-        if (args.Handled)
-            return;
-
-        args.Handled = ToggleBuckle(args.User, args.User, uid);
-    }
-
-    private void AddStrapVerbs(EntityUid uid, StrapComponent component, GetVerbsEvent<InteractionVerb> args)
-    {
-        if (args.Hands == null || !args.CanAccess || !args.CanInteract || !component.Enabled)
-            return;
-
-        // Note that for whatever bloody reason, buckle component has its own interaction range. Additionally, this
-        // range can be set per-component, so we have to check a modified InRangeUnobstructed for every verb.
-
-        // Add unstrap verbs for every strapped entity.
-        foreach (var entity in component.BuckledEntities)
-        {
-            var buckledComp = Comp<BuckleComponent>(entity);
-
-            if (!_interaction.InRangeUnobstructed(args.User, args.Target, range: buckledComp.Range))
-                continue;
-
-            var verb = new InteractionVerb()
-            {
-                Act = () => TryUnbuckle(entity, args.User, buckleComp: buckledComp),
-                Category = VerbCategory.Unbuckle,
-                Text = entity == args.User
-                    ? Loc.GetString("verb-self-target-pronoun")
-                    : Comp<MetaDataComponent>(entity).EntityName
-            };
-
-            // In the event that you have more than once entity with the same name strapped to the same object,
-            // these two verbs will be identical according to Verb.CompareTo, and only one with actually be added to
-            // the verb list. However this should rarely ever be a problem. If it ever is, it could be fixed by
-            // appending an integer to verb.Text to distinguish the verbs.
-
-            args.Verbs.Add(verb);
-        }
-
-        // Add a verb to buckle the user.
-        if (TryComp<BuckleComponent>(args.User, out var buckle) &&
-            buckle.BuckledTo != uid &&
-            args.User != uid &&
-            StrapHasSpace(uid, buckle, component) &&
-            _interaction.InRangeUnobstructed(args.User, args.Target, range: buckle.Range))
-        {
-            InteractionVerb verb = new()
-            {
-                Act = () => TryBuckle(args.User, args.User, args.Target, buckle),
-                Category = VerbCategory.Buckle,
-                Text = Loc.GetString("verb-self-target-pronoun")
-            };
-            args.Verbs.Add(verb);
-        }
-
-        // If the user is currently holding/pulling an entity that can be buckled, add a verb for that.
-        if (args.Using is {Valid: true} @using &&
-            TryComp<BuckleComponent>(@using, out var usingBuckle) &&
-            StrapHasSpace(uid, usingBuckle, component) &&
-            _interaction.InRangeUnobstructed(@using, args.Target, range: usingBuckle.Range))
-        {
-            // Check that the entity is unobstructed from the target (ignoring the user).
-            bool Ignored(EntityUid entity) => entity == args.User || entity == args.Target || entity == @using;
-            if (!_interaction.InRangeUnobstructed(@using, args.Target, usingBuckle.Range, predicate: Ignored))
-                return;
-
-            var isPlayer = _playerManager.TryGetSessionByEntity(@using, out var _);
-            InteractionVerb verb = new()
-            {
-                Act = () => TryBuckle(@using, args.User, args.Target, usingBuckle),
-                Category = VerbCategory.Buckle,
-                Text = Comp<MetaDataComponent>(@using).EntityName,
-                // just a held object, the user is probably just trying to sit down.
-                // If the used entity is a person being pulled, prioritize this verb. Conversely, if it is
-                Priority = isPlayer ? 1 : -1
-            };
-
-            args.Verbs.Add(verb);
-        }
-    }
-
-    private void OnCanDropTarget(EntityUid uid, StrapComponent component, ref CanDropTargetEvent args)
-    {
-        args.CanDrop = StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component);
-        args.Handled = true;
-    }
-
     private void OnAttemptFold(EntityUid uid, StrapComponent component, ref FoldAttemptEvent args)
     {
         if (args.Cancelled)
@@ -192,82 +50,15 @@ private void OnAttemptFold(EntityUid uid, StrapComponent component, ref FoldAtte
         args.Cancelled = component.BuckledEntities.Count != 0;
     }
 
-    private void OnStrapDragDropTarget(EntityUid uid, StrapComponent component, ref DragDropTargetEvent args)
-    {
-        if (!StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component))
-            return;
-
-        args.Handled = TryBuckle(args.Dragged, args.User, uid);
-    }
-
-    private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveEvent args)
-    {
-        // TODO: This looks dirty af.
-        // On rotation of a strap, reattach all buckled entities.
-        // This fixes buckle offsets and draw depths.
-        // This is mega cursed. Please somebody save me from Mr Buckle's wild ride.
-        // Oh god I'm back here again. Send help.
-
-        // Consider a chair that has a player strapped to it. Then the client receives a new server state, showing
-        // that the player entity has moved elsewhere, and the chair has rotated. If the client applies the player
-        // state, then the chairs transform comp state, and then the buckle state. The transform state will
-        // forcefully teleport the player back to the chair (client-side only). This causes even more issues if the
-        // chair was teleporting in from nullspace after having left PVS.
-        //
-        // One option is to just never trigger re-buckles during state application.
-        // another is to.. just not do this? Like wtf is this code. But I CBF with buckle atm.
-
-        if (_gameTiming.ApplyingState || args.NewRotation == args.OldRotation)
-            return;
-
-        foreach (var buckledEntity in component.BuckledEntities)
-        {
-            if (!TryComp<BuckleComponent>(buckledEntity, out var buckled))
-                continue;
-
-            if (!buckled.Buckled || buckled.LastEntityBuckledTo != uid)
-            {
-                Log.Error($"A moving strap entity {ToPrettyString(uid)} attempted to re-parent an entity that does not 'belong' to it {ToPrettyString(buckledEntity)}");
-                continue;
-            }
-
-            ReAttach(buckledEntity, uid, buckled, component);
-            Dirty(buckled);
-        }
-    }
-
-    private bool StrapCanDragDropOn(
-        EntityUid strapUid,
-        EntityUid userUid,
-        EntityUid targetUid,
-        EntityUid buckleUid,
-        StrapComponent? strapComp = null,
-        BuckleComponent? buckleComp = null)
-    {
-        if (!Resolve(strapUid, ref strapComp, false) ||
-            !Resolve(buckleUid, ref buckleComp, false))
-        {
-            return false;
-        }
-
-        bool Ignored(EntityUid entity) => entity == userUid || entity == buckleUid || entity == targetUid;
-
-        return _interaction.InRangeUnobstructed(targetUid, buckleUid, buckleComp.Range, predicate: Ignored);
-    }
-
     /// <summary>
     /// Remove everything attached to the strap
     /// </summary>
-    private void StrapRemoveAll(StrapComponent strapComp)
+    private void StrapRemoveAll(EntityUid uid, StrapComponent strapComp)
     {
         foreach (var entity in strapComp.BuckledEntities.ToArray())
         {
             TryUnbuckle(entity, entity, true);
         }
-
-        strapComp.BuckledEntities.Clear();
-        strapComp.OccupiedSize = 0;
-        Dirty(strapComp);
     }
 
     private bool StrapHasSpace(EntityUid strapUid, BuckleComponent buckleComp, StrapComponent? strapComp = null)
@@ -275,30 +66,13 @@ private bool StrapHasSpace(EntityUid strapUid, BuckleComponent buckleComp, Strap
         if (!Resolve(strapUid, ref strapComp, false))
             return false;
 
-        return strapComp.OccupiedSize + buckleComp.Size <= strapComp.Size;
-    }
-
-    /// <summary>
-    /// Try to add an entity to the strap
-    /// </summary>
-    private bool StrapTryAdd(EntityUid strapUid, EntityUid buckleUid, BuckleComponent buckleComp, bool force = false, StrapComponent? strapComp = null)
-    {
-        if (!Resolve(strapUid, ref strapComp, false) ||
-            !strapComp.Enabled)
-            return false;
-
-        if (!force && !StrapHasSpace(strapUid, buckleComp, strapComp))
-            return false;
-
-        if (!strapComp.BuckledEntities.Add(buckleUid))
-            return false;
-
-        strapComp.OccupiedSize += buckleComp.Size;
-
-        Appearance.SetData(strapUid, StrapVisuals.State, true);
+        var avail = strapComp.Size;
+        foreach (var buckle in strapComp.BuckledEntities)
+        {
+            avail -= CompOrNull<BuckleComponent>(buckle)?.Size ?? 0;
+        }
 
-        Dirty(strapUid, strapComp);
-        return true;
+        return avail >= buckleComp.Size;
     }
 
     /// <summary>
@@ -311,8 +85,9 @@ public void StrapSetEnabled(EntityUid strapUid, bool enabled, StrapComponent? st
             return;
 
         strapComp.Enabled = enabled;
+        Dirty(strapUid, strapComp);
 
         if (!enabled)
-            StrapRemoveAll(strapComp);
+            StrapRemoveAll(strapUid, strapComp);
     }
 }
diff --git a/Content.Shared/Buckle/SharedBuckleSystem.cs b/Content.Shared/Buckle/SharedBuckleSystem.cs
index 67218657e52..770fababded 100644
--- a/Content.Shared/Buckle/SharedBuckleSystem.cs
+++ b/Content.Shared/Buckle/SharedBuckleSystem.cs
@@ -1,21 +1,17 @@
 using Content.Shared.ActionBlocker;
 using Content.Shared.Administration.Logs;
 using Content.Shared.Alert;
-using Content.Shared.Buckle.Components;
 using Content.Shared.Interaction;
 using Content.Shared.Mobs.Systems;
 using Content.Shared.Popups;
-using Content.Shared.Pulling;
+using Content.Shared.Rotation;
 using Content.Shared.Standing;
-using Robust.Shared.Audio;
 using Robust.Shared.Audio.Systems;
 using Robust.Shared.Containers;
-using Robust.Shared.Map;
 using Robust.Shared.Network;
 using Robust.Shared.Physics.Systems;
 using Robust.Shared.Player;
 using Robust.Shared.Timing;
-using PullingSystem = Content.Shared.Movement.Pulling.Systems.PullingSystem;
 
 namespace Content.Shared.Buckle;
 
@@ -36,10 +32,10 @@ public abstract partial class SharedBuckleSystem : EntitySystem
     [Dependency] private readonly SharedInteractionSystem _interaction = default!;
     [Dependency] private readonly SharedJointSystem _joints = default!;
     [Dependency] private readonly SharedPopupSystem _popup = default!;
-    [Dependency] private readonly PullingSystem _pulling = default!;
     [Dependency] private readonly SharedTransformSystem _transform = default!;
     [Dependency] private readonly StandingStateSystem _standing = default!;
     [Dependency] private readonly SharedPhysicsSystem _physics = default!;
+    [Dependency] private readonly SharedRotationVisualsSystem _rotationVisuals = default!;
 
     /// <inheritdoc/>
     public override void Initialize()
@@ -51,45 +47,6 @@ public override void Initialize()
 
         InitializeBuckle();
         InitializeStrap();
-    }
-
-    /// <summary>
-    /// Reattaches this entity to the strap, modifying its position and rotation.
-    /// </summary>
-    /// <param name="buckleUid">The entity to reattach.</param>
-    /// <param name="strapUid">The entity to reattach the buckleUid entity to.</param>
-    private void ReAttach(
-        EntityUid buckleUid,
-        EntityUid strapUid,
-        BuckleComponent? buckleComp = null,
-        StrapComponent? strapComp = null)
-    {
-        if (!Resolve(strapUid, ref strapComp, false)
-            || !Resolve(buckleUid, ref buckleComp, false))
-            return;
-
-        _transform.SetCoordinates(buckleUid, new EntityCoordinates(strapUid, strapComp.BuckleOffsetClamped));
-
-        var buckleTransform = Transform(buckleUid);
-
-        // Buckle subscribes to move for <reasons> so this might fail.
-        // TODO: Make buckle not do that.
-        if (buckleTransform.ParentUid != strapUid)
-            return;
-
-        _transform.SetLocalRotation(buckleUid, Angle.Zero, buckleTransform);
-        _joints.SetRelay(buckleUid, strapUid);
-
-        switch (strapComp.Position)
-        {
-            case StrapPosition.None:
-                break;
-            case StrapPosition.Stand:
-                _standing.Stand(buckleUid);
-                break;
-            case StrapPosition.Down:
-                _standing.Down(buckleUid, false, false);
-                break;
-        }
+        InitializeInteraction();
     }
 }
diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs
index 603855679c4..d8276c04aeb 100644
--- a/Content.Shared/CCVar/CCVars.cs
+++ b/Content.Shared/CCVar/CCVars.cs
@@ -21,16 +21,10 @@ public sealed class CCVars : CVars
             CVarDef.Create("server.id", "unknown_server_id", CVar.REPLICATED | CVar.SERVER);
 
         /// <summary>
-        ///     Name of the rules txt file in the "Resources/Server Info" dir. Include the extension.
+        ///     Guide Entry Prototype ID to be displayed as the server rules.
         /// </summary>
         public static readonly CVarDef<string> RulesFile =
-            CVarDef.Create("server.rules_file", "Rules.txt", CVar.REPLICATED | CVar.SERVER);
-
-        /// <summary>
-        ///     A loc string for what should be displayed as the title on the Rules window.
-        /// </summary>
-        public static readonly CVarDef<string> RulesHeader =
-            CVarDef.Create("server.rules_header", "ui-rules-header", CVar.REPLICATED | CVar.SERVER);
+            CVarDef.Create("server.rules_file", "DefaultRuleset", CVar.REPLICATED | CVar.SERVER);
 
         /*
          * Ambience
@@ -2071,7 +2065,13 @@ public static readonly CVarDef<string>
         ///     Don't show rules to localhost/loopback interface.
         /// </summary>
         public static readonly CVarDef<bool> RulesExemptLocal =
-            CVarDef.Create("rules.exempt_local", true, CVar.SERVERONLY);
+            CVarDef.Create("rules.exempt_local", true, CVar.CLIENT);
+
+        /// <summary>
+        /// The next time the rules will popup for this client, expressed in minutes
+        /// </summary>
+        public static readonly CVarDef<string> RulesNextPopupTime =
+            CVarDef.Create("rules.next_popup_time", "Jan 1, 1997", CVar.CLIENTONLY | CVar.ARCHIVE);
 
 
         /*
diff --git a/Content.Shared/Chapel/SharedSacrificialAltarSystem.cs b/Content.Shared/Chapel/SharedSacrificialAltarSystem.cs
index 92df7e0f6bf..61e9a68817c 100644
--- a/Content.Shared/Chapel/SharedSacrificialAltarSystem.cs
+++ b/Content.Shared/Chapel/SharedSacrificialAltarSystem.cs
@@ -15,7 +15,7 @@ public override void Initialize()
         base.Initialize();
 
         SubscribeLocalEvent<SacrificialAltarComponent, ExaminedEvent>(OnExamined);
-        SubscribeLocalEvent<SacrificialAltarComponent, BuckleChangeEvent>(OnUnstrapped);
+        SubscribeLocalEvent<SacrificialAltarComponent, UnbuckledEvent>(OnUnstrapped);
         SubscribeLocalEvent<SacrificialAltarComponent, GetVerbsEvent<AlternativeVerb>>(OnGetVerbs);
     }
 
@@ -24,7 +24,7 @@ private void OnExamined(Entity<SacrificialAltarComponent> ent, ref ExaminedEvent
         args.PushMarkup(Loc.GetString("altar-examine"));
     }
 
-    private void OnUnstrapped(Entity<SacrificialAltarComponent> ent, ref BuckleChangeEvent args)
+    private void OnUnstrapped(Entity<SacrificialAltarComponent> ent, ref UnbuckledEvent args)
     {
         if (ent.Comp.DoAfter is not { } id)
             return;
diff --git a/Content.Shared/Climbing/Systems/ClimbSystem.cs b/Content.Shared/Climbing/Systems/ClimbSystem.cs
index c570a821a6f..d2b5a25aee7 100644
--- a/Content.Shared/Climbing/Systems/ClimbSystem.cs
+++ b/Content.Shared/Climbing/Systems/ClimbSystem.cs
@@ -59,7 +59,7 @@ public override void Initialize()
         SubscribeLocalEvent<ClimbingComponent, EntParentChangedMessage>(OnParentChange);
         SubscribeLocalEvent<ClimbingComponent, ClimbDoAfterEvent>(OnDoAfter);
         SubscribeLocalEvent<ClimbingComponent, EndCollideEvent>(OnClimbEndCollide);
-        SubscribeLocalEvent<ClimbingComponent, BuckleChangeEvent>(OnBuckleChange);
+        SubscribeLocalEvent<ClimbingComponent, BuckledEvent>(OnBuckled);
 
         SubscribeLocalEvent<ClimbableComponent, CanDropTargetEvent>(OnCanDragDropOn);
         SubscribeLocalEvent<ClimbableComponent, GetVerbsEvent<AlternativeVerb>>(AddClimbableVerb);
@@ -479,10 +479,8 @@ public void ForciblyStopClimbing(EntityUid uid, ClimbingComponent? climbing = nu
         StopClimb(uid, climbing, fixtures);
     }
 
-    private void OnBuckleChange(EntityUid uid, ClimbingComponent component, ref BuckleChangeEvent args)
+    private void OnBuckled(EntityUid uid, ClimbingComponent component, ref BuckledEvent args)
     {
-        if (!args.Buckling)
-            return;
         StopClimb(uid, component);
     }
 
diff --git a/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs b/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs
index a447a54df17..5bf1e6739e9 100644
--- a/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs
+++ b/Content.Shared/Clothing/EntitySystems/SharedChameleonClothingSystem.cs
@@ -77,7 +77,7 @@ protected virtual void UpdateSprite(EntityUid uid, EntityPrototype proto) { }
     public bool IsValidTarget(EntityPrototype proto, SlotFlags chameleonSlot = SlotFlags.NONE)
     {
         // check if entity is valid
-        if (proto.Abstract || proto.NoSpawn)
+        if (proto.Abstract || proto.HideSpawnMenu)
             return false;
 
         // check if it is marked as valid chameleon target
diff --git a/Content.Shared/Clothing/Loadouts/Systems/LoadoutSystem.cs b/Content.Shared/Clothing/Loadouts/Systems/LoadoutSystem.cs
index 7ee0676f9ef..1f0bb99904d 100644
--- a/Content.Shared/Clothing/Loadouts/Systems/LoadoutSystem.cs
+++ b/Content.Shared/Clothing/Loadouts/Systems/LoadoutSystem.cs
@@ -21,6 +21,7 @@ public sealed class LoadoutSystem : EntitySystem
     [Dependency] private readonly InventorySystem _inventory = default!;
     [Dependency] private readonly IConfigurationManager _configuration = default!;
     [Dependency] private readonly CharacterRequirementsSystem _characterRequirements = default!;
+    [Dependency] private readonly SharedTransformSystem _sharedTransformSystem = default!;
 
     public override void Initialize()
     {
@@ -79,7 +80,7 @@ public List<EntityUid> ApplyCharacterLoadout(EntityUid uid, JobPrototype job, Hu
 
             // Spawn the loadout items
             var spawned = EntityManager.SpawnEntities(
-                EntityManager.GetComponent<TransformComponent>(uid).Coordinates.ToMap(EntityManager),
+                _sharedTransformSystem.GetMapCoordinates(uid),
                 loadoutProto.Items.Select(p => (string?) p.ToString()).ToList()); // Dumb cast
 
             foreach (var item in spawned)
diff --git a/Content.Shared/Clothing/MagbootsComponent.cs b/Content.Shared/Clothing/MagbootsComponent.cs
index 0d0d59f89f5..0d074ff38b6 100644
--- a/Content.Shared/Clothing/MagbootsComponent.cs
+++ b/Content.Shared/Clothing/MagbootsComponent.cs
@@ -1,3 +1,4 @@
+using Content.Shared.Alert;
 using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
@@ -16,4 +17,7 @@ public sealed partial class MagbootsComponent : Component
 
     [DataField("on"), AutoNetworkedField]
     public bool On;
+
+    [DataField]
+    public ProtoId<AlertPrototype> MagbootsAlert = "Magboots";
 }
diff --git a/Content.Shared/CombatMode/Pacification/PacificationSystem.cs b/Content.Shared/CombatMode/Pacification/PacificationSystem.cs
index 6d94c087af6..a927e1a6970 100644
--- a/Content.Shared/CombatMode/Pacification/PacificationSystem.cs
+++ b/Content.Shared/CombatMode/Pacification/PacificationSystem.cs
@@ -7,7 +7,6 @@
 using Content.Shared.Popups;
 using Content.Shared.Throwing;
 using Content.Shared.Weapons.Ranged.Events;
-using Content.Shared.Weapons.Ranged.Systems;
 using Robust.Shared.Timing;
 
 namespace Content.Shared.CombatMode.Pacification;
@@ -109,7 +108,7 @@ private void OnStartup(EntityUid uid, PacifiedComponent component, ComponentStar
             _actionsSystem.SetEnabled(combatMode.CombatToggleActionEntity, false);
         }
 
-        _alertsSystem.ShowAlert(uid, AlertType.Pacified);
+        _alertsSystem.ShowAlert(uid, component.PacifiedAlert);
     }
 
     private void OnShutdown(EntityUid uid, PacifiedComponent component, ComponentShutdown args)
@@ -121,7 +120,7 @@ private void OnShutdown(EntityUid uid, PacifiedComponent component, ComponentShu
             _combatSystem.SetCanDisarm(uid, true, combatMode);
 
         _actionsSystem.SetEnabled(combatMode.CombatToggleActionEntity, true);
-        _alertsSystem.ClearAlert(uid, AlertType.Pacified);
+        _alertsSystem.ClearAlert(uid, component.PacifiedAlert);
     }
 
     private void OnBeforeThrow(Entity<PacifiedComponent> ent, ref BeforeThrowEvent args)
diff --git a/Content.Shared/CombatMode/Pacification/PacifiedComponent.cs b/Content.Shared/CombatMode/Pacification/PacifiedComponent.cs
index 464ef778851..96081e5dc67 100644
--- a/Content.Shared/CombatMode/Pacification/PacifiedComponent.cs
+++ b/Content.Shared/CombatMode/Pacification/PacifiedComponent.cs
@@ -1,4 +1,6 @@
+using Content.Shared.Alert;
 using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
 
 namespace Content.Shared.CombatMode.Pacification;
 
@@ -42,4 +44,6 @@ public sealed partial class PacifiedComponent : Component
     [DataField]
     public EntityUid? LastAttackedEntity = null;
 
+    [DataField]
+    public ProtoId<AlertPrototype> PacifiedAlert = "Pacified";
 }
diff --git a/Content.Shared/Containers/ContainerFillSystem.cs b/Content.Shared/Containers/ContainerFillSystem.cs
index e120b6bc883..51c7c48e40f 100644
--- a/Content.Shared/Containers/ContainerFillSystem.cs
+++ b/Content.Shared/Containers/ContainerFillSystem.cs
@@ -1,4 +1,5 @@
 using System.Numerics;
+using Content.Shared.EntityTable;
 using Robust.Shared.Containers;
 using Robust.Shared.Map;
 
@@ -7,11 +8,14 @@ namespace Content.Shared.Containers;
 public sealed class ContainerFillSystem : EntitySystem
 {
     [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
+    [Dependency] private readonly EntityTableSystem _entityTable = default!;
+    [Dependency] private readonly SharedTransformSystem _transform = default!;
 
     public override void Initialize()
     {
         base.Initialize();
         SubscribeLocalEvent<ContainerFillComponent, MapInitEvent>(OnMapInit);
+        SubscribeLocalEvent<EntityTableContainerFillComponent, MapInitEvent>(OnTableMapInit);
     }
 
     private void OnMapInit(EntityUid uid, ContainerFillComponent component, MapInitEvent args)
@@ -42,4 +46,37 @@ private void OnMapInit(EntityUid uid, ContainerFillComponent component, MapInitE
             }
         }
     }
+
+    private void OnTableMapInit(Entity<EntityTableContainerFillComponent> ent, ref MapInitEvent args)
+    {
+        if (!TryComp(ent, out ContainerManagerComponent? containerComp))
+            return;
+
+        if (TerminatingOrDeleted(ent) || !Exists(ent))
+            return;
+
+        var xform = Transform(ent);
+        var coords = new EntityCoordinates(ent, Vector2.Zero);
+
+        foreach (var (containerId, table) in ent.Comp.Containers)
+        {
+            if (!_containerSystem.TryGetContainer(ent, containerId, out var container, containerComp))
+            {
+                Log.Error($"Entity {ToPrettyString(ent)} with a {nameof(EntityTableContainerFillComponent)} is missing a container ({containerId}).");
+                continue;
+            }
+
+            var spawns = _entityTable.GetSpawns(table);
+            foreach (var proto in spawns)
+            {
+                var spawn = Spawn(proto, coords);
+                if (!_containerSystem.Insert(spawn, container, containerXform: xform))
+                {
+                    Log.Error($"Entity {ToPrettyString(ent)} with a {nameof(EntityTableContainerFillComponent)} failed to insert an entity: {ToPrettyString(spawn)}.");
+                    _transform.AttachToGridOrMap(spawn);
+                    break;
+                }
+            }
+        }
+    }
 }
diff --git a/Content.Shared/Containers/EntityTableContainerFillComponent.cs b/Content.Shared/Containers/EntityTableContainerFillComponent.cs
new file mode 100644
index 00000000000..3f30dc86d6d
--- /dev/null
+++ b/Content.Shared/Containers/EntityTableContainerFillComponent.cs
@@ -0,0 +1,13 @@
+using Content.Shared.EntityTable.EntitySelectors;
+
+namespace Content.Shared.Containers;
+
+/// <summary>
+/// Version of <see cref="ContainerFillComponent"/> that utilizes <see cref="EntityTableSelector"/>
+/// </summary>
+[RegisterComponent, Access(typeof(ContainerFillSystem))]
+public sealed partial class EntityTableContainerFillComponent : Component
+{
+    [DataField]
+    public Dictionary<string, EntityTableSelector> Containers = new();
+}
diff --git a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs
index cb6b2a747bc..4bdcdf07695 100644
--- a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs
+++ b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs
@@ -96,7 +96,7 @@ private void Oninitialize(EntityUid uid, ItemSlotsComponent itemSlots, Component
         /// </summary>
         public void AddItemSlot(EntityUid uid, string id, ItemSlot slot, ItemSlotsComponent? itemSlots = null)
         {
-            itemSlots ??= EntityManager.EnsureComponent<ItemSlotsComponent>(uid);
+            itemSlots ??= EnsureComp<ItemSlotsComponent>(uid);
             DebugTools.AssertOwner(uid, itemSlots);
 
             if (itemSlots.Slots.TryGetValue(id, out var existing))
@@ -110,7 +110,7 @@ public void AddItemSlot(EntityUid uid, string id, ItemSlot slot, ItemSlotsCompon
 
             slot.ContainerSlot = _containers.EnsureContainer<ContainerSlot>(uid, id);
             itemSlots.Slots[id] = slot;
-            Dirty(itemSlots);
+            Dirty(uid, itemSlots);
         }
 
         /// <summary>
@@ -134,7 +134,7 @@ public void RemoveItemSlot(EntityUid uid, ItemSlot slot, ItemSlotsComponent? ite
             if (itemSlots.Slots.Count == 0)
                 EntityManager.RemoveComponent(uid, itemSlots);
             else
-                Dirty(itemSlots);
+                Dirty(uid, itemSlots);
         }
 
         public bool TryGetSlot(EntityUid uid, string slotId, [NotNullWhen(true)] out ItemSlot? itemSlot, ItemSlotsComponent? component = null)
diff --git a/Content.Shared/Cuffs/Components/CuffableComponent.cs b/Content.Shared/Cuffs/Components/CuffableComponent.cs
index 5da6fa41a5f..4ddfe1b53ee 100644
--- a/Content.Shared/Cuffs/Components/CuffableComponent.cs
+++ b/Content.Shared/Cuffs/Components/CuffableComponent.cs
@@ -1,6 +1,8 @@
+using Content.Shared.Alert;
 using Content.Shared.Damage;
 using Robust.Shared.Containers;
 using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
 using Robust.Shared.Utility;
 
@@ -39,6 +41,9 @@ public sealed partial class CuffableComponent : Component
     /// </summary>
     [DataField("canStillInteract"), ViewVariables(VVAccess.ReadWrite)]
     public bool CanStillInteract = true;
+
+    [DataField]
+    public ProtoId<AlertPrototype> CuffedAlert = "Handcuffed";
 }
 
 [Serializable, NetSerializable]
diff --git a/Content.Shared/Cuffs/SharedCuffableSystem.cs b/Content.Shared/Cuffs/SharedCuffableSystem.cs
index 9777b239884..d70a1e63083 100644
--- a/Content.Shared/Cuffs/SharedCuffableSystem.cs
+++ b/Content.Shared/Cuffs/SharedCuffableSystem.cs
@@ -75,6 +75,7 @@ public override void Initialize()
             SubscribeLocalEvent<CuffableComponent, IsUnequippingAttemptEvent>(OnUnequipAttempt);
             SubscribeLocalEvent<CuffableComponent, BeingPulledAttemptEvent>(OnBeingPulledAttempt);
             SubscribeLocalEvent<CuffableComponent, BuckleAttemptEvent>(OnBuckleAttemptEvent);
+            SubscribeLocalEvent<CuffableComponent, UnbuckleAttemptEvent>(OnUnbuckleAttemptEvent);
             SubscribeLocalEvent<CuffableComponent, GetVerbsEvent<Verb>>(AddUncuffVerb);
             SubscribeLocalEvent<CuffableComponent, UnCuffDoAfterEvent>(OnCuffableDoAfter);
             SubscribeLocalEvent<CuffableComponent, PullStartedMessage>(OnPull);
@@ -177,12 +178,13 @@ public void UpdateCuffState(EntityUid uid, CuffableComponent component)
 
             if (component.CanStillInteract)
             {
-                _alerts.ClearAlert(uid, AlertType.Handcuffed);
+                _alerts.ClearAlert(uid, component.CuffedAlert);
                 RaiseLocalEvent(uid, new MoodRemoveEffectEvent("Handcuffed"));
             }
+
             else
             {
-                _alerts.ShowAlert(uid, AlertType.Handcuffed);
+                _alerts.ShowAlert(uid, component.CuffedAlert);
                 RaiseLocalEvent(uid, new MoodEffectEvent("Handcuffed"));
             }
 
@@ -199,21 +201,33 @@ private void OnBeingPulledAttempt(EntityUid uid, CuffableComponent component, Be
                 args.Cancel();
         }
 
-        private void OnBuckleAttemptEvent(EntityUid uid, CuffableComponent component, ref BuckleAttemptEvent args)
+        private void OnBuckleAttempt(Entity<CuffableComponent> ent, EntityUid? user, ref bool cancelled, bool buckling, bool popup)
         {
-            // if someone else is doing it, let it pass.
-            if (args.UserEntity != uid)
+            if (cancelled || user != ent.Owner)
+                return;
+
+            if (!TryComp<HandsComponent>(ent, out var hands) || ent.Comp.CuffedHandCount != hands.Count)
                 return;
 
-            if (!TryComp<HandsComponent>(uid, out var hands) || component.CuffedHandCount != hands.Count)
+            cancelled = true;
+            if (!popup)
                 return;
 
-            args.Cancelled = true;
-            var message = args.Buckling
+            var message = buckling
                 ? Loc.GetString("handcuff-component-cuff-interrupt-buckled-message")
                 : Loc.GetString("handcuff-component-cuff-interrupt-unbuckled-message");
 
-            _popup.PopupClient(message, uid, args.UserEntity);
+            _popup.PopupClient(message, ent, user);
+        }
+
+        private void OnBuckleAttemptEvent(Entity<CuffableComponent> ent, ref BuckleAttemptEvent args)
+        {
+            OnBuckleAttempt(ent, args.User, ref args.Cancelled, true, args.Popup);
+        }
+
+        private void OnUnbuckleAttemptEvent(Entity<CuffableComponent> ent, ref UnbuckleAttemptEvent args)
+        {
+            OnBuckleAttempt(ent, args.User, ref args.Cancelled, false, args.Popup);
         }
 
         private void OnPull(EntityUid uid, CuffableComponent component, PullMessage args)
@@ -739,4 +753,4 @@ private sealed partial class AddCuffDoAfterEvent : SimpleDoAfterEvent
         {
         }
     }
-}
\ No newline at end of file
+}
diff --git a/Content.Shared/Damage/Components/StaminaComponent.cs b/Content.Shared/Damage/Components/StaminaComponent.cs
index b78fe978090..46eaa9f1f05 100644
--- a/Content.Shared/Damage/Components/StaminaComponent.cs
+++ b/Content.Shared/Damage/Components/StaminaComponent.cs
@@ -1,4 +1,6 @@
+using Content.Shared.Alert;
 using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
 namespace Content.Shared.Damage.Components;
@@ -70,4 +72,7 @@ public sealed partial class StaminaComponent : Component
     /// </summary>
     [DataField, AutoNetworkedField]
     public float SlowdownMultiplier = 0.75f;
-}
\ No newline at end of file
+
+    [DataField]
+    public ProtoId<AlertPrototype> StaminaAlert = "Stamina";
+}
diff --git a/Content.Shared/Damage/Systems/StaminaSystem.cs b/Content.Shared/Damage/Systems/StaminaSystem.cs
index e4840a6630b..40d1c77314f 100644
--- a/Content.Shared/Damage/Systems/StaminaSystem.cs
+++ b/Content.Shared/Damage/Systems/StaminaSystem.cs
@@ -84,8 +84,7 @@ private void OnShutdown(EntityUid uid, StaminaComponent component, ComponentShut
         {
             RemCompDeferred<ActiveStaminaComponent>(uid);
         }
-
-        SetStaminaAlert(uid);
+        _alerts.ClearAlert(uid, component.StaminaAlert);
     }
 
     private void OnStartup(EntityUid uid, StaminaComponent component, ComponentStartup args)
@@ -230,13 +229,10 @@ private void OnCollide(EntityUid uid, StaminaDamageOnCollideComponent component,
     private void SetStaminaAlert(EntityUid uid, StaminaComponent? component = null)
     {
         if (!Resolve(uid, ref component, false) || component.Deleted)
-        {
-            _alerts.ClearAlert(uid, AlertType.Stamina);
             return;
-        }
 
         var severity = ContentHelpers.RoundToLevels(MathF.Max(0f, component.CritThreshold - component.StaminaDamage), component.CritThreshold, 7);
-        _alerts.ShowAlert(uid, AlertType.Stamina, (short) severity);
+        _alerts.ShowAlert(uid, component.StaminaAlert, (short) severity);
     }
 
     /// <summary>
@@ -304,7 +300,7 @@ public void TakeStaminaDamage(EntityUid uid, float value, StaminaComponent? comp
         }
 
         EnsureComp<ActiveStaminaComponent>(uid);
-        Dirty(component);
+        Dirty(uid, component);
 
         if (value <= 0)
             return;
@@ -410,7 +406,7 @@ private void EnterStamCrit(EntityUid uid, StaminaComponent? component = null)
         // Give them buffer before being able to be re-stunned
         component.NextUpdate = _timing.CurTime + component.StunTime + StamCritBufferTime;
         EnsureComp<ActiveStaminaComponent>(uid);
-        Dirty(component);
+        Dirty(uid, component);
         _adminLogger.Add(LogType.Stamina, LogImpact.Medium, $"{ToPrettyString(uid):user} entered stamina crit");
     }
 
@@ -424,7 +420,8 @@ private void ExitStamCrit(EntityUid uid, StaminaComponent? component = null)
         component.NextUpdate = _timing.CurTime;
         _movementSpeed.RefreshMovementSpeedModifiers(uid);
         SetStaminaAlert(uid, component);
-        Dirty(component);
+        RemComp<ActiveStaminaComponent>(uid);
+        Dirty(uid, component);
         _adminLogger.Add(LogType.Stamina, LogImpact.Low, $"{ToPrettyString(uid):user} recovered from stamina crit");
     }
 }
@@ -433,4 +430,4 @@ private void ExitStamCrit(EntityUid uid, StaminaComponent? component = null)
 ///     Raised before stamina damage is dealt to allow other systems to cancel it.
 /// </summary>
 [ByRefEvent]
-public record struct BeforeStaminaDamageEvent(float Value, bool Cancelled = false);
\ No newline at end of file
+public record struct BeforeStaminaDamageEvent(float Value, bool Cancelled = false);
diff --git a/Content.Shared/Decals/SharedDecalSystem.cs b/Content.Shared/Decals/SharedDecalSystem.cs
index 9dbef60f0da..0a2349ea299 100644
--- a/Content.Shared/Decals/SharedDecalSystem.cs
+++ b/Content.Shared/Decals/SharedDecalSystem.cs
@@ -69,7 +69,7 @@ private void OnCompStartup(EntityUid uid, DecalGridComponent component, Componen
 
             // This **shouldn't** be required, but just in case we ever get entity prototypes that have decal grids, we
             // need to ensure that we send an initial full state to players.
-            Dirty(component);
+            Dirty(uid, component);
         }
 
         protected Dictionary<Vector2i, DecalChunk>? ChunkCollection(EntityUid gridEuid, DecalGridComponent? comp = null)
diff --git a/Content.Shared/Destructible/Thresholds/MinMax.cs b/Content.Shared/Destructible/Thresholds/MinMax.cs
new file mode 100644
index 00000000000..e086a0f61c3
--- /dev/null
+++ b/Content.Shared/Destructible/Thresholds/MinMax.cs
@@ -0,0 +1,24 @@
+using Robust.Shared.Random;
+
+namespace Content.Shared.Destructible.Thresholds;
+
+[DataDefinition, Serializable]
+public partial struct MinMax
+{
+    [DataField]
+    public int Min;
+
+    [DataField]
+    public int Max;
+
+    public MinMax(int min, int max)
+    {
+        Min = min;
+        Max = max;
+    }
+
+    public int Next(IRobustRandom random)
+    {
+        return random.Next(Min, Max + 1);
+    }
+}
diff --git a/Content.Shared/Dice/SharedDiceSystem.cs b/Content.Shared/Dice/SharedDiceSystem.cs
index defb3d5f0e3..8e2868e791d 100644
--- a/Content.Shared/Dice/SharedDiceSystem.cs
+++ b/Content.Shared/Dice/SharedDiceSystem.cs
@@ -59,7 +59,7 @@ public void SetCurrentSide(EntityUid uid, int side, DiceComponent? die = null)
         }
 
         die.CurrentValue = (side - die.Offset) * die.Multiplier;
-        Dirty(die);
+        Dirty(uid, die);
         UpdateVisuals(uid, die);
     }
 
diff --git a/Content.Shared/Electrocution/SharedElectrocutionSystem.cs b/Content.Shared/Electrocution/SharedElectrocutionSystem.cs
index 5031d8a9115..b228a987af4 100644
--- a/Content.Shared/Electrocution/SharedElectrocutionSystem.cs
+++ b/Content.Shared/Electrocution/SharedElectrocutionSystem.cs
@@ -20,7 +20,7 @@ public void SetInsulatedSiemensCoefficient(EntityUid uid, float siemensCoefficie
                 return;
 
             insulated.Coefficient = siemensCoefficient;
-            Dirty(insulated);
+            Dirty(uid, insulated);
         }
 
         /// <param name="uid">Entity being electrocuted.</param>
diff --git a/Content.Shared/Emoting/EmoteSystem.cs b/Content.Shared/Emoting/EmoteSystem.cs
index fd6361245b1..1e06d7e982b 100644
--- a/Content.Shared/Emoting/EmoteSystem.cs
+++ b/Content.Shared/Emoting/EmoteSystem.cs
@@ -19,7 +19,7 @@ public void SetEmoting(EntityUid uid, bool value, EmotingComponent? component =
         if (component.Enabled == value)
             return;
 
-        Dirty(component);
+        Dirty(uid, component);
     }
 
     private void OnEmoteAttempt(EmoteAttemptEvent args)
diff --git a/Content.Shared/Ensnaring/Components/EnsnareableComponent.cs b/Content.Shared/Ensnaring/Components/EnsnareableComponent.cs
index 553f6df1c77..2536fac4edc 100644
--- a/Content.Shared/Ensnaring/Components/EnsnareableComponent.cs
+++ b/Content.Shared/Ensnaring/Components/EnsnareableComponent.cs
@@ -1,5 +1,7 @@
+using Content.Shared.Alert;
 using Robust.Shared.Containers;
 using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
 
 namespace Content.Shared.Ensnaring.Components;
@@ -40,6 +42,9 @@ public sealed partial class EnsnareableComponent : Component
 
     [DataField("state")]
     public string? State;
+
+    [DataField]
+    public ProtoId<AlertPrototype> EnsnaredAlert = "Ensnared";
 }
 
 [Serializable, NetSerializable]
diff --git a/Content.Shared/EntityTable/EntitySelectors/AllSelector.cs b/Content.Shared/EntityTable/EntitySelectors/AllSelector.cs
new file mode 100644
index 00000000000..8fb8b5e546e
--- /dev/null
+++ b/Content.Shared/EntityTable/EntitySelectors/AllSelector.cs
@@ -0,0 +1,25 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.EntityTable.EntitySelectors;
+
+/// <summary>
+/// Gets spawns from all of the child selectors
+/// </summary>
+public sealed partial class AllSelector : EntityTableSelector
+{
+    [DataField(required: true)]
+    public List<EntityTableSelector> Children;
+
+    protected override IEnumerable<EntProtoId> GetSpawnsImplementation(System.Random rand,
+        IEntityManager entMan,
+        IPrototypeManager proto)
+    {
+        foreach (var child in Children)
+        {
+            foreach (var spawn in child.GetSpawns(rand, entMan, proto))
+            {
+                yield return spawn;
+            }
+        }
+    }
+}
diff --git a/Content.Shared/EntityTable/EntitySelectors/EntSelector.cs b/Content.Shared/EntityTable/EntitySelectors/EntSelector.cs
new file mode 100644
index 00000000000..b1e712b4b3a
--- /dev/null
+++ b/Content.Shared/EntityTable/EntitySelectors/EntSelector.cs
@@ -0,0 +1,27 @@
+using Content.Shared.EntityTable.ValueSelector;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.EntityTable.EntitySelectors;
+
+/// <summary>
+/// Gets the spawn for the entity prototype specified at whatever count specified.
+/// </summary>
+public sealed partial class EntSelector : EntityTableSelector
+{
+    [DataField(required: true)]
+    public EntProtoId Id;
+
+    [DataField]
+    public NumberSelector Amount = new ConstantNumberSelector(1);
+
+    protected override IEnumerable<EntProtoId> GetSpawnsImplementation(System.Random rand,
+        IEntityManager entMan,
+        IPrototypeManager proto)
+    {
+        var num = (int) Math.Round(Amount.Get(rand, entMan, proto));
+        for (var i = 0; i < num; i++)
+        {
+            yield return Id;
+        }
+    }
+}
diff --git a/Content.Shared/EntityTable/EntitySelectors/EntityTableSelector.cs b/Content.Shared/EntityTable/EntitySelectors/EntityTableSelector.cs
new file mode 100644
index 00000000000..2533f17dc51
--- /dev/null
+++ b/Content.Shared/EntityTable/EntitySelectors/EntityTableSelector.cs
@@ -0,0 +1,49 @@
+using Content.Shared.EntityTable.ValueSelector;
+using JetBrains.Annotations;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Shared.EntityTable.EntitySelectors;
+
+[ImplicitDataDefinitionForInheritors, UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
+public abstract partial class EntityTableSelector
+{
+    /// <summary>
+    /// The number of times this selector is run
+    /// </summary>
+    [DataField]
+    public NumberSelector Rolls = new ConstantNumberSelector(1);
+
+    /// <summary>
+    /// A weight used to pick between selectors.
+    /// </summary>
+    [DataField]
+    public float Weight = 1;
+
+    /// <summary>
+    /// A simple chance that the selector will run.
+    /// </summary>
+    [DataField]
+    public double Prob = 1;
+
+    public IEnumerable<EntProtoId> GetSpawns(System.Random rand,
+        IEntityManager entMan,
+        IPrototypeManager proto)
+    {
+        var rolls = Rolls.Get(rand, entMan, proto);
+        for (var i = 0; i < rolls; i++)
+        {
+            if (!rand.Prob(Prob))
+                continue;
+
+            foreach (var spawn in GetSpawnsImplementation(rand, entMan, proto))
+            {
+                yield return spawn;
+            }
+        }
+    }
+
+    protected abstract IEnumerable<EntProtoId> GetSpawnsImplementation(System.Random rand,
+        IEntityManager entMan,
+        IPrototypeManager proto);
+}
diff --git a/Content.Shared/EntityTable/EntitySelectors/GroupSelector.cs b/Content.Shared/EntityTable/EntitySelectors/GroupSelector.cs
new file mode 100644
index 00000000000..8f761f9866e
--- /dev/null
+++ b/Content.Shared/EntityTable/EntitySelectors/GroupSelector.cs
@@ -0,0 +1,28 @@
+using Content.Shared.Random.Helpers;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.EntityTable.EntitySelectors;
+
+/// <summary>
+/// Gets the spawns from one of the child selectors, based on the weight of the children
+/// </summary>
+public sealed partial class GroupSelector : EntityTableSelector
+{
+    [DataField(required: true)]
+    public List<EntityTableSelector> Children = new();
+
+    protected override IEnumerable<EntProtoId> GetSpawnsImplementation(System.Random rand,
+        IEntityManager entMan,
+        IPrototypeManager proto)
+    {
+        var children = new Dictionary<EntityTableSelector, float>(Children.Count);
+        foreach (var child in Children)
+        {
+            children.Add(child, child.Weight);
+        }
+
+        var pick = SharedRandomExtensions.Pick(children, rand);
+
+        return pick.GetSpawns(rand, entMan, proto);
+    }
+}
diff --git a/Content.Shared/EntityTable/EntitySelectors/NestedSelector.cs b/Content.Shared/EntityTable/EntitySelectors/NestedSelector.cs
new file mode 100644
index 00000000000..fc8d8f08d37
--- /dev/null
+++ b/Content.Shared/EntityTable/EntitySelectors/NestedSelector.cs
@@ -0,0 +1,20 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.EntityTable.EntitySelectors;
+
+/// <summary>
+/// Gets the spawns from the entity table prototype specified.
+/// Can be used to reuse common tables.
+/// </summary>
+public sealed partial class NestedSelector : EntityTableSelector
+{
+    [DataField(required: true)]
+    public ProtoId<EntityTablePrototype> TableId;
+
+    protected override IEnumerable<EntProtoId> GetSpawnsImplementation(System.Random rand,
+        IEntityManager entMan,
+        IPrototypeManager proto)
+    {
+        return proto.Index(TableId).Table.GetSpawns(rand, entMan, proto);
+    }
+}
diff --git a/Content.Shared/EntityTable/EntitySelectors/NoneSelector.cs b/Content.Shared/EntityTable/EntitySelectors/NoneSelector.cs
new file mode 100644
index 00000000000..21fcb6d2792
--- /dev/null
+++ b/Content.Shared/EntityTable/EntitySelectors/NoneSelector.cs
@@ -0,0 +1,16 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.EntityTable.EntitySelectors;
+
+/// <summary>
+/// Selects nothing.
+/// </summary>
+public sealed partial class NoneSelector : EntityTableSelector
+{
+    protected override IEnumerable<EntProtoId> GetSpawnsImplementation(System.Random rand,
+        IEntityManager entMan,
+        IPrototypeManager proto)
+    {
+        yield break;
+    }
+}
diff --git a/Content.Shared/EntityTable/EntityTablePrototype.cs b/Content.Shared/EntityTable/EntityTablePrototype.cs
new file mode 100644
index 00000000000..63cebe9aeb7
--- /dev/null
+++ b/Content.Shared/EntityTable/EntityTablePrototype.cs
@@ -0,0 +1,18 @@
+using Content.Shared.EntityTable.EntitySelectors;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.EntityTable;
+
+/// <summary>
+/// This is a prototype for...
+/// </summary>
+[Prototype]
+public sealed partial class EntityTablePrototype : IPrototype
+{
+    /// <inheritdoc/>
+    [IdDataField]
+    public string ID { get; } = default!;
+
+    [DataField(required: true)]
+    public EntityTableSelector Table = default!;
+}
diff --git a/Content.Shared/EntityTable/EntityTableSystem.cs b/Content.Shared/EntityTable/EntityTableSystem.cs
new file mode 100644
index 00000000000..ff499e67604
--- /dev/null
+++ b/Content.Shared/EntityTable/EntityTableSystem.cs
@@ -0,0 +1,20 @@
+using Content.Shared.EntityTable.EntitySelectors;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Shared.EntityTable;
+
+public sealed class EntityTableSystem : EntitySystem
+{
+    [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+    [Dependency] private readonly IRobustRandom _random = default!;
+
+    public IEnumerable<EntProtoId> GetSpawns(EntityTableSelector? table, System.Random? rand = null)
+    {
+        if (table == null)
+            return new List<EntProtoId>();
+
+        rand ??= _random.GetRandom();
+        return table.GetSpawns(rand, EntityManager, _prototypeManager);
+    }
+}
diff --git a/Content.Shared/EntityTable/ValueSelector/ConstantNumberSelector.cs b/Content.Shared/EntityTable/ValueSelector/ConstantNumberSelector.cs
new file mode 100644
index 00000000000..0baf6785f4f
--- /dev/null
+++ b/Content.Shared/EntityTable/ValueSelector/ConstantNumberSelector.cs
@@ -0,0 +1,22 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.EntityTable.ValueSelector;
+
+/// <summary>
+/// Gives a constant value.
+/// </summary>
+public sealed partial class ConstantNumberSelector : NumberSelector
+{
+    [DataField]
+    public float Value = 1;
+
+    public ConstantNumberSelector(float value)
+    {
+        Value = value;
+    }
+
+    public override float Get(System.Random rand, IEntityManager entMan, IPrototypeManager proto)
+    {
+        return Value;
+    }
+}
diff --git a/Content.Shared/EntityTable/ValueSelector/NumberSelector.cs b/Content.Shared/EntityTable/ValueSelector/NumberSelector.cs
new file mode 100644
index 00000000000..8a7743c9dd8
--- /dev/null
+++ b/Content.Shared/EntityTable/ValueSelector/NumberSelector.cs
@@ -0,0 +1,16 @@
+using Content.Shared.EntityTable.EntitySelectors;
+using JetBrains.Annotations;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.EntityTable.ValueSelector;
+
+/// <summary>
+/// Used for implementing custom value selection for <see cref="EntityTableSelector"/>
+/// </summary>
+[ImplicitDataDefinitionForInheritors, UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
+public abstract partial class NumberSelector
+{
+    public abstract float Get(System.Random rand,
+        IEntityManager entMan,
+        IPrototypeManager proto);
+}
diff --git a/Content.Shared/EntityTable/ValueSelector/RangeNumberSelector.cs b/Content.Shared/EntityTable/ValueSelector/RangeNumberSelector.cs
new file mode 100644
index 00000000000..e8356fcbb72
--- /dev/null
+++ b/Content.Shared/EntityTable/ValueSelector/RangeNumberSelector.cs
@@ -0,0 +1,19 @@
+using System.Numerics;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Shared.EntityTable.ValueSelector;
+
+/// <summary>
+/// Gives a value between the two numbers specified, inclusive.
+/// </summary>
+public sealed partial class RangeNumberSelector : NumberSelector
+{
+    [DataField]
+    public Vector2 Range = new(1, 1);
+
+    public override float Get(System.Random rand, IEntityManager entMan, IPrototypeManager proto)
+    {
+        return rand.NextFloat(Range.X, Range.Y + 1);
+    }
+}
diff --git a/Content.Shared/Examine/ExamineSystemShared.cs b/Content.Shared/Examine/ExamineSystemShared.cs
index f792862be14..397a8f74484 100644
--- a/Content.Shared/Examine/ExamineSystemShared.cs
+++ b/Content.Shared/Examine/ExamineSystemShared.cs
@@ -175,9 +175,9 @@ public bool InRangeUnOccluded<TState>(MapCoordinates origin, MapCoordinates othe
                 length = MaxRaycastRange;
             }
 
-            var occluderSystem = Get<OccluderSystem>();
             IoCManager.Resolve(ref entMan);
 
+            var occluderSystem = EntityManager.System<OccluderSystem>();
             var ray = new Ray(origin.Position, dir.Normalized());
             var rayResults = occluderSystem
                 .IntersectRayWithPredicate(origin.MapId, ray, length, state, predicate, false).ToList();
@@ -194,7 +194,7 @@ public bool InRangeUnOccluded<TState>(MapCoordinates origin, MapCoordinates othe
                 }
 
                 var bBox = o.BoundingBox;
-                bBox = bBox.Translated(entMan.GetComponent<TransformComponent>(result.HitEntity).WorldPosition);
+                bBox = bBox.Translated(_transform.GetWorldPosition(result.HitEntity));
 
                 if (bBox.Contains(origin.Position) || bBox.Contains(other.Position))
                 {
@@ -210,8 +210,8 @@ public bool InRangeUnOccluded<TState>(MapCoordinates origin, MapCoordinates othe
         public bool InRangeUnOccluded(EntityUid origin, EntityUid other, float range = ExamineRange, Ignored? predicate = null, bool ignoreInsideBlocker = true)
         {
             var entMan = IoCManager.Resolve<IEntityManager>();
-            var originPos = entMan.GetComponent<TransformComponent>(origin).MapPosition;
-            var otherPos = entMan.GetComponent<TransformComponent>(other).MapPosition;
+            var originPos = _transform.GetMapCoordinates(origin);
+            var otherPos = _transform.GetMapCoordinates(other);
 
             return InRangeUnOccluded(originPos, otherPos, range, predicate, ignoreInsideBlocker);
         }
@@ -219,8 +219,8 @@ public bool InRangeUnOccluded(EntityUid origin, EntityUid other, float range = E
         public bool InRangeUnOccluded(EntityUid origin, EntityCoordinates other, float range = ExamineRange, Ignored? predicate = null, bool ignoreInsideBlocker = true)
         {
             var entMan = IoCManager.Resolve<IEntityManager>();
-            var originPos = entMan.GetComponent<TransformComponent>(origin).MapPosition;
-            var otherPos = other.ToMap(entMan);
+            var originPos = _transform.GetMapCoordinates(origin);
+            var otherPos = _transform.ToMapCoordinates(other);
 
             return InRangeUnOccluded(originPos, otherPos, range, predicate, ignoreInsideBlocker);
         }
@@ -228,7 +228,7 @@ public bool InRangeUnOccluded(EntityUid origin, EntityCoordinates other, float r
         public bool InRangeUnOccluded(EntityUid origin, MapCoordinates other, float range = ExamineRange, Ignored? predicate = null, bool ignoreInsideBlocker = true)
         {
             var entMan = IoCManager.Resolve<IEntityManager>();
-            var originPos = entMan.GetComponent<TransformComponent>(origin).MapPosition;
+            var originPos = _transform.GetMapCoordinates(origin);
 
             return InRangeUnOccluded(originPos, other, range, predicate, ignoreInsideBlocker);
         }
@@ -370,7 +370,7 @@ int Comparison(ExamineMessagePart a, ExamineMessagePart b)
         ///     sort messages the same as well as grouped together properly, even if subscriptions are different.
         ///     You should wrap it in a using() block so popping automatically occurs.
         /// </summary>
-        public ExamineGroupDisposable PushGroup(string groupName, int priority=0)
+        public ExamineGroupDisposable PushGroup(string groupName, int priority = 0)
         {
             // Ensure that other examine events correctly ended their groups.
             DebugTools.Assert(_currentGroupPart == null);
@@ -398,7 +398,7 @@ private void PopGroup()
         /// </summary>
         /// <seealso cref="PushMarkup"/>
         /// <seealso cref="PushText"/>
-        public void PushMessage(FormattedMessage message, int priority=0)
+        public void PushMessage(FormattedMessage message, int priority = 0)
         {
             if (message.Nodes.Count == 0)
                 return;
@@ -421,9 +421,9 @@ public void PushMessage(FormattedMessage message, int priority=0)
         /// </summary>
         /// <seealso cref="PushText"/>
         /// <seealso cref="PushMessage"/>
-        public void PushMarkup(string markup, int priority=0)
+        public void PushMarkup(string markup, int priority = 0)
         {
-            PushMessage(FormattedMessage.FromMarkup(markup), priority);
+            PushMessage(FormattedMessage.FromMarkupPermissive(markup), priority);
         }
 
         /// <summary>
@@ -433,7 +433,7 @@ public void PushMarkup(string markup, int priority=0)
         /// </summary>
         /// <seealso cref="PushMarkup"/>
         /// <seealso cref="PushMessage"/>
-        public void PushText(string text, int priority=0)
+        public void PushText(string text, int priority = 0)
         {
             var msg = new FormattedMessage();
             msg.AddText(text);
@@ -469,9 +469,9 @@ public void AddMessage(FormattedMessage message, int priority = 0)
         /// </summary>
         /// <seealso cref="AddText"/>
         /// <seealso cref="AddMessage"/>
-        public void AddMarkup(string markup, int priority=0)
+        public void AddMarkup(string markup, int priority = 0)
         {
-            AddMessage(FormattedMessage.FromMarkup(markup), priority);
+            AddMessage(FormattedMessage.FromMarkupPermissive(markup), priority);
         }
 
         /// <summary>
@@ -481,7 +481,7 @@ public void AddMarkup(string markup, int priority=0)
         /// </summary>
         /// <seealso cref="AddMarkup"/>
         /// <seealso cref="AddMessage"/>
-        public void AddText(string text, int priority=0)
+        public void AddText(string text, int priority = 0)
         {
             var msg = new FormattedMessage();
             msg.AddText(text);
diff --git a/Content.Shared/Explosion/Components/OnTrigger/SmokeOnTriggerComponent.cs b/Content.Shared/Explosion/Components/OnTrigger/SmokeOnTriggerComponent.cs
index 80d65f4c2cd..1138e74af8f 100644
--- a/Content.Shared/Explosion/Components/OnTrigger/SmokeOnTriggerComponent.cs
+++ b/Content.Shared/Explosion/Components/OnTrigger/SmokeOnTriggerComponent.cs
@@ -29,7 +29,7 @@ public sealed partial class SmokeOnTriggerComponent : Component
     /// Defaults to smoke but you can use foam if you want.
     /// </summary>
     [DataField, ViewVariables(VVAccess.ReadWrite)]
-    public ProtoId<EntityPrototype> SmokePrototype = "Smoke";
+    public EntProtoId SmokePrototype = "Smoke";
 
     /// <summary>
     /// Solution to add to each smoke cloud.
diff --git a/Content.Shared/Foldable/FoldableSystem.cs b/Content.Shared/Foldable/FoldableSystem.cs
index 10baf8165b5..2a846f4f234 100644
--- a/Content.Shared/Foldable/FoldableSystem.cs
+++ b/Content.Shared/Foldable/FoldableSystem.cs
@@ -26,7 +26,7 @@ public override void Initialize()
         SubscribeLocalEvent<FoldableComponent, StoreMobInItemContainerAttemptEvent>(OnStoreThisAttempt);
         SubscribeLocalEvent<FoldableComponent, StorageOpenAttemptEvent>(OnFoldableOpenAttempt);
 
-        SubscribeLocalEvent<FoldableComponent, BuckleAttemptEvent>(OnBuckleAttempt);
+        SubscribeLocalEvent<FoldableComponent, StrapAttemptEvent>(OnStrapAttempt);
     }
 
     private void OnHandleState(EntityUid uid, FoldableComponent component, ref AfterAutoHandleStateEvent args)
@@ -53,9 +53,9 @@ public void OnStoreThisAttempt(EntityUid uid, FoldableComponent comp, ref StoreM
             args.Cancelled = true;
     }
 
-    public void OnBuckleAttempt(EntityUid uid, FoldableComponent comp, ref BuckleAttemptEvent args)
+    public void OnStrapAttempt(EntityUid uid, FoldableComponent comp, ref StrapAttemptEvent args)
     {
-        if (args.Buckling && comp.IsFolded)
+        if (comp.IsFolded)
             args.Cancelled = true;
     }
 
diff --git a/Content.Shared/Friction/TileFrictionController.cs b/Content.Shared/Friction/TileFrictionController.cs
index 3583947ee36..930de07dab9 100644
--- a/Content.Shared/Friction/TileFrictionController.cs
+++ b/Content.Shared/Friction/TileFrictionController.cs
@@ -214,7 +214,7 @@ public void SetModifier(EntityUid entityUid, float value, TileFrictionModifierCo
                 return;
 
             friction.Modifier = value;
-            Dirty(friction);
+            Dirty(entityUid, friction);
         }
     }
 }
diff --git a/Content.Server/GameTicking/Components/ActiveGameRuleComponent.cs b/Content.Shared/GameTicking/Components/ActiveGameRuleComponent.cs
similarity index 67%
rename from Content.Server/GameTicking/Components/ActiveGameRuleComponent.cs
rename to Content.Shared/GameTicking/Components/ActiveGameRuleComponent.cs
index b9e6fa5d4b8..51bdd1c0371 100644
--- a/Content.Server/GameTicking/Components/ActiveGameRuleComponent.cs
+++ b/Content.Shared/GameTicking/Components/ActiveGameRuleComponent.cs
@@ -1,10 +1,8 @@
-namespace Content.Server.GameTicking.Components;
+namespace Content.Shared.GameTicking.Components;
 
 /// <summary>
 ///     Added to game rules before <see cref="GameRuleStartedEvent"/> and removed before <see cref="GameRuleEndedEvent"/>.
 ///     Mutually exclusive with <seealso cref="EndedGameRuleComponent"/>.
 /// </summary>
 [RegisterComponent]
-public sealed partial class ActiveGameRuleComponent : Component
-{
-}
+public sealed partial class ActiveGameRuleComponent : Component;
diff --git a/Content.Server/GameTicking/Components/DelayedStartRuleComponent.cs b/Content.Shared/GameTicking/Components/DelayedStartRuleComponent.cs
similarity index 91%
rename from Content.Server/GameTicking/Components/DelayedStartRuleComponent.cs
rename to Content.Shared/GameTicking/Components/DelayedStartRuleComponent.cs
index de4be83627d..9275da29b01 100644
--- a/Content.Server/GameTicking/Components/DelayedStartRuleComponent.cs
+++ b/Content.Shared/GameTicking/Components/DelayedStartRuleComponent.cs
@@ -1,6 +1,6 @@
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
-namespace Content.Server.GameTicking.Components;
+namespace Content.Shared.GameTicking.Components;
 
 /// <summary>
 /// Generic component used to track a gamerule that's start has been delayed.
diff --git a/Content.Server/GameTicking/Components/EndedGameRuleComponent.cs b/Content.Shared/GameTicking/Components/EndedGameRuleComponent.cs
similarity index 61%
rename from Content.Server/GameTicking/Components/EndedGameRuleComponent.cs
rename to Content.Shared/GameTicking/Components/EndedGameRuleComponent.cs
index 3234bfff3a0..5e209ed78a2 100644
--- a/Content.Server/GameTicking/Components/EndedGameRuleComponent.cs
+++ b/Content.Shared/GameTicking/Components/EndedGameRuleComponent.cs
@@ -1,10 +1,8 @@
-namespace Content.Server.GameTicking.Components;
+namespace Content.Shared.GameTicking.Components;
 
 /// <summary>
 ///     Added to game rules before <see cref="GameRuleEndedEvent"/>.
 ///     Mutually exclusive with <seealso cref="ActiveGameRuleComponent"/>.
 /// </summary>
 [RegisterComponent]
-public sealed partial class EndedGameRuleComponent : Component
-{
-}
+public sealed partial class EndedGameRuleComponent : Component;
diff --git a/Content.Server/GameTicking/Components/GameRuleComponent.cs b/Content.Shared/GameTicking/Components/GameRuleComponent.cs
similarity index 92%
rename from Content.Server/GameTicking/Components/GameRuleComponent.cs
rename to Content.Shared/GameTicking/Components/GameRuleComponent.cs
index 1e6c3f0ab1d..28ea435f1b7 100644
--- a/Content.Server/GameTicking/Components/GameRuleComponent.cs
+++ b/Content.Shared/GameTicking/Components/GameRuleComponent.cs
@@ -1,7 +1,8 @@
-using Content.Server.Destructible.Thresholds;
+using Content.Shared.Destructible.Thresholds;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
-namespace Content.Server.GameTicking.Components;
+namespace Content.Shared.GameTicking.Components;
 
 /// <summary>
 /// Component attached to all gamerule entities.
diff --git a/Content.Shared/Gravity/SharedFloatingVisualizerSystem.cs b/Content.Shared/Gravity/SharedFloatingVisualizerSystem.cs
index 8fe9e00e7eb..9d8aa4f146e 100644
--- a/Content.Shared/Gravity/SharedFloatingVisualizerSystem.cs
+++ b/Content.Shared/Gravity/SharedFloatingVisualizerSystem.cs
@@ -35,7 +35,7 @@ protected bool CanFloat(EntityUid uid, FloatingVisualsComponent component, Trans
             return false;
 
         component.CanFloat = GravitySystem.IsWeightless(uid, xform: transform);
-        Dirty(component);
+        Dirty(uid, component);
         return component.CanFloat;
     }
 
diff --git a/Content.Shared/Gravity/SharedGravitySystem.Shake.cs b/Content.Shared/Gravity/SharedGravitySystem.Shake.cs
index ad2e0e3ad57..41cf616cc4b 100644
--- a/Content.Shared/Gravity/SharedGravitySystem.Shake.cs
+++ b/Content.Shared/Gravity/SharedGravitySystem.Shake.cs
@@ -24,7 +24,7 @@ private void UpdateShake()
                 ShakeGrid(uid, gravity);
                 comp.ShakeTimes--;
                 comp.NextShake += TimeSpan.FromSeconds(ShakeCooldown);
-                Dirty(comp);
+                Dirty(uid, comp);
             }
         }
     }
@@ -44,7 +44,7 @@ public void StartGridShake(EntityUid uid, GravityComponent? gravity = null)
         }
 
         shake.ShakeTimes = 10;
-        Dirty(shake);
+        Dirty(uid, shake);
     }
 
     protected virtual void ShakeGrid(EntityUid uid, GravityComponent? comp = null) {}
diff --git a/Content.Shared/Gravity/SharedGravitySystem.cs b/Content.Shared/Gravity/SharedGravitySystem.cs
index 55187bf14ac..59d75e453af 100644
--- a/Content.Shared/Gravity/SharedGravitySystem.cs
+++ b/Content.Shared/Gravity/SharedGravitySystem.cs
@@ -18,6 +18,9 @@ public abstract partial class SharedGravitySystem : EntitySystem
         [Dependency] private readonly AlertsSystem _alerts = default!;
         [Dependency] private readonly InventorySystem _inventory = default!;
 
+        [ValidatePrototypeId<AlertPrototype>]
+        public const string WeightlessAlert = "Weightless";
+
         public bool IsWeightless(EntityUid uid, PhysicsComponent? body = null, TransformComponent? xform = null)
         {
             Resolve(uid, ref body, false);
@@ -97,11 +100,11 @@ private void OnGravityChange(ref GravityChangedEvent ev)
 
                 if (!ev.HasGravity)
                 {
-                    _alerts.ShowAlert(uid, AlertType.Weightless);
+                    _alerts.ShowAlert(uid, WeightlessAlert);
                 }
                 else
                 {
-                    _alerts.ClearAlert(uid, AlertType.Weightless);
+                    _alerts.ClearAlert(uid, WeightlessAlert);
                 }
             }
         }
@@ -110,11 +113,11 @@ private void OnAlertsSync(AlertSyncEvent ev)
         {
             if (IsWeightless(ev.Euid))
             {
-                _alerts.ShowAlert(ev.Euid, AlertType.Weightless);
+                _alerts.ShowAlert(ev.Euid, WeightlessAlert);
             }
             else
             {
-                _alerts.ClearAlert(ev.Euid, AlertType.Weightless);
+                _alerts.ClearAlert(ev.Euid, WeightlessAlert);
             }
         }
 
@@ -122,11 +125,11 @@ private void OnAlertsParentChange(EntityUid uid, AlertsComponent component, ref
         {
             if (IsWeightless(uid))
             {
-                _alerts.ShowAlert(uid, AlertType.Weightless);
+                _alerts.ShowAlert(uid, WeightlessAlert);
             }
             else
             {
-                _alerts.ClearAlert(uid, AlertType.Weightless);
+                _alerts.ClearAlert(uid, WeightlessAlert);
             }
         }
 
diff --git a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Drop.cs b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Drop.cs
index 7b169b5d0a6..4d7a0f377f5 100644
--- a/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Drop.cs
+++ b/Content.Shared/Hands/EntitySystems/SharedHandsSystem.Drop.cs
@@ -128,7 +128,7 @@ public bool TryDrop(EntityUid uid, Hand hand, EntityCoordinates? targetDropLocat
             // TODO recursively check upwards for containers
 
             if (!isInContainer
-                || !ContainerSystem.TryGetContainingContainer(userXform.ParentUid, uid, out var container, skipExistCheck: true)
+                || !ContainerSystem.TryGetContainingContainer(userXform.ParentUid, uid, out var container)
                 || !ContainerSystem.Insert((entity, itemXform), container))
                 TransformSystem.AttachToGridOrMap(entity, itemXform);
             return true;
@@ -136,7 +136,7 @@ public bool TryDrop(EntityUid uid, Hand hand, EntityCoordinates? targetDropLocat
 
         var (itemPos, itemRot) = TransformSystem.GetWorldPositionRotation(entity);
         var origin = new MapCoordinates(itemPos, itemXform.MapID);
-        var target = targetDropLocation.Value.ToMap(EntityManager, TransformSystem);
+        var target = TransformSystem.ToMapCoordinates(targetDropLocation.Value);
         TransformSystem.SetWorldPositionRotation(entity, GetFinalDropCoordinates(uid, origin, target), itemRot);
         return true;
     }
diff --git a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs
index f33c65b5915..bf3addea99e 100644
--- a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs
+++ b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs
@@ -157,7 +157,7 @@ public void SetLayersVisibility(EntityUid uid, IEnumerable<HumanoidVisualLayers>
             SetLayerVisibility(uid, humanoid, layer, visible, permanent, ref dirty);
 
         if (dirty)
-            Dirty(humanoid);
+            Dirty(uid, humanoid);
     }
 
     protected virtual void SetLayerVisibility(
@@ -203,7 +203,7 @@ public void SetSpecies(EntityUid uid, string species, bool sync = true, Humanoid
         humanoid.MarkingSet = new(oldMarkings, prototype.MarkingPoints, _markingManager, _proto);
 
         if (sync)
-            Dirty(humanoid);
+            Dirty(uid, humanoid);
     }
 
     /// <summary>
@@ -229,7 +229,7 @@ public virtual void SetSkinColor(EntityUid uid, Color skinColor, bool sync = tru
         humanoid.SkinColor = skinColor;
 
         if (sync)
-            Dirty(humanoid);
+            Dirty(uid, humanoid);
     }
 
     /// <summary>
@@ -253,7 +253,7 @@ public void SetBaseLayerId(EntityUid uid, HumanoidVisualLayers layer, string? id
             humanoid.CustomBaseLayers[layer] = new(id);
 
         if (sync)
-            Dirty(humanoid);
+            Dirty(uid, humanoid);
     }
 
     /// <summary>
@@ -274,7 +274,7 @@ public void SetBaseLayerColor(EntityUid uid, HumanoidVisualLayers layer, Color?
             humanoid.CustomBaseLayers[layer] = new(null, color);
 
         if (sync)
-            Dirty(humanoid);
+            Dirty(uid, humanoid);
     }
 
     /// <summary>
@@ -295,7 +295,7 @@ public void SetSex(EntityUid uid, Sex sex, bool sync = true, HumanoidAppearanceC
         RaiseLocalEvent(uid, new SexChangedEvent(oldSex, sex));
 
         if (sync)
-            Dirty(humanoid);
+            Dirty(uid, humanoid);
     }
 
     /// <summary>
@@ -314,7 +314,7 @@ public void SetHeight(EntityUid uid, float height, bool sync = true, HumanoidApp
         humanoid.Height = Math.Clamp(height, species.MinHeight, species.MaxHeight);
 
         if (sync)
-            Dirty(humanoid);
+            Dirty(uid, humanoid);
     }
 
     /// <summary>
@@ -333,7 +333,7 @@ public void SetWidth(EntityUid uid, float width, bool sync = true, HumanoidAppea
         humanoid.Width = Math.Clamp(width, species.MinWidth, species.MaxWidth);
 
         if (sync)
-            Dirty(humanoid);
+            Dirty(uid, humanoid);
     }
 
     /// <summary>
@@ -353,7 +353,7 @@ public void SetScale(EntityUid uid, Vector2 scale, bool sync = true, HumanoidApp
         humanoid.Width = Math.Clamp(scale.X, species.MinWidth, species.MaxWidth);
 
         if (sync)
-            Dirty(humanoid);
+            Dirty(uid, humanoid);
     }
 
     /// <summary>
@@ -440,7 +440,7 @@ public virtual void LoadProfile(EntityUid uid, HumanoidCharacterProfile profile,
 
         humanoid.LastProfileLoaded = profile; // DeltaV - let paradox anomaly be cloned
 
-        Dirty(humanoid);
+        Dirty(uid, humanoid);
         RaiseLocalEvent(uid, new ProfileLoadFinishedEvent());
     }
 
@@ -472,7 +472,7 @@ public void AddMarking(EntityUid uid, string marking, Color? color = null, bool
         humanoid.MarkingSet.AddBack(prototype.MarkingCategory, markingObject);
 
         if (sync)
-            Dirty(humanoid);
+            Dirty(uid, humanoid);
     }
 
     private void EnsureDefaultMarkings(EntityUid uid, HumanoidAppearanceComponent? humanoid)
@@ -502,7 +502,7 @@ public void AddMarking(EntityUid uid, string marking, IReadOnlyList<Color> color
         humanoid.MarkingSet.AddBack(prototype.MarkingCategory, markingObject);
 
         if (sync)
-            Dirty(humanoid);
+            Dirty(uid, humanoid);
     }
 
     /// <summary>
diff --git a/Content.Shared/Implants/SharedImplanterSystem.cs b/Content.Shared/Implants/SharedImplanterSystem.cs
index 36a31bac1d2..d78522b56cc 100644
--- a/Content.Shared/Implants/SharedImplanterSystem.cs
+++ b/Content.Shared/Implants/SharedImplanterSystem.cs
@@ -77,7 +77,7 @@ public void Implant(EntityUid user, EntityUid target, EntityUid implanter, Impla
         var ev = new TransferDnaEvent { Donor = target, Recipient = implanter };
         RaiseLocalEvent(target, ref ev);
 
-        Dirty(component);
+        Dirty(implanter, component);
     }
 
     public bool CanImplant(
@@ -156,7 +156,7 @@ public void Draw(EntityUid implanter, EntityUid user, EntityUid target, Implante
             if (component.CurrentMode == ImplanterToggleMode.Draw && !component.ImplantOnly && !permanentFound)
                 ImplantMode(implanter, component);
 
-            Dirty(component);
+            Dirty(implanter, component);
         }
     }
 
diff --git a/Content.Shared/Info/RulesMessages.cs b/Content.Shared/Info/RulesMessages.cs
new file mode 100644
index 00000000000..9cb73c9aa86
--- /dev/null
+++ b/Content.Shared/Info/RulesMessages.cs
@@ -0,0 +1,25 @@
+using Lidgren.Network;
+using Robust.Shared.Network;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Info;
+
+/// <summary>
+///  Sent by the server to show the rules to the client instantly.
+/// </summary>
+public sealed class ShowRulesPopupMessage : NetMessage
+{
+    public override MsgGroups MsgGroup => MsgGroups.Command;
+
+    public float PopupTime { get; set; }
+
+    public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
+    {
+        PopupTime = buffer.ReadFloat();
+    }
+
+    public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
+    {
+        buffer.Write(PopupTime);
+    }
+}
diff --git a/Content.Shared/Info/SharedInfo.cs b/Content.Shared/Info/SharedInfo.cs
deleted file mode 100644
index 4a0e688cf9a..00000000000
--- a/Content.Shared/Info/SharedInfo.cs
+++ /dev/null
@@ -1,28 +0,0 @@
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Info
-{
-    /// <summary>
-    ///     A client request for server rules.
-    /// </summary>
-    [Serializable, NetSerializable]
-    public sealed class RequestRulesMessage : EntityEventArgs
-    {
-    }
-
-    /// <summary>
-    ///     A server response with server rules.
-    /// </summary>
-    [Serializable, NetSerializable]
-    public sealed class RulesMessage : EntityEventArgs
-    {
-        public string Title;
-        public string Text;
-
-        public RulesMessage(string title, string rules)
-        {
-            Title = title;
-            Text = rules;
-        }
-    }
-}
diff --git a/Content.Shared/Info/SharedRulesManager.cs b/Content.Shared/Info/SharedRulesManager.cs
deleted file mode 100644
index 932150d58ef..00000000000
--- a/Content.Shared/Info/SharedRulesManager.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-using Lidgren.Network;
-using Robust.Shared.Network;
-using Robust.Shared.Serialization;
-
-namespace Content.Shared.Info;
-
-public abstract class SharedRulesManager
-{
-    /// <summary>
-    ///     Sent by the server to show the rules to the client instantly.
-    /// </summary>
-    public sealed class ShowRulesPopupMessage : NetMessage
-    {
-        public override MsgGroups MsgGroup => MsgGroups.Command;
-
-        public float PopupTime { get; set; }
-
-        public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
-        {
-            PopupTime = buffer.ReadFloat();
-        }
-
-        public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
-        {
-            buffer.Write(PopupTime);
-        }
-    }
-
-    /// <summary>
-    ///     Sent by the server when the client needs to display the rules on join.
-    /// </summary>
-    public sealed class ShouldShowRulesPopupMessage : NetMessage
-    {
-        public override MsgGroups MsgGroup => MsgGroups.Command;
-
-        public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
-        {
-        }
-
-        public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
-        {
-        }
-    }
-
-    /// <summary>
-    ///     Sent by the client when it has accepted the rules.
-    /// </summary>
-    public sealed class RulesAcceptedMessage : NetMessage
-    {
-        public override MsgGroups MsgGroup => MsgGroups.Command;
-
-        public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
-        {
-        }
-
-        public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
-        {
-        }
-    }
-}
diff --git a/Content.Shared/Instruments/SharedInstrumentSystem.cs b/Content.Shared/Instruments/SharedInstrumentSystem.cs
index 87e3a69489c..23bcf67de0e 100644
--- a/Content.Shared/Instruments/SharedInstrumentSystem.cs
+++ b/Content.Shared/Instruments/SharedInstrumentSystem.cs
@@ -12,10 +12,10 @@ public virtual void EndRenderer(EntityUid uid, bool fromStateChange, SharedInstr
     {
     }
 
-    public void SetInstrumentProgram(SharedInstrumentComponent component, byte program, byte bank)
+    public void SetInstrumentProgram(EntityUid uid, SharedInstrumentComponent component, byte program, byte bank)
     {
         component.InstrumentBank = bank;
         component.InstrumentProgram = program;
-        Dirty(component);
+        Dirty(uid, component);
     }
 }
diff --git a/Content.Shared/Interaction/RotateToFaceSystem.cs b/Content.Shared/Interaction/RotateToFaceSystem.cs
index ed950240af6..fa213011ef1 100644
--- a/Content.Shared/Interaction/RotateToFaceSystem.cs
+++ b/Content.Shared/Interaction/RotateToFaceSystem.cs
@@ -1,7 +1,6 @@
 using System.Numerics;
 using Content.Shared.ActionBlocker;
 using Content.Shared.Buckle.Components;
-using Content.Shared.Mobs.Systems;
 using Content.Shared.Rotatable;
 using JetBrains.Annotations;
 
@@ -83,24 +82,21 @@ public bool TryFaceAngle(EntityUid user, Angle diffAngle, TransformComponent? xf
             if (!_actionBlockerSystem.CanChangeDirection(user))
                 return false;
 
-            if (EntityManager.TryGetComponent(user, out BuckleComponent? buckle) && buckle.Buckled)
+            if (TryComp(user, out BuckleComponent? buckle) && buckle.BuckledTo is {} strap)
             {
-                var suid = buckle.LastEntityBuckledTo;
-                if (suid != null)
-                {
-                    // We're buckled to another object. Is that object rotatable?
-                    if (TryComp<RotatableComponent>(suid.Value, out var rotatable) && rotatable.RotateWhileAnchored)
-                    {
-                        // Note the assumption that even if unanchored, user can only do spinnychair with an "independent wheel".
-                        // (Since the user being buckled to it holds it down with their weight.)
-                        // This is logically equivalent to RotateWhileAnchored.
-                        // Barstools and office chairs have independent wheels, while regular chairs don't.
-                        _transform.SetWorldRotation(Transform(suid.Value), diffAngle);
-                        return true;
-                    }
-                }
-
-                return false;
+                // What if a person is strapped to a borg?
+                // I'm pretty sure this would allow them to be partially ratatouille'd
+
+                // We're buckled to another object. Is that object rotatable?
+                if (!TryComp<RotatableComponent>(strap, out var rotatable) || !rotatable.RotateWhileAnchored)
+                    return false;
+
+                // Note the assumption that even if unanchored, user can only do spinnychair with an "independent wheel".
+                // (Since the user being buckled to it holds it down with their weight.)
+                // This is logically equivalent to RotateWhileAnchored.
+                // Barstools and office chairs have independent wheels, while regular chairs don't.
+                _transform.SetWorldRotation(Transform(strap), diffAngle);
+                return true;
             }
 
             // user is not buckled in; apply to their transform
diff --git a/Content.Shared/Interaction/SharedInteractionSystem.cs b/Content.Shared/Interaction/SharedInteractionSystem.cs
index 4c22bcb14e4..1c4a697cc4e 100644
--- a/Content.Shared/Interaction/SharedInteractionSystem.cs
+++ b/Content.Shared/Interaction/SharedInteractionSystem.cs
@@ -1167,7 +1167,7 @@ public bool CanAccessViaStorage(EntityUid user, EntityUid target, BaseContainer
                 return false;
 
             // we don't check if the user can access the storage entity itself. This should be handed by the UI system.
-            return _ui.IsUiOpen(target, StorageComponent.StorageUiKey.Key, user);
+            return _ui.IsUiOpen(container.Owner, StorageComponent.StorageUiKey.Key, user);
         }
 
         /// <summary>
diff --git a/Content.Shared/Item/ItemToggle/SharedItemToggleSystem.cs b/Content.Shared/Item/ItemToggle/SharedItemToggleSystem.cs
index 523f67bac3d..fa23339223e 100644
--- a/Content.Shared/Item/ItemToggle/SharedItemToggleSystem.cs
+++ b/Content.Shared/Item/ItemToggle/SharedItemToggleSystem.cs
@@ -245,9 +245,22 @@ private void UpdateActiveSound(EntityUid uid, ItemToggleActiveSoundComponent act
             if (activeSound.ActiveSound != null && activeSound.PlayingStream == null)
             {
                 if (args.Predicted)
-                    activeSound.PlayingStream = _audio.PlayPredicted(activeSound.ActiveSound, uid, args.User, AudioParams.Default.WithLoop(true)).Value.Entity;
-                else
-                    activeSound.PlayingStream = _audio.PlayPvs(activeSound.ActiveSound, uid, AudioParams.Default.WithLoop(true)).Value.Entity;
+                {
+                    var playingStream = _audio.PlayPredicted(activeSound.ActiveSound, uid, args.User, AudioParams.Default.WithLoop(true));
+
+                    if (playingStream == null)
+                        return;
+
+                    activeSound.PlayingStream = playingStream!.Value.Entity;
+                } else
+                {
+                    var playingStream = _audio.PlayPvs(activeSound.ActiveSound, uid, AudioParams.Default.WithLoop(true));
+
+                    if (playingStream == null)
+                        return;
+
+                    activeSound.PlayingStream = playingStream!.Value.Entity;
+                }
             }
         }
         else
diff --git a/Content.Shared/Light/SharedHandheldLightSystem.cs b/Content.Shared/Light/SharedHandheldLightSystem.cs
index 2fa15800a31..9bec37a3140 100644
--- a/Content.Shared/Light/SharedHandheldLightSystem.cs
+++ b/Content.Shared/Light/SharedHandheldLightSystem.cs
@@ -29,7 +29,7 @@ private void OnInit(EntityUid uid, HandheldLightComponent component, ComponentIn
         UpdateVisuals(uid, component);
 
         // Want to make sure client has latest data on level so battery displays properly.
-        Dirty(component);
+        Dirty(uid, component);
     }
 
     private void OnHandleState(EntityUid uid, HandheldLightComponent component, ref ComponentHandleState args)
diff --git a/Content.Shared/Light/SharedRgbLightControllerSystem.cs b/Content.Shared/Light/SharedRgbLightControllerSystem.cs
index 1bba91c5e7b..7d4928f5bc1 100644
--- a/Content.Shared/Light/SharedRgbLightControllerSystem.cs
+++ b/Content.Shared/Light/SharedRgbLightControllerSystem.cs
@@ -17,13 +17,13 @@ private void OnGetState(EntityUid uid, RgbLightControllerComponent component, re
         args.State = new RgbLightControllerState(component.CycleRate, component.Layers);
     }
 
-    public void SetLayers(EntityUid uid, List<int>? layers,  RgbLightControllerComponent? rgb = null)
+    public void SetLayers(EntityUid uid, List<int>? layers, RgbLightControllerComponent? rgb = null)
     {
         if (!Resolve(uid, ref rgb))
             return;
 
         rgb.Layers = layers;
-        Dirty(rgb);
+        Dirty(uid, rgb);
     }
 
     public void SetCycleRate(EntityUid uid, float rate, RgbLightControllerComponent? rgb = null)
@@ -32,6 +32,6 @@ public void SetCycleRate(EntityUid uid, float rate, RgbLightControllerComponent?
             return;
 
         rgb.CycleRate = Math.Clamp(0.01f, rate, 1); // lets not give people seizures
-        Dirty(rgb);
+        Dirty(uid, rgb);
     }
 }
diff --git a/Content.Shared/Mech/EntitySystems/SharedMechSystem.cs b/Content.Shared/Mech/EntitySystems/SharedMechSystem.cs
index 7e44dea5078..b4f1ae9a268 100644
--- a/Content.Shared/Mech/EntitySystems/SharedMechSystem.cs
+++ b/Content.Shared/Mech/EntitySystems/SharedMechSystem.cs
@@ -194,7 +194,7 @@ public void CycleEquipment(EntityUid uid, MechComponent? component = null)
         if (_net.IsServer)
             _popup.PopupEntity(popupString, uid);
 
-        Dirty(component);
+        Dirty(uid, component);
     }
 
     /// <summary>
@@ -278,7 +278,7 @@ public virtual bool TryChangeEnergy(EntityUid uid, FixedPoint2 delta, MechCompon
             return false;
 
         component.Energy = FixedPoint2.Clamp(component.Energy + delta, 0, component.MaxEnergy);
-        Dirty(component);
+        Dirty(uid, component);
         UpdateUserInterface(uid, component);
         return true;
     }
@@ -306,7 +306,7 @@ public void SetIntegrity(EntityUid uid, FixedPoint2 value, MechComponent? compon
             UpdateAppearance(uid, component);
         }
 
-        Dirty(component);
+        Dirty(uid, component);
         UpdateUserInterface(uid, component);
     }
 
diff --git a/Content.Shared/Medical/CPR/Systems/CPRSystem.cs b/Content.Shared/Medical/CPR/Systems/CPRSystem.cs
index 799c0664a66..e050f1b4e1d 100644
--- a/Content.Shared/Medical/CPR/Systems/CPRSystem.cs
+++ b/Content.Shared/Medical/CPR/Systems/CPRSystem.cs
@@ -84,7 +84,13 @@ private void StartCPR(EntityUid performer, EntityUid target, CPRTrainingComponen
             {
                 _popupSystem.PopupEntity(Loc.GetString("cpr-start-second-person", ("target", target)), target, performer, PopupType.Medium);
                 _popupSystem.PopupEntity(Loc.GetString("cpr-start-second-person-patient", ("user", performer)), target, target, PopupType.Medium);
-                cprComponent.CPRPlayingStream = _audio.PlayPvs(cprComponent.CPRSound, performer).Value.Entity;
+
+                var playingStream = _audio.PlayPvs(cprComponent.CPRSound, performer);
+
+                if (playingStream == null)
+                    return;
+
+                cprComponent.CPRPlayingStream = _audio.PlayPvs(cprComponent.CPRSound, performer)!.Value.Entity;
             }
 
             _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, performer, cprComponent.DoAfterDuration, new CPRDoAfterEvent(), performer, target, performer)
diff --git a/Content.Shared/Mobs/Components/MobThresholdsComponent.cs b/Content.Shared/Mobs/Components/MobThresholdsComponent.cs
index e97d3672a21..0e37cf9b10e 100644
--- a/Content.Shared/Mobs/Components/MobThresholdsComponent.cs
+++ b/Content.Shared/Mobs/Components/MobThresholdsComponent.cs
@@ -2,6 +2,7 @@
 using Content.Shared.FixedPoint;
 using Content.Shared.Mobs.Systems;
 using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
 
 namespace Content.Shared.Mobs.Components;
@@ -24,13 +25,16 @@ public sealed partial class MobThresholdsComponent : Component
     /// Used for alternate health alerts (silicons, for example)
     /// </summary>
     [DataField("stateAlertDict")]
-    public Dictionary<MobState, AlertType> StateAlertDict = new()
+    public Dictionary<MobState, ProtoId<AlertPrototype>> StateAlertDict = new()
     {
-        {MobState.Alive, AlertType.HumanHealth},
-        {MobState.Critical, AlertType.HumanCrit},
-        {MobState.Dead, AlertType.HumanDead},
+        {MobState.Alive, "HumanHealth"},
+        {MobState.Critical, "HumanCrit"},
+        {MobState.Dead, "HumanDead"},
     };
 
+    [DataField]
+    public ProtoId<AlertCategoryPrototype> HealthAlertCategory = "Health";
+
     /// <summary>
     /// Whether or not this entity should display damage overlays (robots don't feel pain, black out etc.)
     /// </summary>
@@ -53,19 +57,19 @@ public sealed class MobThresholdsComponentState : ComponentState
 
     public MobState CurrentThresholdState;
 
-    public Dictionary<MobState, AlertType> StateAlertDict = new()
-    {
-        {MobState.Alive, AlertType.HumanHealth},
-        {MobState.Critical, AlertType.HumanCrit},
-        {MobState.Dead, AlertType.HumanDead},
-    };
+    public Dictionary<MobState, ProtoId<AlertPrototype>> StateAlertDict;
 
     public bool ShowOverlays;
 
     public bool AllowRevives;
 
-    public MobThresholdsComponentState(Dictionary<FixedPoint2, MobState> unsortedThresholds, bool triggersAlerts, MobState currentThresholdState,
-        Dictionary<MobState, AlertType> stateAlertDict, bool showOverlays, bool allowRevives)
+    public MobThresholdsComponentState(Dictionary<FixedPoint2, MobState> unsortedThresholds,
+        bool triggersAlerts,
+        MobState currentThresholdState,
+        Dictionary<MobState,
+        ProtoId<AlertPrototype>> stateAlertDict,
+        bool showOverlays,
+        bool allowRevives)
     {
         UnsortedThresholds = unsortedThresholds;
         TriggersAlerts = triggersAlerts;
diff --git a/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs b/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs
index d9ef671afe2..2088bd4161e 100644
--- a/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs
+++ b/Content.Shared/Mobs/Systems/MobStateSystem.Subscribers.cs
@@ -1,4 +1,5 @@
 using Content.Shared.Bed.Sleep;
+using Content.Shared.Buckle.Components;
 using Content.Shared.CombatMode.Pacification;
 using Content.Shared.Damage.ForceSay;
 using Content.Shared.Emoting;
@@ -10,15 +11,12 @@
 using Content.Shared.Mobs.Components;
 using Content.Shared.Movement.Events;
 using Content.Shared.Pointing;
-using Content.Shared.Projectiles;
 using Content.Shared.Pulling.Events;
 using Content.Shared.Speech;
 using Content.Shared.Standing;
 using Content.Shared.Strip.Components;
 using Content.Shared.Throwing;
-using Content.Shared.Weapons.Ranged.Components;
 using Robust.Shared.Physics.Components;
-using Robust.Shared.Physics.Events;
 
 namespace Content.Shared.Mobs.Systems;
 
@@ -46,6 +44,16 @@ private void SubscribeEvents()
         SubscribeLocalEvent<MobStateComponent, TryingToSleepEvent>(OnSleepAttempt);
         SubscribeLocalEvent<MobStateComponent, CombatModeShouldHandInteractEvent>(OnCombatModeShouldHandInteract);
         SubscribeLocalEvent<MobStateComponent, AttemptPacifiedAttackEvent>(OnAttemptPacifiedAttack);
+
+        SubscribeLocalEvent<MobStateComponent, UnbuckleAttemptEvent>(OnUnbuckleAttempt);
+    }
+
+    private void OnUnbuckleAttempt(Entity<MobStateComponent> ent, ref UnbuckleAttemptEvent args)
+    {
+        // TODO is this necessary?
+        // Shouldn't the interaction have already been blocked by a general interaction check?
+        if (args.User == ent.Owner && IsIncapacitated(ent))
+            args.Cancelled = true;
     }
 
     private void OnStateExitSubscribers(EntityUid target, MobStateComponent component, MobState state)
@@ -90,12 +98,12 @@ private void OnStateEnteredSubscribers(EntityUid target, MobStateComponent compo
                 _appearance.SetData(target, MobStateVisuals.State, MobState.Alive);
                 break;
             case MobState.Critical:
-                _standing.Down(target, setDrawDepth: true);
+                _standing.Down(target);
                 _appearance.SetData(target, MobStateVisuals.State, MobState.Critical);
                 break;
             case MobState.Dead:
                 EnsureComp<CollisionWakeComponent>(target);
-                _standing.Down(target, setDrawDepth: true);
+                _standing.Down(target);
 
                 if (_standing.IsDown(target) && TryComp<PhysicsComponent>(target, out var physics))
                 {
diff --git a/Content.Shared/Mobs/Systems/MobThresholdSystem.cs b/Content.Shared/Mobs/Systems/MobThresholdSystem.cs
index 59d9fb4c239..b11de9eac56 100644
--- a/Content.Shared/Mobs/Systems/MobThresholdSystem.cs
+++ b/Content.Shared/Mobs/Systems/MobThresholdSystem.cs
@@ -431,7 +431,7 @@ private void MobThresholdStartup(EntityUid target, MobThresholdsComponent thresh
     private void MobThresholdShutdown(EntityUid target, MobThresholdsComponent component, ComponentShutdown args)
     {
         if (component.TriggersAlerts)
-            _alerts.ClearAlertCategory(target, AlertCategory.Health);
+            _alerts.ClearAlertCategory(target, component.HealthAlertCategory);
     }
 
     private void OnUpdateMobState(EntityUid target, MobThresholdsComponent component, ref UpdateMobStateEvent args)
diff --git a/Content.Shared/Movement/Pulling/Components/PullableComponent.cs b/Content.Shared/Movement/Pulling/Components/PullableComponent.cs
index 01ce0efaae6..9d342fec3cf 100644
--- a/Content.Shared/Movement/Pulling/Components/PullableComponent.cs
+++ b/Content.Shared/Movement/Pulling/Components/PullableComponent.cs
@@ -1,4 +1,6 @@
+using Content.Shared.Alert;
 using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
 
 namespace Content.Shared.Movement.Pulling.Components;
 
@@ -43,4 +45,6 @@ public sealed partial class PullableComponent : Component
     /// </summary>
     [DataField, AutoNetworkedField]
     public bool BeingActivelyPushed = false;
+    [DataField]
+    public ProtoId<AlertPrototype> PulledAlert = "Pulled";
 }
diff --git a/Content.Shared/Movement/Pulling/Components/PullerComponent.cs b/Content.Shared/Movement/Pulling/Components/PullerComponent.cs
index 648f06086ba..80a12be690a 100644
--- a/Content.Shared/Movement/Pulling/Components/PullerComponent.cs
+++ b/Content.Shared/Movement/Pulling/Components/PullerComponent.cs
@@ -1,6 +1,8 @@
-using Content.Shared.Movement.Pulling.Systems;
+using Content.Shared.Alert;
+using Content.Shared.Movement.Pulling.Systems;
 using Robust.Shared.GameStates;
 using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
 namespace Content.Shared.Movement.Pulling.Components;
@@ -64,4 +66,6 @@ public sealed partial class PullerComponent : Component
     /// </summary>
     [DataField]
     public float MaxPushRange = 2f;
+    [DataField]
+    public ProtoId<AlertPrototype> PullingAlert = "Pulling";
 }
diff --git a/Content.Shared/Movement/Pulling/Events/PullStartedMessage.cs b/Content.Shared/Movement/Pulling/Events/PullStartedMessage.cs
index 29460e1dfc1..c0775b4ce2d 100644
--- a/Content.Shared/Movement/Pulling/Events/PullStartedMessage.cs
+++ b/Content.Shared/Movement/Pulling/Events/PullStartedMessage.cs
@@ -1,9 +1,6 @@
 namespace Content.Shared.Movement.Pulling.Events;
 
-public sealed class PullStartedMessage : PullMessage
-{
-    public PullStartedMessage(EntityUid pullerUid, EntityUid pullableUid) :
-        base(pullerUid, pullableUid)
-    {
-    }
-}
+/// <summary>
+/// Event raised directed BOTH at the puller and pulled entity when a pull starts.
+/// </summary>
+public sealed class PullStartedMessage(EntityUid pullerUid, EntityUid pullableUid) : PullMessage(pullerUid, pullableUid);
diff --git a/Content.Shared/Movement/Pulling/Events/PullStoppedMessage.cs b/Content.Shared/Movement/Pulling/Events/PullStoppedMessage.cs
index 47aa34562fb..6df4d174839 100644
--- a/Content.Shared/Movement/Pulling/Events/PullStoppedMessage.cs
+++ b/Content.Shared/Movement/Pulling/Events/PullStoppedMessage.cs
@@ -1,13 +1,6 @@
-using Robust.Shared.Physics.Components;
-
-namespace Content.Shared.Movement.Pulling.Events;
+namespace Content.Shared.Movement.Pulling.Events;
 
 /// <summary>
-/// Raised directed on both puller and pullable.
+/// Event raised directed BOTH at the puller and pulled entity when a pull starts.
 /// </summary>
-public sealed class PullStoppedMessage : PullMessage
-{
-    public PullStoppedMessage(EntityUid pullerUid, EntityUid pulledUid) : base(pullerUid, pulledUid)
-    {
-    }
-}
+public sealed class PullStoppedMessage(EntityUid pullerUid, EntityUid pulledUid) : PullMessage(pullerUid, pulledUid);
diff --git a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs
index 4bf53c8dbdd..11a1d94b29b 100644
--- a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs
+++ b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs
@@ -1,4 +1,3 @@
-using System.Numerics;
 using Content.Shared.ActionBlocker;
 using Content.Shared.Administration.Logs;
 using Content.Shared.Alert;
@@ -17,7 +16,6 @@
 using Content.Shared.Projectiles;
 using Content.Shared.Pulling.Events;
 using Content.Shared.Standing;
-using Content.Shared.Throwing;
 using Content.Shared.Verbs;
 using Robust.Shared.Containers;
 using Robust.Shared.Input.Binding;
@@ -29,6 +27,8 @@
 using Robust.Shared.Physics.Systems;
 using Robust.Shared.Player;
 using Robust.Shared.Timing;
+using Content.Shared.Throwing;
+using System.Numerics;
 
 namespace Content.Shared.Movement.Pulling.Systems;
 
@@ -73,11 +73,25 @@ public override void Initialize()
         SubscribeLocalEvent<PullerComponent, RefreshMovementSpeedModifiersEvent>(OnRefreshMovespeed);
         SubscribeLocalEvent<PullerComponent, DropHandItemsEvent>(OnDropHandItems);
 
+        SubscribeLocalEvent<PullableComponent, StrappedEvent>(OnBuckled);
+        SubscribeLocalEvent<PullableComponent, BuckledEvent>(OnGotBuckled);
+
         CommandBinds.Builder
             .Bind(ContentKeyFunctions.MovePulledObject, new PointerInputCmdHandler(OnRequestMovePulledObject))
             .Bind(ContentKeyFunctions.ReleasePulledObject, InputCmdHandler.FromDelegate(OnReleasePulledObject, handle: false))
             .Register<PullingSystem>();
     }
+    private void OnBuckled(Entity<PullableComponent> ent, ref StrappedEvent args)
+    {
+        // Prevent people from pulling the entity they are buckled to
+        if (ent.Comp.Puller == args.Buckle.Owner && !args.Buckle.Comp.PullStrap)
+            StopPulling(ent, ent);
+    }
+
+    private void OnGotBuckled(Entity<PullableComponent> ent, ref BuckledEvent args)
+    {
+        StopPulling(ent, ent);
+    }
 
     public override void Shutdown()
     {
@@ -174,7 +188,8 @@ private void OnDropHandItems(EntityUid uid, PullerComponent pullerComp, DropHand
 
     private void OnPullerContainerInsert(Entity<PullerComponent> ent, ref EntGotInsertedIntoContainerMessage args)
     {
-        if (ent.Comp.Pulling == null) return;
+        if (ent.Comp.Pulling == null)
+            return;
 
         if (!TryComp(ent.Comp.Pulling.Value, out PullableComponent? pulling))
             return;
@@ -307,8 +322,18 @@ private void OnJointRemoved(EntityUid uid, PullableComponent component, JointRem
     /// </summary>
     private void StopPulling(EntityUid pullableUid, PullableComponent pullableComp)
     {
+        if (pullableComp.Puller == null)
+            return;
+
         if (!_timing.ApplyingState)
         {
+            // Joint shutdown
+            if (pullableComp.PullJointId != null)
+            {
+                _joints.RemoveJoint(pullableUid, pullableComp.PullJointId);
+                pullableComp.PullJointId = null;
+            }
+
             if (TryComp<PhysicsComponent>(pullableUid, out var pullablePhysics))
             {
                 _physics.SetFixedRotation(pullableUid, pullableComp.PrevFixedRotation, body: pullablePhysics);
@@ -325,7 +350,7 @@ private void StopPulling(EntityUid pullableUid, PullableComponent pullableComp)
         if (TryComp<PullerComponent>(oldPuller, out var pullerComp))
         {
             var pullerUid = oldPuller.Value;
-            _alertsSystem.ClearAlert(pullerUid, AlertType.Pulling);
+            _alertsSystem.ClearAlert(pullerUid, pullerComp.PullingAlert);
             pullerComp.Pulling = null;
             Dirty(oldPuller.Value, pullerComp);
 
@@ -339,7 +364,7 @@ private void StopPulling(EntityUid pullableUid, PullableComponent pullableComp)
         }
 
 
-        _alertsSystem.ClearAlert(pullableUid, AlertType.Pulled);
+        _alertsSystem.ClearAlert(pullableUid, pullableComp.PulledAlert);
     }
 
     public bool IsPulled(EntityUid uid, PullableComponent? component = null)
@@ -440,15 +465,6 @@ public bool CanPull(EntityUid puller, EntityUid pullableUid, PullerComponent? pu
             return false;
         }
 
-        if (EntityManager.TryGetComponent(puller, out BuckleComponent? buckle))
-        {
-            // Prevent people pulling the chair they're on, etc.
-            if (buckle is { PullStrap: false, Buckled: true } && (buckle.LastEntityBuckledTo == pullableUid))
-            {
-                return false;
-            }
-        }
-
         var getPulled = new BeingPulledAttemptEvent(puller, pullableUid);
         RaiseLocalEvent(pullableUid, getPulled, true);
         var startPull = new StartPullAttemptEvent(puller, pullableUid);
@@ -492,11 +508,8 @@ public bool TryStartPull(EntityUid pullerUid, EntityUid pullableUid,
         if (!CanPull(pullerUid, pullableUid))
             return false;
 
-        if (!EntityManager.TryGetComponent<PhysicsComponent>(pullerUid, out var pullerPhysics) ||
-            !EntityManager.TryGetComponent<PhysicsComponent>(pullableUid, out var pullablePhysics))
-        {
+        if (!HasComp<PhysicsComponent>(pullerUid) || !TryComp(pullableUid, out PhysicsComponent? pullablePhysics))
             return false;
-        }
 
         // Ensure that the puller is not currently pulling anything.
         if (TryComp<PullableComponent>(pullerComp.Pulling, out var oldPullable)
@@ -540,7 +553,7 @@ public bool TryStartPull(EntityUid pullerUid, EntityUid pullableUid,
         {
             // Joint startup
             var union = _physics.GetHardAABB(pullerUid).Union(_physics.GetHardAABB(pullableUid, body: pullablePhysics));
-            var length = Math.Max((float) union.Size.X, (float) union.Size.Y) * 0.75f;
+            var length = Math.Max(union.Size.X, union.Size.Y) * 0.75f;
 
             var joint = _joints.CreateDistanceJoint(pullableUid, pullerUid, id: pullableComp.PullJointId);
             joint.CollideConnected = false;
@@ -557,8 +570,8 @@ public bool TryStartPull(EntityUid pullerUid, EntityUid pullableUid,
 
         // Messaging
         var message = new PullStartedMessage(pullerUid, pullableUid);
-        _alertsSystem.ShowAlert(pullerUid, AlertType.Pulling);
-        _alertsSystem.ShowAlert(pullableUid, AlertType.Pulled);
+        _alertsSystem.ShowAlert(pullerUid, pullerComp.PullingAlert);
+        _alertsSystem.ShowAlert(pullableUid, pullableComp.PulledAlert);
 
         RaiseLocalEvent(pullerUid, message);
         RaiseLocalEvent(pullableUid, message);
@@ -584,17 +597,6 @@ public bool TryStopPull(EntityUid pullableUid, PullableComponent pullable, Entit
         if (msg.Cancelled)
             return false;
 
-        // Stop pulling confirmed!
-        if (!_timing.ApplyingState)
-        {
-            // Joint shutdown
-            if (pullable.PullJointId != null)
-            {
-                _joints.RemoveJoint(pullableUid, pullable.PullJointId);
-                pullable.PullJointId = null;
-            }
-        }
-
         StopPulling(pullableUid, pullable);
         return true;
     }
diff --git a/Content.Shared/Movement/Systems/SharedMoverController.cs b/Content.Shared/Movement/Systems/SharedMoverController.cs
index 00afa3a4fb8..43a63068cf2 100644
--- a/Content.Shared/Movement/Systems/SharedMoverController.cs
+++ b/Content.Shared/Movement/Systems/SharedMoverController.cs
@@ -296,7 +296,7 @@ protected void HandleMobMovement(
 
         private void WalkingAlert(EntityUid player, InputMoverComponent component)
         {
-            _alerts.ShowAlert(player, AlertType.Walking, component.Sprinting ? (short) 1 : (short) 0);
+            _alerts.ShowAlert(player, component.WalkingAlert, component.Sprinting ? (short) 1 : (short) 0);
         }
 
         public void LerpRotation(EntityUid uid, InputMoverComponent mover, float frameTime)
diff --git a/Content.Shared/Movement/Systems/SpeedModifierContactsSystem.cs b/Content.Shared/Movement/Systems/SpeedModifierContactsSystem.cs
index f9f6b82bb18..400a675cd25 100644
--- a/Content.Shared/Movement/Systems/SpeedModifierContactsSystem.cs
+++ b/Content.Shared/Movement/Systems/SpeedModifierContactsSystem.cs
@@ -58,7 +58,7 @@ public void ChangeModifiers(EntityUid uid, float walkSpeed, float sprintSpeed, S
         }
         component.WalkSpeedModifier = walkSpeed;
         component.SprintSpeedModifier = sprintSpeed;
-        Dirty(component);
+        Dirty(uid, component);
         _toUpdate.UnionWith(_physics.GetContactingEntities(uid));
     }
 
diff --git a/Content.Shared/Ninja/Components/SpaceNinjaComponent.cs b/Content.Shared/Ninja/Components/SpaceNinjaComponent.cs
index 0f3bff265cb..91c816df5c9 100644
--- a/Content.Shared/Ninja/Components/SpaceNinjaComponent.cs
+++ b/Content.Shared/Ninja/Components/SpaceNinjaComponent.cs
@@ -1,3 +1,4 @@
+using Content.Shared.Alert;
 using Content.Shared.Ninja.Systems;
 using Robust.Shared.GameStates;
 using Robust.Shared.Prototypes;
@@ -53,4 +54,7 @@ public sealed partial class SpaceNinjaComponent : Component
     /// </summary>
     [DataField]
     public EntProtoId SpiderChargeObjective = "SpiderChargeObjective";
+
+    [DataField]
+    public ProtoId<AlertPrototype> SuitPowerAlert = "SuitPower";
 }
diff --git a/Content.Shared/Nutrition/Components/HungerComponent.cs b/Content.Shared/Nutrition/Components/HungerComponent.cs
index 9ac82ba283c..79d895ddae6 100644
--- a/Content.Shared/Nutrition/Components/HungerComponent.cs
+++ b/Content.Shared/Nutrition/Components/HungerComponent.cs
@@ -2,6 +2,7 @@
 using Content.Shared.Damage;
 using Content.Shared.Nutrition.EntitySystems;
 using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Generic;
@@ -65,15 +66,18 @@ public sealed partial class HungerComponent : Component
     /// <summary>
     /// A dictionary relating hunger thresholds to corresponding alerts.
     /// </summary>
-    [DataField("hungerThresholdAlerts", customTypeSerializer: typeof(DictionarySerializer<HungerThreshold, AlertType>))]
+    [DataField("hungerThresholdAlerts")]
     [AutoNetworkedField]
-    public Dictionary<HungerThreshold, AlertType> HungerThresholdAlerts = new()
+    public Dictionary<HungerThreshold, ProtoId<AlertPrototype>> HungerThresholdAlerts = new()
     {
-        { HungerThreshold.Peckish, AlertType.Peckish },
-        { HungerThreshold.Starving, AlertType.Starving },
-        { HungerThreshold.Dead, AlertType.Starving }
+        { HungerThreshold.Peckish, "Peckish" },
+        { HungerThreshold.Starving, "Starving" },
+        { HungerThreshold.Dead, "Starving" }
     };
 
+    [DataField]
+    public ProtoId<AlertCategoryPrototype> HungerAlertCategory = "Hunger";
+
     /// <summary>
     /// A dictionary relating HungerThreshold to how much they modify <see cref="BaseDecayRate"/>.
     /// </summary>
diff --git a/Content.Shared/Nutrition/Components/ThirstComponent.cs b/Content.Shared/Nutrition/Components/ThirstComponent.cs
index 731346401fd..f3ac881361f 100644
--- a/Content.Shared/Nutrition/Components/ThirstComponent.cs
+++ b/Content.Shared/Nutrition/Components/ThirstComponent.cs
@@ -1,6 +1,7 @@
 using Content.Shared.Alert;
 using Content.Shared.Nutrition.EntitySystems;
 using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
 
 namespace Content.Shared.Nutrition.Components;
@@ -56,11 +57,14 @@ public sealed partial class ThirstComponent : Component
         {ThirstThreshold.Dead, 0.0f},
     };
 
-    public static readonly Dictionary<ThirstThreshold, AlertType> ThirstThresholdAlertTypes = new()
+    [DataField]
+    public ProtoId<AlertCategoryPrototype> ThirstyCategory = "Thirst";
+
+    public static readonly Dictionary<ThirstThreshold, ProtoId<AlertPrototype>> ThirstThresholdAlertTypes = new()
     {
-        {ThirstThreshold.Thirsty, AlertType.Thirsty},
-        {ThirstThreshold.Parched, AlertType.Parched},
-        {ThirstThreshold.Dead, AlertType.Parched},
+        {ThirstThreshold.Thirsty, "Thirsty"},
+        {ThirstThreshold.Parched, "Parched"},
+        {ThirstThreshold.Dead, "Parched"},
     };
 }
 
diff --git a/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs b/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs
index e6d82553336..6535390d646 100644
--- a/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs
+++ b/Content.Shared/Nutrition/EntitySystems/HungerSystem.cs
@@ -67,7 +67,7 @@ private void OnMapInit(EntityUid uid, HungerComponent component, MapInitEvent ar
 
     private void OnShutdown(EntityUid uid, HungerComponent component, ComponentShutdown args)
     {
-        _alerts.ClearAlertCategory(uid, AlertCategory.Hunger);
+        _alerts.ClearAlertCategory(uid, component.HungerAlertCategory);
     }
 
     private void OnRefreshMovespeed(EntityUid uid, HungerComponent component, RefreshMovementSpeedModifiersEvent args)
@@ -112,7 +112,7 @@ public void SetHunger(EntityUid uid, float amount, HungerComponent? component =
             component.Thresholds[HungerThreshold.Dead],
             component.Thresholds[HungerThreshold.Overfed]);
         UpdateCurrentThreshold(uid, component);
-        Dirty(component);
+        Dirty(uid, component);
     }
 
     private void UpdateCurrentThreshold(EntityUid uid, HungerComponent? component = null)
@@ -125,7 +125,7 @@ private void UpdateCurrentThreshold(EntityUid uid, HungerComponent? component =
             return;
         component.CurrentThreshold = calculatedHungerThreshold;
         DoHungerThresholdEffects(uid, component);
-        Dirty(component);
+        Dirty(uid, component);
     }
 
     private void DoHungerThresholdEffects(EntityUid uid, HungerComponent? component = null, bool force = false)
@@ -153,7 +153,7 @@ private void DoHungerThresholdEffects(EntityUid uid, HungerComponent? component
         }
         else
         {
-            _alerts.ClearAlertCategory(uid, AlertCategory.Hunger);
+            _alerts.ClearAlertCategory(uid, component.HungerAlertCategory);
         }
 
         if (component.HungerThresholdDecayModifiers.TryGetValue(component.CurrentThreshold, out var modifier))
diff --git a/Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs b/Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs
index a068b19104c..4ff49e795c2 100644
--- a/Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs
+++ b/Content.Shared/Nutrition/EntitySystems/ThirstSystem.cs
@@ -171,7 +171,7 @@ private void UpdateEffects(EntityUid uid, ThirstComponent component)
         }
         else
         {
-            _alerts.ClearAlertCategory(uid, AlertCategory.Thirst);
+            _alerts.ClearAlertCategory(uid, component.ThirstyCategory);
         }
 
         var ev = new MoodEffectEvent("Thirst" + component.CurrentThirstThreshold);
diff --git a/Content.Shared/OfferItem/OfferItemComponent.cs b/Content.Shared/OfferItem/OfferItemComponent.cs
index eb4d84932e5..f9f55291ddc 100644
--- a/Content.Shared/OfferItem/OfferItemComponent.cs
+++ b/Content.Shared/OfferItem/OfferItemComponent.cs
@@ -1,4 +1,6 @@
 using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Content.Shared.Alert;
 
 namespace Content.Shared.OfferItem;
 
@@ -23,4 +25,7 @@ public sealed partial class OfferItemComponent : Component
 
     [DataField]
     public float MaxOfferDistance = 2f;
+
+    [DataField]
+    public ProtoId<AlertPrototype> OfferAlert = "Offer";
 }
diff --git a/Content.Shared/PAI/PAIComponent.cs b/Content.Shared/PAI/PAIComponent.cs
index b4e4c927354..9d5be302758 100644
--- a/Content.Shared/PAI/PAIComponent.cs
+++ b/Content.Shared/PAI/PAIComponent.cs
@@ -31,7 +31,7 @@ public sealed partial class PAIComponent : Component
     public EntityUid? MidiAction;
 
     [DataField]
-    public ProtoId<EntityPrototype> MapActionId = "ActionPAIOpenMap";
+    public EntProtoId MapActionId = "ActionPAIOpenMap";
 
     [DataField, AutoNetworkedField]
     public EntityUid? MapAction;
diff --git a/Content.Shared/Physics/Controllers/SharedConveyorController.cs b/Content.Shared/Physics/Controllers/SharedConveyorController.cs
index c9ec77ba1c9..e3b22d84319 100644
--- a/Content.Shared/Physics/Controllers/SharedConveyorController.cs
+++ b/Content.Shared/Physics/Controllers/SharedConveyorController.cs
@@ -101,10 +101,10 @@ private void Convey(EntityUid uid, ConveyorComponent comp, EntityQuery<Transform
             transform.LocalPosition = localPos;
 
             // Force it awake for collisionwake reasons.
-            Physics.SetAwake(entity, body, true);
+            Physics.SetAwake((entity, body), true);
             Physics.SetSleepTime(body, 0f);
         }
-        Dirty(comp);
+        Dirty(uid, comp);
     }
 
     private static Vector2 Convey(Vector2 direction, float speed, float frameTime, Vector2 itemRelative)
diff --git a/Content.Shared/Preferences/HumanoidCharacterProfile.cs b/Content.Shared/Preferences/HumanoidCharacterProfile.cs
index 4feb193a65c..19646f7e12d 100644
--- a/Content.Shared/Preferences/HumanoidCharacterProfile.cs
+++ b/Content.Shared/Preferences/HumanoidCharacterProfile.cs
@@ -255,7 +255,7 @@ public static HumanoidCharacterProfile RandomWithSpecies(string species = Shared
     public HumanoidCharacterProfile WithSex(Sex sex) => new(this) { Sex = sex };
     public HumanoidCharacterProfile WithGender(Gender gender) => new(this) { Gender = gender };
     public HumanoidCharacterProfile WithSpecies(string species) => new(this) { Species = species };
-    public HumanoidCharacterProfile WithCustomSpeciesName(string customspeciename) => new(this) { Customspeciename = customspeciename};
+    public HumanoidCharacterProfile WithCustomSpeciesName(string customspeciename) => new(this) { Customspeciename = customspeciename };
     public HumanoidCharacterProfile WithHeight(float height) => new(this) { Height = height };
     public HumanoidCharacterProfile WithWidth(float width) => new(this) { Width = width };
 
@@ -368,7 +368,9 @@ public void EnsureValid(ICommonSession session, IDependencyCollection collection
 
         // ensure the species can be that sex and their age fits the founds
         if (!speciesPrototype.Sexes.Contains(sex))
+        {
             sex = speciesPrototype.Sexes[0];
+        }
 
         var age = Math.Clamp(Age, speciesPrototype.MinAge, speciesPrototype.MaxAge);
 
@@ -383,16 +385,24 @@ public void EnsureValid(ICommonSession session, IDependencyCollection collection
 
         string name;
         if (string.IsNullOrEmpty(Name))
+        {
             name = GetName(Species, gender);
+        }
         else if (Name.Length > MaxNameLength)
+        {
             name = Name[..MaxNameLength];
+        }
         else
+        {
             name = Name;
+        }
 
         name = name.Trim();
 
         if (configManager.GetCVar(CCVars.RestrictedNames))
+        {
             name = RestrictedNameRegex.Replace(name, string.Empty);
+        }
 
         if (configManager.GetCVar(CCVars.ICNameCase))
         {
@@ -405,17 +415,23 @@ public void EnsureValid(ICommonSession session, IDependencyCollection collection
             || string.IsNullOrEmpty(Customspeciename)
                 ? ""
                 : Customspeciename.Length > MaxNameLength
-                    ? FormattedMessage.RemoveMarkup(Customspeciename)[..MaxNameLength]
-                    : FormattedMessage.RemoveMarkup(Customspeciename);
+                    ? FormattedMessage.RemoveMarkupPermissive(Customspeciename)[..MaxNameLength]
+                    : FormattedMessage.RemoveMarkupPermissive(Customspeciename);
 
         if (string.IsNullOrEmpty(name))
+        {
             name = GetName(Species, gender);
+        }
 
         string flavortext;
         if (FlavorText.Length > MaxDescLength)
-            flavortext = FormattedMessage.RemoveMarkup(FlavorText)[..MaxDescLength];
+        {
+            flavortext = FormattedMessage.RemoveMarkupPermissive(FlavorText)[..MaxDescLength];
+        }
         else
-            flavortext = FormattedMessage.RemoveMarkup(FlavorText);
+        {
+            flavortext = FormattedMessage.RemoveMarkupPermissive(FlavorText);
+        }
 
         var appearance = HumanoidCharacterAppearance.EnsureValid(Appearance, Species, Sex);
 
@@ -468,7 +484,9 @@ public void EnsureValid(ICommonSession session, IDependencyCollection collection
         _jobPriorities.Clear();
 
         foreach (var (job, priority) in priorities)
+        {
             _jobPriorities.Add(job, priority);
+        }
 
         PreferenceUnavailable = prefsUnavailableMode;
 
@@ -513,11 +531,11 @@ public override int GetHashCode()
         hashCode.Add(FlavorText);
         hashCode.Add(Species);
         hashCode.Add(Age);
-        hashCode.Add((int)Sex);
-        hashCode.Add((int)Gender);
+        hashCode.Add((int) Sex);
+        hashCode.Add((int) Gender);
         hashCode.Add(Appearance);
-        hashCode.Add((int)SpawnPriority);
-        hashCode.Add((int)PreferenceUnavailable);
+        hashCode.Add((int) SpawnPriority);
+        hashCode.Add((int) PreferenceUnavailable);
         hashCode.Add(Customspeciename);
         return hashCode.ToHashCode();
     }
diff --git a/Content.Shared/RCD/Systems/RCDAmmoSystem.cs b/Content.Shared/RCD/Systems/RCDAmmoSystem.cs
index 9481d299aaa..9cb3c264851 100644
--- a/Content.Shared/RCD/Systems/RCDAmmoSystem.cs
+++ b/Content.Shared/RCD/Systems/RCDAmmoSystem.cs
@@ -36,7 +36,7 @@ private void OnAfterInteract(EntityUid uid, RCDAmmoComponent comp, AfterInteract
         if (args.Handled || !args.CanReach || !_timing.IsFirstTimePredicted)
             return;
 
-        if (args.Target is not {Valid: true} target ||
+        if (args.Target is not { Valid: true } target ||
             !HasComp<RCDComponent>(target) ||
             !TryComp<LimitedChargesComponent>(target, out var charges))
             return;
@@ -53,7 +53,7 @@ private void OnAfterInteract(EntityUid uid, RCDAmmoComponent comp, AfterInteract
         _popup.PopupClient(Loc.GetString("rcd-ammo-component-after-interact-refilled"), target, user);
         _charges.AddCharges(target, count, charges);
         comp.Charges -= count;
-        Dirty(comp);
+        Dirty(uid, comp);
 
         // prevent having useless ammo with 0 charges
         if (comp.Charges <= 0)
diff --git a/Content.Shared/Random/Helpers/SharedRandomExtensions.cs b/Content.Shared/Random/Helpers/SharedRandomExtensions.cs
index 20e57e94212..3941c2859bc 100644
--- a/Content.Shared/Random/Helpers/SharedRandomExtensions.cs
+++ b/Content.Shared/Random/Helpers/SharedRandomExtensions.cs
@@ -1,3 +1,4 @@
+using System.Diagnostics.CodeAnalysis;
 using System.Linq;
 using Content.Shared.Dataset;
 using Content.Shared.FixedPoint;
@@ -41,7 +42,7 @@ public static string Pick(this IWeightedRandomPrototype prototype, IRobustRandom
             var sum = picks.Values.Sum();
             var accumulated = 0f;
 
-            var rand = random.NextFloat() * sum;
+            var rand = random!.NextFloat() * sum;
 
             foreach (var (key, weight) in picks)
             {
@@ -78,6 +79,47 @@ public static T Pick<T>(this IRobustRandom random, Dictionary<T, float> weights)
             throw new InvalidOperationException("Invalid weighted pick");
         }
 
+        public static T PickAndTake<T>(this IRobustRandom random, Dictionary<T, float> weights)
+            where T : notnull
+        {
+            var pick = Pick(random, weights);
+            weights.Remove(pick);
+            return pick;
+        }
+
+        public static bool TryPickAndTake<T>(this IRobustRandom random, Dictionary<T, float> weights, [NotNullWhen(true)] out T? pick)
+            where T : notnull
+        {
+            if (weights.Count == 0)
+            {
+                pick = default;
+                return false;
+            }
+            pick = PickAndTake(random, weights);
+            return true;
+        }
+
+        public static T Pick<T>(Dictionary<T, float> weights, System.Random random)
+            where T : notnull
+        {
+            var sum = weights.Values.Sum();
+            var accumulated = 0f;
+
+            var rand = random.NextFloat() * sum;
+
+            foreach (var (key, weight) in weights)
+            {
+                accumulated += weight;
+
+                if (accumulated >= rand)
+                {
+                    return key;
+                }
+            }
+
+            throw new InvalidOperationException("Invalid weighted pick");
+        }
+
         public static (string reagent, FixedPoint2 quantity) Pick(this WeightedRandomFillSolutionPrototype prototype, IRobustRandom? random = null)
         {
             var randomFill = prototype.PickRandomFill(random);
@@ -87,7 +129,7 @@ public static (string reagent, FixedPoint2 quantity) Pick(this WeightedRandomFil
             var sum = randomFill.Reagents.Count;
             var accumulated = 0f;
 
-            var rand = random.NextFloat() * sum;
+            var rand = random!.NextFloat() * sum;
 
             foreach (var reagent in randomFill.Reagents)
             {
@@ -118,7 +160,7 @@ public static RandomFillSolution PickRandomFill(this WeightedRandomFillSolutionP
             var sum = picks.Values.Sum();
             var accumulated = 0f;
 
-            var rand = random.NextFloat() * sum;
+            var rand = random!.NextFloat() * sum;
 
             foreach (var (randSolution, weight) in picks)
             {
diff --git a/Content.Shared/Revenant/Components/RevenantComponent.cs b/Content.Shared/Revenant/Components/RevenantComponent.cs
index 947c1a4b3fc..d7fb28ef136 100644
--- a/Content.Shared/Revenant/Components/RevenantComponent.cs
+++ b/Content.Shared/Revenant/Components/RevenantComponent.cs
@@ -1,4 +1,5 @@
 using System.Numerics;
+using Content.Shared.Alert;
 using Content.Shared.FixedPoint;
 using Content.Shared.Store;
 using Content.Shared.Whitelist;
@@ -200,6 +201,9 @@ public sealed partial class RevenantComponent : Component
     public EntityWhitelist? MalfunctionBlacklist;
     #endregion
 
+    [DataField]
+    public ProtoId<AlertPrototype> EssenceAlert = "Essence";
+
     #region Visualizer
     [DataField("state")]
     public string State = "idle";
diff --git a/Content.Shared/Shadowkin/ShadowkinComponent.cs b/Content.Shared/Shadowkin/ShadowkinComponent.cs
index b382f3112b7..a2a4fdf334f 100644
--- a/Content.Shared/Shadowkin/ShadowkinComponent.cs
+++ b/Content.Shared/Shadowkin/ShadowkinComponent.cs
@@ -1,4 +1,6 @@
 using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+using Content.Shared.Alert;
 
 namespace Content.Shared.Shadowkin;
 
@@ -39,4 +41,7 @@ public sealed partial class ShadowkinComponent : Component
 
     [DataField]
     public EntityUid? ShadowkinSleepAction;
+
+    [DataField]
+    public ProtoId<AlertPrototype> ShadowkinPowerAlert = "ShadowkinPower";
 }
\ No newline at end of file
diff --git a/Content.Shared/Showers/SharedShowerSystem.cs b/Content.Shared/Showers/SharedShowerSystem.cs
index be3af6228f2..138a6869a94 100644
--- a/Content.Shared/Showers/SharedShowerSystem.cs
+++ b/Content.Shared/Showers/SharedShowerSystem.cs
@@ -79,7 +79,12 @@ private void UpdateAppearance(EntityUid uid, ShowerComponent? component = null)
             {
                 if (component.PlayingStream == null)
                 {
-                    component.PlayingStream = _audio.PlayPvs(component.LoopingSound, uid, AudioParams.Default.WithLoop(true).WithMaxDistance(5)).Value.Entity;
+                    var audio = _audio.PlayPvs(component.LoopingSound, uid, AudioParams.Default.WithLoop(true).WithMaxDistance(5));
+
+                    if (audio == null)
+                        return;
+
+                    component.PlayingStream = audio!.Value.Entity;
                 }
             }
             else
diff --git a/Content.Server/Shuttles/Components/FTLComponent.cs b/Content.Shared/Shuttles/Components/FTLComponent.cs
similarity index 81%
rename from Content.Server/Shuttles/Components/FTLComponent.cs
rename to Content.Shared/Shuttles/Components/FTLComponent.cs
index c9b84064234..9acca7c1d45 100644
--- a/Content.Server/Shuttles/Components/FTLComponent.cs
+++ b/Content.Shared/Shuttles/Components/FTLComponent.cs
@@ -2,16 +2,16 @@
 using Content.Shared.Tag;
 using Content.Shared.Timing;
 using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
 using Robust.Shared.Map;
 using Robust.Shared.Prototypes;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
 
-namespace Content.Server.Shuttles.Components;
+namespace Content.Shared.Shuttles.Components;
 
 /// <summary>
 /// Added to a component when it is queued or is travelling via FTL.
 /// </summary>
-[RegisterComponent]
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
 public sealed partial class FTLComponent : Component
 {
     // TODO Full game save / add datafields
@@ -29,13 +29,19 @@ public sealed partial class FTLComponent : Component
     [ViewVariables(VVAccess.ReadWrite)]
     public float TravelTime = 0f;
 
+    [DataField]
+    public EntProtoId? VisualizerProto = "FtlVisualizerEntity";
+
+    [DataField, AutoNetworkedField]
+    public EntityUid? VisualizerEntity;
+
     /// <summary>
     /// Coordinates to arrive it: May be relative to another grid (for docking) or map coordinates.
     /// </summary>
-    [ViewVariables(VVAccess.ReadWrite), DataField]
+    [DataField, AutoNetworkedField]
     public EntityCoordinates TargetCoordinates;
 
-    [DataField]
+    [DataField, AutoNetworkedField]
     public Angle TargetAngle;
 
     /// <summary>
diff --git a/Content.Shared/Shuttles/Components/FtlVisualizerComponent.cs b/Content.Shared/Shuttles/Components/FtlVisualizerComponent.cs
new file mode 100644
index 00000000000..628a4f828b2
--- /dev/null
+++ b/Content.Shared/Shuttles/Components/FtlVisualizerComponent.cs
@@ -0,0 +1,23 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Shuttles.Components;
+
+[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
+public sealed partial class FtlVisualizerComponent : Component
+{
+    /// <summary>
+    /// Clientside time tracker for the animation.
+    /// </summary>
+    [ViewVariables(VVAccess.ReadWrite)]
+    public float Elapsed;
+
+    [DataField(required: true)]
+    public SpriteSpecifier.Rsi Sprite;
+
+    /// <summary>
+    /// Target grid to pull FTL visualization from.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public EntityUid Grid;
+}
diff --git a/Content.Shared/Shuttles/Components/PilotComponent.cs b/Content.Shared/Shuttles/Components/PilotComponent.cs
index 1a6927cf813..cb42db4436f 100644
--- a/Content.Shared/Shuttles/Components/PilotComponent.cs
+++ b/Content.Shared/Shuttles/Components/PilotComponent.cs
@@ -1,7 +1,9 @@
 using System.Numerics;
+using Content.Shared.Alert;
 using Content.Shared.Movement.Systems;
 using Robust.Shared.GameStates;
 using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Timing;
 
 namespace Content.Shared.Shuttles.Components
@@ -32,6 +34,9 @@ public sealed partial class PilotComponent : Component
         [ViewVariables]
         public ShuttleButtons HeldButtons = ShuttleButtons.None;
 
+        [DataField]
+        public ProtoId<AlertPrototype> PilotingAlert = "PilotingShuttle";
+
         public override bool SendOnlyToOwner => true;
     }
 }
diff --git a/Content.Shared/Silicon/Systems/SharedSiliconSystem.cs b/Content.Shared/Silicon/Systems/SharedSiliconSystem.cs
index 8fe87e162bc..37d3bcd5c3b 100644
--- a/Content.Shared/Silicon/Systems/SharedSiliconSystem.cs
+++ b/Content.Shared/Silicon/Systems/SharedSiliconSystem.cs
@@ -60,12 +60,12 @@ private void OnSiliconInit(EntityUid uid, SiliconComponent component, ComponentI
         if (!component.BatteryPowered)
             return;
 
-        _alertsSystem.ShowAlert(uid, AlertType.BorgBattery, component.ChargeState);
+        _alertsSystem.ShowAlert(uid, component.BatteryAlert, component.ChargeState);
     }
 
     private void OnSiliconChargeStateUpdate(EntityUid uid, SiliconComponent component, SiliconChargeStateUpdateEvent ev)
     {
-        _alertsSystem.ShowAlert(uid, AlertType.BorgBattery, ev.ChargePercent);
+        _alertsSystem.ShowAlert(uid, component.BatteryAlert, ev.ChargePercent);
     }
 
     private void OnRefreshMovespeed(EntityUid uid, SiliconComponent component, RefreshMovementSpeedModifiersEvent args)
diff --git a/Content.Shared/Silicons/Borgs/Components/BorgChassisComponent.cs b/Content.Shared/Silicons/Borgs/Components/BorgChassisComponent.cs
index 71d3a7bd166..e1776873da9 100644
--- a/Content.Shared/Silicons/Borgs/Components/BorgChassisComponent.cs
+++ b/Content.Shared/Silicons/Borgs/Components/BorgChassisComponent.cs
@@ -1,6 +1,8 @@
-using Content.Shared.Whitelist;
+using Content.Shared.Alert;
+using Content.Shared.Whitelist;
 using Robust.Shared.Containers;
 using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization;
 
 namespace Content.Shared.Silicons.Borgs.Components;
@@ -76,6 +78,12 @@ public sealed partial class BorgChassisComponent : Component
     [DataField("noMindState")]
     public string NoMindState = string.Empty;
     #endregion
+
+    [DataField]
+    public ProtoId<AlertPrototype> BatteryAlert = "BorgBattery";
+
+    [DataField]
+    public ProtoId<AlertPrototype> NoBatteryAlert = "BorgBatteryNone";
 }
 
 [Serializable, NetSerializable]
diff --git a/Content.Shared/Singularity/EntitySystems/SharedEventHorizonSystem.cs b/Content.Shared/Singularity/EntitySystems/SharedEventHorizonSystem.cs
index f31dd8776a4..c2b52c5af35 100644
--- a/Content.Shared/Singularity/EntitySystems/SharedEventHorizonSystem.cs
+++ b/Content.Shared/Singularity/EntitySystems/SharedEventHorizonSystem.cs
@@ -66,7 +66,7 @@ public void SetRadius(EntityUid uid, float value, bool updateFixture = true, Eve
             return;
 
         eventHorizon.Radius = value;
-        Dirty(eventHorizon);
+        Dirty(uid, eventHorizon);
         if (updateFixture)
             UpdateEventHorizonFixture(uid, eventHorizon: eventHorizon);
     }
@@ -89,7 +89,7 @@ public void SetCanBreachContainment(EntityUid uid, bool value, bool updateFixtur
             return;
 
         eventHorizon.CanBreachContainment = value;
-        Dirty(eventHorizon);
+        Dirty(uid, eventHorizon);
         if (updateFixture)
             UpdateEventHorizonFixture(uid, eventHorizon: eventHorizon);
     }
@@ -112,7 +112,7 @@ public void SetColliderFixtureId(EntityUid uid, string? value, bool updateFixtur
             return;
 
         eventHorizon.ColliderFixtureId = value;
-        Dirty(eventHorizon);
+        Dirty(uid, eventHorizon);
         if (updateFixture)
             UpdateEventHorizonFixture(uid, eventHorizon: eventHorizon);
     }
@@ -135,7 +135,7 @@ public void SetConsumerFixtureId(EntityUid uid, string? value, bool updateFixtur
             return;
 
         eventHorizon.ConsumerFixtureId = value;
-        Dirty(eventHorizon);
+        Dirty(uid, eventHorizon);
         if (updateFixture)
             UpdateEventHorizonFixture(uid, eventHorizon: eventHorizon);
     }
diff --git a/Content.Shared/Stacks/SharedStackSystem.cs b/Content.Shared/Stacks/SharedStackSystem.cs
index 756c84cac55..e12edd323c7 100644
--- a/Content.Shared/Stacks/SharedStackSystem.cs
+++ b/Content.Shared/Stacks/SharedStackSystem.cs
@@ -23,8 +23,8 @@ public abstract class SharedStackSystem : EntitySystem
         [Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
         [Dependency] protected readonly SharedHandsSystem Hands = default!;
         [Dependency] protected readonly SharedTransformSystem Xform = default!;
-        [Dependency] private   readonly EntityLookupSystem _entityLookup = default!;
-        [Dependency] private   readonly SharedPhysicsSystem _physics = default!;
+        [Dependency] private readonly EntityLookupSystem _entityLookup = default!;
+        [Dependency] private readonly SharedPhysicsSystem _physics = default!;
         [Dependency] protected readonly SharedPopupSystem Popup = default!;
         [Dependency] private readonly SharedStorageSystem _storage = default!;
 
@@ -175,7 +175,7 @@ public virtual void SetCount(EntityUid uid, int amount, StackComponent? componen
 
             // Server-side override deletes the entity if count == 0
             component.Count = amount;
-            Dirty(component);
+            Dirty(uid, component);
 
             Appearance.SetData(uid, StackVisuals.Actual, component.Count);
             RaiseLocalEvent(uid, new StackCountChangedEvent(old, component.Count));
diff --git a/Content.Shared/Standing/SharedLayingDownSystem.cs b/Content.Shared/Standing/SharedLayingDownSystem.cs
index b2bb5add5f4..2f95a06f197 100644
--- a/Content.Shared/Standing/SharedLayingDownSystem.cs
+++ b/Content.Shared/Standing/SharedLayingDownSystem.cs
@@ -174,7 +174,7 @@ public bool TryLieDown(EntityUid uid, LayingDownComponent? layingDown = null, St
             return false;
         }
 
-        _standing.Down(uid, true, behavior != DropHeldItemsBehavior.NoDrop, standingState, setDrawDepth: true);
+        _standing.Down(uid, true, behavior != DropHeldItemsBehavior.NoDrop, standingState);
         return true;
     }
 }
@@ -188,4 +188,4 @@ public enum DropHeldItemsBehavior : byte
     NoDrop,
     DropIfStanding,
     AlwaysDrop
-}
+}
\ No newline at end of file
diff --git a/Content.Shared/Standing/StandingStateSystem.cs b/Content.Shared/Standing/StandingStateSystem.cs
index 5abbf53f1b2..a1b5418941a 100644
--- a/Content.Shared/Standing/StandingStateSystem.cs
+++ b/Content.Shared/Standing/StandingStateSystem.cs
@@ -57,7 +57,7 @@ public bool Down(EntityUid uid, bool playSound = true, bool dropHeldItems = true
         if (dropHeldItems && hands != null)
             RaiseLocalEvent(uid, new DropHandItemsEvent(), false);
 
-        if (TryComp(uid, out BuckleComponent? buckle) && buckle.Buckled && !_buckle.TryUnbuckle(uid, uid, buckleComp: buckle))
+        if (TryComp(uid, out BuckleComponent? buckle) && buckle.Buckled)
             return false;
 
         var msg = new DownAttemptEvent();
@@ -67,7 +67,7 @@ public bool Down(EntityUid uid, bool playSound = true, bool dropHeldItems = true
             return false;
 
         standingState.CurrentState = StandingState.Lying;
-        Dirty(standingState);
+        Dirty(uid, standingState);
         RaiseLocalEvent(uid, new DownedEvent(), false);
 
         // Seemed like the best place to put it
@@ -184,4 +184,4 @@ public sealed class StoodEvent : EntityEventArgs { }
 /// <summary>
 ///     Raised when an entity is not standing
 /// </summary>
-public sealed class DownedEvent : EntityEventArgs { }
+public sealed class DownedEvent : EntityEventArgs { }
\ No newline at end of file
diff --git a/Content.Shared/StationRecords/StationRecordKeyStorageSystem.cs b/Content.Shared/StationRecords/StationRecordKeyStorageSystem.cs
index 05af0807f21..e9d68721b63 100644
--- a/Content.Shared/StationRecords/StationRecordKeyStorageSystem.cs
+++ b/Content.Shared/StationRecords/StationRecordKeyStorageSystem.cs
@@ -58,7 +58,7 @@ public void AssignKey(EntityUid uid, StationRecordKey key, StationRecordKeyStora
 
         var key = keyStorage.Key;
         keyStorage.Key = null;
-        Dirty(keyStorage);
+        Dirty(uid, keyStorage);
 
         return key;
     }
diff --git a/Content.Shared/StatusEffect/StatusEffectPrototype.cs b/Content.Shared/StatusEffect/StatusEffectPrototype.cs
index ae9e26879eb..8b1f84e4d81 100644
--- a/Content.Shared/StatusEffect/StatusEffectPrototype.cs
+++ b/Content.Shared/StatusEffect/StatusEffectPrototype.cs
@@ -10,7 +10,7 @@ public sealed partial class StatusEffectPrototype : IPrototype
         public string ID { get; private set; } = default!;
 
         [DataField("alert")]
-        public AlertType? Alert { get; private set; }
+        public ProtoId<AlertPrototype>? Alert { get; private set; }
 
         /// <summary>
         ///     Whether a status effect should be able to apply to any entity,
diff --git a/Content.Shared/StatusEffect/StatusEffectsSystem.cs b/Content.Shared/StatusEffect/StatusEffectsSystem.cs
index cc6dedae495..124dcdd8d67 100644
--- a/Content.Shared/StatusEffect/StatusEffectsSystem.cs
+++ b/Content.Shared/StatusEffect/StatusEffectsSystem.cs
@@ -234,7 +234,7 @@ public bool TryAddStatusEffect(EntityUid uid, string key, TimeSpan time, bool re
                 _alertsSystem.ShowAlert(uid, proto.Alert.Value, null, cooldown1);
             }
 
-            Dirty(status);
+            Dirty(uid, status);
             RaiseLocalEvent(uid, new StatusEffectAddedEvent(uid, key));
             return true;
         }
@@ -246,7 +246,7 @@ public bool TryAddStatusEffect(EntityUid uid, string key, TimeSpan time, bool re
         ///     This is mostly for stuns, since Stun and Knockdown share an alert key. Other times this pretty much
         ///     will not be useful.
         /// </remarks>
-        private (TimeSpan, TimeSpan)? GetAlertCooldown(EntityUid uid, AlertType alert, StatusEffectsComponent status)
+        private (TimeSpan, TimeSpan)? GetAlertCooldown(EntityUid uid, ProtoId<AlertPrototype> alert, StatusEffectsComponent status)
         {
             (TimeSpan, TimeSpan)? maxCooldown = null;
             foreach (var kvp in status.ActiveEffects)
@@ -310,7 +310,7 @@ public bool TryRemoveStatusEffect(EntityUid uid, string key,
                 RemComp<ActiveStatusEffectsComponent>(uid);
             }
 
-            Dirty(status);
+            Dirty(uid, status);
             RaiseLocalEvent(uid, new StatusEffectEndedEvent(uid, key));
             return true;
         }
@@ -334,7 +334,7 @@ public bool TryRemoveAllStatusEffects(EntityUid uid,
                     failed = true;
             }
 
-            Dirty(status);
+            Dirty(uid, status);
             return failed;
         }
 
@@ -408,7 +408,7 @@ public bool TryAddTime(EntityUid uid, string key, TimeSpan time,
                 _alertsSystem.ShowAlert(uid, proto.Alert.Value, null, cooldown);
             }
 
-            Dirty(status);
+            Dirty(uid, status);
             return true;
         }
 
@@ -444,7 +444,7 @@ public bool TryRemoveTime(EntityUid uid, string key, TimeSpan time,
                 _alertsSystem.ShowAlert(uid, proto.Alert.Value, null, cooldown);
             }
 
-            Dirty(status);
+            Dirty(uid, status);
             return true;
         }
 
@@ -465,7 +465,7 @@ public bool TrySetTime(EntityUid uid, string key, TimeSpan time,
 
             status.ActiveEffects[key].Cooldown = (_gameTiming.CurTime, _gameTiming.CurTime + time);
 
-            Dirty(status);
+            Dirty(uid, status);
             return true;
         }
 
diff --git a/Content.Shared/Storage/EntitySystems/BinSystem.cs b/Content.Shared/Storage/EntitySystems/BinSystem.cs
index 17c3eb4288c..1cc95337ea4 100644
--- a/Content.Shared/Storage/EntitySystems/BinSystem.cs
+++ b/Content.Shared/Storage/EntitySystems/BinSystem.cs
@@ -135,7 +135,7 @@ public bool TryInsertIntoBin(EntityUid uid, EntityUid toInsert, BinComponent? co
 
         _container.Insert(toInsert, component.ItemContainer);
         component.Items.Add(toInsert);
-        Dirty(component);
+        Dirty(uid, component);
         return true;
     }
 
@@ -151,7 +151,7 @@ public bool TryRemoveFromBin(EntityUid uid, EntityUid? toRemove, BinComponent? c
         if (!Resolve(uid, ref component))
             return false;
 
-        if (!component.Items.Any())
+        if (component.Items.Count == 0)
             return false;
 
         if (toRemove == null || toRemove != component.Items.LastOrDefault())
@@ -161,7 +161,7 @@ public bool TryRemoveFromBin(EntityUid uid, EntityUid? toRemove, BinComponent? c
             return false;
 
         component.Items.Remove(toRemove.Value);
-        Dirty(component);
+        Dirty(uid, component);
         return true;
     }
 }
diff --git a/Content.Shared/Stunnable/SharedStunSystem.cs b/Content.Shared/Stunnable/SharedStunSystem.cs
index 05d6b8ec533..ad36ba9329a 100644
--- a/Content.Shared/Stunnable/SharedStunSystem.cs
+++ b/Content.Shared/Stunnable/SharedStunSystem.cs
@@ -1,7 +1,5 @@
 using Content.Shared.ActionBlocker;
 using Content.Shared.Administration.Logs;
-using Content.Shared.Audio;
-using Content.Shared.DragDrop;
 using Content.Shared.Interaction;
 using Content.Shared.Interaction.Events;
 using Content.Shared.Inventory.Events;
@@ -11,13 +9,11 @@
 using Content.Shared.Hands;
 using Content.Shared.Mobs;
 using Content.Shared.Mobs.Components;
-using Content.Shared.Mobs.Systems;
 using Content.Shared.Movement.Events;
 using Content.Shared.Movement.Systems;
 using Content.Shared.Standing;
 using Content.Shared.StatusEffect;
 using Content.Shared.Throwing;
-using Robust.Shared.Audio;
 using Robust.Shared.Audio.Systems;
 using Robust.Shared.Containers;
 using Robust.Shared.GameStates;
@@ -88,15 +84,15 @@ private void OnMobStateChanged(EntityUid uid, MobStateComponent component, MobSt
             case MobState.Alive:
                 break;
             case MobState.Critical:
-            {
-                _statusEffect.TryRemoveStatusEffect(uid, "Stun");
-                break;
-            }
+                {
+                    _statusEffect.TryRemoveStatusEffect(uid, "Stun");
+                    break;
+                }
             case MobState.Dead:
-            {
-                _statusEffect.TryRemoveStatusEffect(uid, "Stun");
-                break;
-            }
+                {
+                    _statusEffect.TryRemoveStatusEffect(uid, "Stun");
+                    break;
+                }
             case MobState.Invalid:
             default:
                 return;
@@ -258,11 +254,11 @@ private void OnInteractHand(EntityUid uid, KnockedDownComponent knocked, Interac
             return;
 
         // Set it to half the help interval so helping is actually useful...
-        knocked.HelpTimer = knocked.HelpInterval/2f;
+        knocked.HelpTimer = knocked.HelpInterval / 2f;
 
         _statusEffect.TryRemoveTime(uid, "KnockedDown", TimeSpan.FromSeconds(knocked.HelpInterval));
         _audio.PlayPredicted(knocked.StunAttemptSound, uid, args.User);
-        Dirty(knocked);
+        Dirty(uid, knocked);
 
         args.Handled = true;
     }
diff --git a/Content.Shared/SubFloor/SharedTrayScannerSystem.cs b/Content.Shared/SubFloor/SharedTrayScannerSystem.cs
index 3dc1575ddb9..6e8393036d4 100644
--- a/Content.Shared/SubFloor/SharedTrayScannerSystem.cs
+++ b/Content.Shared/SubFloor/SharedTrayScannerSystem.cs
@@ -35,7 +35,7 @@ private void SetScannerEnabled(EntityUid uid, bool enabled, TrayScannerComponent
             return;
 
         scanner.Enabled = enabled;
-        Dirty(scanner);
+        Dirty(uid, scanner);
 
         // We don't remove from _activeScanners on disabled, because the update function will handle that, as well as
         // managing the revealed subfloor entities
diff --git a/Content.Shared/Tag/TagSystem.cs b/Content.Shared/Tag/TagSystem.cs
index 0707308e486..b9fef076c88 100644
--- a/Content.Shared/Tag/TagSystem.cs
+++ b/Content.Shared/Tag/TagSystem.cs
@@ -79,7 +79,7 @@ private void AssertValidTag(string id)
     /// </exception>
     public bool AddTag(EntityUid entity, string id)
     {
-        return AddTag(EnsureComp<TagComponent>(entity), id);
+        return AddTag(entity, EnsureComp<TagComponent>(entity), id);
     }
 
     /// <summary>
@@ -95,7 +95,7 @@ public bool AddTag(EntityUid entity, string id)
     /// </exception>
     public bool AddTags(EntityUid entity, params string[] ids)
     {
-        return AddTags(EnsureComp<TagComponent>(entity), ids);
+        return AddTags(entity, EnsureComp<TagComponent>(entity), ids);
     }
 
     /// <summary>
@@ -111,7 +111,7 @@ public bool AddTags(EntityUid entity, params string[] ids)
     /// </exception>
     public bool AddTags(EntityUid entity, IEnumerable<string> ids)
     {
-        return AddTags(EnsureComp<TagComponent>(entity), ids);
+        return AddTags(entity, EnsureComp<TagComponent>(entity), ids);
     }
 
     /// <summary>
@@ -128,8 +128,8 @@ public bool AddTags(EntityUid entity, IEnumerable<string> ids)
     /// </exception>
     public bool TryAddTag(EntityUid entity, string id)
     {
-        return _tagQuery.TryComp(entity, out var component) &&
-               AddTag(component, id);
+        return TryComp<TagComponent>(entity, out var component) &&
+               AddTag(entity, component, id);
     }
 
     /// <summary>
@@ -146,8 +146,8 @@ public bool TryAddTag(EntityUid entity, string id)
     /// </exception>
     public bool TryAddTags(EntityUid entity, params string[] ids)
     {
-        return _tagQuery.TryComp(entity, out var component) &&
-               AddTags(component, ids);
+        return TryComp<TagComponent>(entity, out var component) &&
+               AddTags(entity, component, ids);
     }
 
     /// <summary>
@@ -164,8 +164,8 @@ public bool TryAddTags(EntityUid entity, params string[] ids)
     /// </exception>
     public bool TryAddTags(EntityUid entity, IEnumerable<string> ids)
     {
-        return _tagQuery.TryComp(entity, out var component) &&
-               AddTags(component, ids);
+        return TryComp<TagComponent>(entity, out var component) &&
+               AddTags(entity, component, ids);
     }
 
     /// <summary>
@@ -333,8 +333,8 @@ public bool HasAnyTag(EntityUid entity, IEnumerable<string> ids)
     /// </exception>
     public bool RemoveTag(EntityUid entity, string id)
     {
-        return _tagQuery.TryComp(entity, out var component) &&
-               RemoveTag(component, id);
+        return TryComp<TagComponent>(entity, out var component) &&
+               RemoveTag(entity, component, id);
     }
 
     /// <summary>
@@ -350,8 +350,8 @@ public bool RemoveTag(EntityUid entity, string id)
     /// </returns>
     public bool RemoveTags(EntityUid entity, params string[] ids)
     {
-        return _tagQuery.TryComp(entity, out var component) &&
-               RemoveTags(component, ids);
+        return TryComp<TagComponent>(entity, out var component) &&
+               RemoveTags(entity, component, ids);
     }
 
     /// <summary>
@@ -367,8 +367,8 @@ public bool RemoveTags(EntityUid entity, params string[] ids)
     /// </exception>
     public bool RemoveTags(EntityUid entity, IEnumerable<string> ids)
     {
-        return _tagQuery.TryComp(entity, out var component) &&
-               RemoveTags(component, ids);
+        return TryComp<TagComponent>(entity, out var component) &&
+               RemoveTags(entity, component, ids);
     }
 
     /// <summary>
@@ -379,14 +379,14 @@ public bool RemoveTags(EntityUid entity, IEnumerable<string> ids)
     /// <exception cref="UnknownPrototypeException">
     ///     Thrown if no <see cref="TagPrototype"/> exists with the given id.
     /// </exception>
-    public bool AddTag(TagComponent component, string id)
+    public bool AddTag(EntityUid uid, TagComponent component, string id)
     {
         AssertValidTag(id);
         var added = component.Tags.Add(id);
 
         if (added)
         {
-            Dirty(component);
+            Dirty(uid, component);
             return true;
         }
 
@@ -401,9 +401,9 @@ public bool AddTag(TagComponent component, string id)
     /// <exception cref="UnknownPrototypeException">
     ///     Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>.
     /// </exception>
-    public bool AddTags(TagComponent component, params string[] ids)
+    public bool AddTags(EntityUid uid, TagComponent component, params string[] ids)
     {
-        return AddTags(component, ids.AsEnumerable());
+        return AddTags(uid, component, ids.AsEnumerable());
     }
 
     /// <summary>
@@ -414,7 +414,7 @@ public bool AddTags(TagComponent component, params string[] ids)
     /// <exception cref="UnknownPrototypeException">
     ///     Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>.
     /// </exception>
-    public bool AddTags(TagComponent component, IEnumerable<string> ids)
+    public bool AddTags(EntityUid uid, TagComponent component, IEnumerable<string> ids)
     {
         var count = component.Tags.Count;
 
@@ -426,7 +426,7 @@ public bool AddTags(TagComponent component, IEnumerable<string> ids)
 
         if (component.Tags.Count > count)
         {
-            Dirty(component);
+            Dirty(uid, component);
             return true;
         }
 
@@ -642,13 +642,13 @@ public bool HasAnyTag(TagComponent comp, List<ProtoId<TagPrototype>> ids)
     /// <exception cref="UnknownPrototypeException">
     ///     Thrown if no <see cref="TagPrototype"/> exists with the given id.
     /// </exception>
-    public bool RemoveTag(TagComponent component, string id)
+    public bool RemoveTag(EntityUid uid, TagComponent component, string id)
     {
         AssertValidTag(id);
 
         if (component.Tags.Remove(id))
         {
-            Dirty(component);
+            Dirty(uid, component);
             return true;
         }
 
@@ -665,9 +665,9 @@ public bool RemoveTag(TagComponent component, string id)
     /// <exception cref="UnknownPrototypeException">
     ///     Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>.
     /// </exception>
-    public bool RemoveTags(TagComponent component, params string[] ids)
+    public bool RemoveTags(EntityUid uid, TagComponent component, params string[] ids)
     {
-        return RemoveTags(component, ids.AsEnumerable());
+        return RemoveTags(uid, component, ids.AsEnumerable());
     }
 
     /// <summary>
@@ -678,7 +678,7 @@ public bool RemoveTags(TagComponent component, params string[] ids)
     /// <exception cref="UnknownPrototypeException">
     ///     Thrown if one of the ids represents an unregistered <see cref="TagPrototype"/>.
     /// </exception>
-    public bool RemoveTags(TagComponent component, IEnumerable<string> ids)
+    public bool RemoveTags(EntityUid uid, TagComponent component, IEnumerable<string> ids)
     {
         var count = component.Tags.Count;
 
@@ -690,7 +690,7 @@ public bool RemoveTags(TagComponent component, IEnumerable<string> ids)
 
         if (component.Tags.Count < count)
         {
-            Dirty(component);
+            Dirty(uid, component);
             return true;
         }
 
diff --git a/Content.Shared/Teleportation/Systems/LinkedEntitySystem.cs b/Content.Shared/Teleportation/Systems/LinkedEntitySystem.cs
index bf2d087c761..35ce5665ddf 100644
--- a/Content.Shared/Teleportation/Systems/LinkedEntitySystem.cs
+++ b/Content.Shared/Teleportation/Systems/LinkedEntitySystem.cs
@@ -87,7 +87,7 @@ public bool OneWayLink(EntityUid source, EntityUid target, bool deleteOnEmptyLin
     /// <param name="secondLink">Resolve comp</param>
     /// <returns>Whether unlinking was successful (e.g. they both were actually linked to one another)</returns>
     public bool TryUnlink(EntityUid first, EntityUid second,
-        LinkedEntityComponent? firstLink=null, LinkedEntityComponent? secondLink=null)
+        LinkedEntityComponent? firstLink = null, LinkedEntityComponent? secondLink = null)
     {
         if (!Resolve(first, ref firstLink))
             return false;
@@ -101,8 +101,8 @@ public bool TryUnlink(EntityUid first, EntityUid second,
         _appearance.SetData(first, LinkedEntityVisuals.HasAnyLinks, firstLink.LinkedEntities.Any());
         _appearance.SetData(second, LinkedEntityVisuals.HasAnyLinks, secondLink.LinkedEntities.Any());
 
-        Dirty(firstLink);
-        Dirty(secondLink);
+        Dirty(first, firstLink);
+        Dirty(second, secondLink);
 
         if (firstLink.LinkedEntities.Count == 0 && firstLink.DeleteOnEmptyLinks)
             QueueDel(first);
diff --git a/Content.Shared/Traits/Assorted/Systems/LegsParalyzedSystem.cs b/Content.Shared/Traits/Assorted/Systems/LegsParalyzedSystem.cs
index 8ae0f251b86..4556d8ee991 100644
--- a/Content.Shared/Traits/Assorted/Systems/LegsParalyzedSystem.cs
+++ b/Content.Shared/Traits/Assorted/Systems/LegsParalyzedSystem.cs
@@ -17,7 +17,8 @@ public override void Initialize()
     {
         SubscribeLocalEvent<Components.LegsParalyzedComponent, ComponentStartup>(OnStartup);
         SubscribeLocalEvent<Components.LegsParalyzedComponent, ComponentShutdown>(OnShutdown);
-        SubscribeLocalEvent<Components.LegsParalyzedComponent, BuckleChangeEvent>(OnBuckleChange);
+        SubscribeLocalEvent<Components.LegsParalyzedComponent, BuckledEvent>(OnBuckled);
+        SubscribeLocalEvent<Components.LegsParalyzedComponent, UnbuckledEvent>(OnUnbuckled);
         SubscribeLocalEvent<Components.LegsParalyzedComponent, ThrowPushbackAttemptEvent>(OnThrowPushbackAttempt);
         SubscribeLocalEvent<Components.LegsParalyzedComponent, UpdateCanMoveEvent>(OnUpdateCanMoveEvent);
     }
@@ -34,25 +35,11 @@ private void OnShutdown(EntityUid uid, Components.LegsParalyzedComponent compone
         _bodySystem.UpdateMovementSpeed(uid);
     }
 
-    private void OnBuckleChange(EntityUid uid, Components.LegsParalyzedComponent component, ref BuckleChangeEvent args)
-    {
-        if (args.Buckling)
-        {
-            _standingSystem.Stand(args.BuckledEntity);
-        }
-        else
-        {
-            _standingSystem.Down(args.BuckledEntity);
-        }
-    }
+    private void OnBuckled(EntityUid uid, Components.LegsParalyzedComponent component, ref BuckledEvent args) => _standingSystem.Stand(uid);
 
-    private void OnUpdateCanMoveEvent(EntityUid uid, Components.LegsParalyzedComponent component, UpdateCanMoveEvent args)
-    {
-        args.Cancel();
-    }
+    private void OnUnbuckled(EntityUid uid, Components.LegsParalyzedComponent component, ref UnbuckledEvent args) => _standingSystem.Down(uid);
 
-    private void OnThrowPushbackAttempt(EntityUid uid, Components.LegsParalyzedComponent component, ThrowPushbackAttemptEvent args)
-    {
-        args.Cancel();
-    }
+    private void OnUpdateCanMoveEvent(EntityUid uid, Components.LegsParalyzedComponent component, UpdateCanMoveEvent args) => args.Cancel();
+
+    private void OnThrowPushbackAttempt(EntityUid uid, Components.LegsParalyzedComponent component, ThrowPushbackAttemptEvent args) => args.Cancel();
 }
diff --git a/Content.Shared/Traits/Assorted/Systems/SharedSingerSystem.cs b/Content.Shared/Traits/Assorted/Systems/SharedSingerSystem.cs
index 13270ae45d2..5bf4e0d3789 100644
--- a/Content.Shared/Traits/Assorted/Systems/SharedSingerSystem.cs
+++ b/Content.Shared/Traits/Assorted/Systems/SharedSingerSystem.cs
@@ -38,7 +38,7 @@ private void OnStartup(Entity<SingerComponent> ent, ref ComponentStartup args)
 
         var instrumentComp = EnsureInstrumentComp(ent);
         var defaultData = singer.InstrumentList[singer.DefaultInstrument];
-        _instrument.SetInstrumentProgram(instrumentComp, defaultData.Item1, defaultData.Item2);
+        _instrument.SetInstrumentProgram(ent.Owner, instrumentComp, defaultData.Item1, defaultData.Item2);
         SetUpSwappableInstrument(ent, singer);
     }
 
diff --git a/Content.Shared/Weapons/Marker/SharedDamageMarkerSystem.cs b/Content.Shared/Weapons/Marker/SharedDamageMarkerSystem.cs
index 63b2d5f2115..d1814020e6e 100644
--- a/Content.Shared/Weapons/Marker/SharedDamageMarkerSystem.cs
+++ b/Content.Shared/Weapons/Marker/SharedDamageMarkerSystem.cs
@@ -71,7 +71,7 @@ private void OnMarkerCollide(EntityUid uid, DamageMarkerOnCollideComponent compo
         marker.Marker = projectile.Weapon.Value;
         marker.EndTime = _timing.CurTime + component.Duration;
         component.Amount--;
-        Dirty(marker);
+        Dirty(args.OtherEntity, marker);
 
         if (_netManager.IsServer)
         {
@@ -81,7 +81,7 @@ private void OnMarkerCollide(EntityUid uid, DamageMarkerOnCollideComponent compo
             }
             else
             {
-                Dirty(component);
+                Dirty(uid, component);
             }
         }
     }
diff --git a/Content.Shared/Weapons/Misc/SharedTetherGunSystem.cs b/Content.Shared/Weapons/Misc/SharedTetherGunSystem.cs
index 3a950bcd29e..dd297422c37 100644
--- a/Content.Shared/Weapons/Misc/SharedTetherGunSystem.cs
+++ b/Content.Shared/Weapons/Misc/SharedTetherGunSystem.cs
@@ -21,17 +21,17 @@ namespace Content.Shared.Weapons.Misc;
 
 public abstract partial class SharedTetherGunSystem : EntitySystem
 {
-    [Dependency] private   readonly INetManager _netManager = default!;
-    [Dependency] private   readonly ActionBlockerSystem _blocker = default!;
-    [Dependency] private   readonly MobStateSystem _mob = default!;
-    [Dependency] private   readonly SharedAppearanceSystem _appearance = default!;
-    [Dependency] private   readonly SharedAudioSystem _audio = default!;
-    [Dependency] private   readonly SharedContainerSystem _container = default!;
-    [Dependency] private   readonly SharedJointSystem _joints = default!;
-    [Dependency] private   readonly SharedPhysicsSystem _physics = default!;
+    [Dependency] private readonly INetManager _netManager = default!;
+    [Dependency] private readonly ActionBlockerSystem _blocker = default!;
+    [Dependency] private readonly MobStateSystem _mob = default!;
+    [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+    [Dependency] private readonly SharedAudioSystem _audio = default!;
+    [Dependency] private readonly SharedContainerSystem _container = default!;
+    [Dependency] private readonly SharedJointSystem _joints = default!;
+    [Dependency] private readonly SharedPhysicsSystem _physics = default!;
     [Dependency] protected readonly SharedTransformSystem TransformSystem = default!;
-    [Dependency] private   readonly ThrowingSystem _throwing = default!;
-    [Dependency] private   readonly ThrownItemSystem _thrown = default!;
+    [Dependency] private readonly ThrowingSystem _throwing = default!;
+    [Dependency] private readonly ThrownItemSystem _thrown = default!;
 
     private const string TetherJoint = "tether";
 
@@ -282,7 +282,7 @@ protected virtual void StopTether(EntityUid gunUid, BaseForceGunComponent compon
         RemComp<TetheredComponent>(component.Tethered.Value);
         _blocker.UpdateCanMove(component.Tethered.Value);
         component.Tethered = null;
-        Dirty(component);
+        Dirty(gunUid, component);
     }
 
     [Serializable, NetSerializable]
diff --git a/Content.Shared/Weapons/Ranged/Systems/RechargeBasicEntityAmmoSystem.cs b/Content.Shared/Weapons/Ranged/Systems/RechargeBasicEntityAmmoSystem.cs
index b774c8ab450..9d6d5524001 100644
--- a/Content.Shared/Weapons/Ranged/Systems/RechargeBasicEntityAmmoSystem.cs
+++ b/Content.Shared/Weapons/Ranged/Systems/RechargeBasicEntityAmmoSystem.cs
@@ -49,19 +49,19 @@ public override void Update(float frameTime)
             if (ammo.Count == ammo.Capacity)
             {
                 recharge.NextCharge = null;
-                Dirty(recharge);
+                Dirty(uid, recharge);
                 continue;
             }
 
             recharge.NextCharge = recharge.NextCharge.Value + TimeSpan.FromSeconds(recharge.RechargeCooldown);
-            Dirty(recharge);
+            Dirty(uid, recharge);
         }
     }
 
     private void OnInit(EntityUid uid, RechargeBasicEntityAmmoComponent component, MapInitEvent args)
     {
         component.NextCharge = _timing.CurTime;
-        Dirty(component);
+        Dirty(uid, component);
     }
 
     private void OnExamined(EntityUid uid, RechargeBasicEntityAmmoComponent component, ExaminedEvent args)
@@ -86,7 +86,7 @@ public void Reset(EntityUid uid, RechargeBasicEntityAmmoComponent? recharge = nu
         if (recharge.NextCharge == null || recharge.NextCharge < _timing.CurTime)
         {
             recharge.NextCharge = _timing.CurTime + TimeSpan.FromSeconds(recharge.RechargeCooldown);
-            Dirty(recharge);
+            Dirty(uid, recharge);
         }
     }
 }
diff --git a/Content.Shared/Weapons/Ranged/Systems/RechargeCycleAmmoSystem.cs b/Content.Shared/Weapons/Ranged/Systems/RechargeCycleAmmoSystem.cs
index 136e9b59b2f..a014f8e5c74 100644
--- a/Content.Shared/Weapons/Ranged/Systems/RechargeCycleAmmoSystem.cs
+++ b/Content.Shared/Weapons/Ranged/Systems/RechargeCycleAmmoSystem.cs
@@ -25,7 +25,7 @@ private void OnRechargeCycled(EntityUid uid, RechargeCycleAmmoComponent componen
             return;
 
         _gun.UpdateBasicEntityAmmoCount(uid, basic.Count.Value + 1, basic);
-        Dirty(basic);
+        Dirty(uid, basic);
         args.Handled = true;
     }
 }
diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs
index b714acefbd1..af295cc83ff 100644
--- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs
+++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs
@@ -118,7 +118,7 @@ private void OnGunMelee(EntityUid uid, GunComponent component, MeleeHitEvent arg
         if (melee.NextAttack > component.NextFire)
         {
             component.NextFire = melee.NextAttack;
-            Dirty(component);
+            Dirty(uid, component);
         }
     }
 
diff --git a/Content.Shared/Weapons/Reflect/ReflectSystem.cs b/Content.Shared/Weapons/Reflect/ReflectSystem.cs
index 36dbedb4cb1..03ad97edff2 100644
--- a/Content.Shared/Weapons/Reflect/ReflectSystem.cs
+++ b/Content.Shared/Weapons/Reflect/ReflectSystem.cs
@@ -42,6 +42,9 @@ public sealed class ReflectSystem : EntitySystem
     [Dependency] private readonly StandingStateSystem _standing = default!;
     [Dependency] private readonly AlertsSystem _alerts = default!;
 
+    [ValidatePrototypeId<AlertPrototype>]
+    private const string DeflectingAlert = "Deflecting";
+
     public override void Initialize()
     {
         base.Initialize();
@@ -296,11 +299,11 @@ private void RefreshReflectUser(EntityUid user)
 
     private void EnableAlert(EntityUid alertee)
     {
-        _alerts.ShowAlert(alertee, AlertType.Deflecting);
+        _alerts.ShowAlert(alertee, DeflectingAlert);
     }
 
     private void DisableAlert(EntityUid alertee)
     {
-        _alerts.ClearAlert(alertee, AlertType.Deflecting);
+        _alerts.ClearAlert(alertee, DeflectingAlert);
     }
 }
diff --git a/Content.Shared/Weather/SharedWeatherSystem.cs b/Content.Shared/Weather/SharedWeatherSystem.cs
index 45a2afe7cd9..19671bd77b0 100644
--- a/Content.Shared/Weather/SharedWeatherSystem.cs
+++ b/Content.Shared/Weather/SharedWeatherSystem.cs
@@ -15,8 +15,8 @@ public abstract class SharedWeatherSystem : EntitySystem
     [Dependency] protected readonly IGameTiming Timing = default!;
     [Dependency] protected readonly IMapManager MapManager = default!;
     [Dependency] protected readonly IPrototypeManager ProtoMan = default!;
-    [Dependency] private   readonly ITileDefinitionManager _tileDefManager = default!;
-    [Dependency] private   readonly MetaDataSystem _metadata = default!;
+    [Dependency] private readonly ITileDefinitionManager _tileDefManager = default!;
+    [Dependency] private readonly MetaDataSystem _metadata = default!;
     [Dependency] private readonly SharedAudioSystem _audio = default!;
 
     private EntityQuery<BlockWeatherComponent> _blockQuery;
@@ -129,7 +129,7 @@ public override void Update(float frameTime)
                 // Shutting down
                 if (endTime != null && remainingTime < WeatherComponent.ShutdownTime)
                 {
-                    SetState(WeatherState.Ending, comp, weather, weatherProto);
+                    SetState(uid, WeatherState.Ending, comp, weather, weatherProto);
                 }
                 // Starting up
                 else
@@ -139,7 +139,7 @@ public override void Update(float frameTime)
 
                     if (elapsed < WeatherComponent.StartupTime)
                     {
-                        SetState(WeatherState.Starting, comp, weather, weatherProto);
+                        SetState(uid, WeatherState.Starting, comp, weather, weatherProto);
                     }
                 }
 
@@ -182,15 +182,15 @@ public void SetWeather(MapId mapId, WeatherPrototype? proto, TimeSpan? endTime)
         }
 
         if (proto != null)
-            StartWeather(weatherComp, proto, endTime);
+            StartWeather(mapUid, weatherComp, proto, endTime);
     }
 
     /// <summary>
     /// Run every tick when the weather is running.
     /// </summary>
-    protected virtual void Run(EntityUid uid, WeatherData weather, WeatherPrototype weatherProto, float frameTime) {}
+    protected virtual void Run(EntityUid uid, WeatherData weather, WeatherPrototype weatherProto, float frameTime) { }
 
-    protected void StartWeather(WeatherComponent component, WeatherPrototype weather, TimeSpan? endTime)
+    protected void StartWeather(EntityUid uid, WeatherComponent component, WeatherPrototype weather, TimeSpan? endTime)
     {
         if (component.Weather.ContainsKey(weather.ID))
             return;
@@ -202,7 +202,7 @@ protected void StartWeather(WeatherComponent component, WeatherPrototype weather
         };
 
         component.Weather.Add(weather.ID, data);
-        Dirty(component);
+        Dirty(uid, component);
     }
 
     protected virtual void EndWeather(EntityUid uid, WeatherComponent component, string proto)
@@ -216,13 +216,13 @@ protected virtual void EndWeather(EntityUid uid, WeatherComponent component, str
         Dirty(uid, component);
     }
 
-    protected virtual bool SetState(WeatherState state, WeatherComponent component, WeatherData weather, WeatherPrototype weatherProto)
+    protected virtual bool SetState(EntityUid uid, WeatherState state, WeatherComponent component, WeatherData weather, WeatherPrototype weatherProto)
     {
         if (weather.State.Equals(state))
             return false;
 
         weather.State = state;
-        Dirty(component);
+        Dirty(uid, component);
         return true;
     }
 
diff --git a/Content.Tests/Shared/Alert/AlertManagerTests.cs b/Content.Tests/Shared/Alert/AlertManagerTests.cs
index 2d5f6af5a7f..c57df63d5b7 100644
--- a/Content.Tests/Shared/Alert/AlertManagerTests.cs
+++ b/Content.Tests/Shared/Alert/AlertManagerTests.cs
@@ -1,6 +1,5 @@
 using System.IO;
 using Content.Client.Alerts;
-using Content.Server.Alert;
 using Content.Shared.Alert;
 using NUnit.Framework;
 using Robust.Shared.GameObjects;
@@ -45,15 +44,15 @@ public void TestAlertManager()
             prototypeManager.Initialize();
             prototypeManager.LoadFromStream(new StringReader(PROTOTYPES));
 
-            Assert.That(alertsSystem.TryGet(AlertType.LowPressure, out var lowPressure));
-            Assert.That(lowPressure.Icons[0], Is.EqualTo(new SpriteSpecifier.Texture(new ("/Textures/Interface/Alerts/Pressure/lowpressure.png"))));
-            Assert.That(alertsSystem.TryGet(AlertType.HighPressure, out var highPressure));
-            Assert.That(highPressure.Icons[0], Is.EqualTo(new SpriteSpecifier.Texture(new ("/Textures/Interface/Alerts/Pressure/highpressure.png"))));
+            Assert.That(alertsSystem.TryGet("LowPressure", out var lowPressure));
+            Assert.That(lowPressure!.Icons[0], Is.EqualTo(new SpriteSpecifier.Texture(new ("/Textures/Interface/Alerts/Pressure/lowpressure.png"))));
+            Assert.That(alertsSystem.TryGet("HighPressure", out var highPressure));
+            Assert.That(highPressure!.Icons[0], Is.EqualTo(new SpriteSpecifier.Texture(new ("/Textures/Interface/Alerts/Pressure/highpressure.png"))));
 
-            Assert.That(alertsSystem.TryGet(AlertType.LowPressure, out lowPressure));
-            Assert.That(lowPressure.Icons[0], Is.EqualTo(new SpriteSpecifier.Texture(new ("/Textures/Interface/Alerts/Pressure/lowpressure.png"))));
-            Assert.That(alertsSystem.TryGet(AlertType.HighPressure, out highPressure));
-            Assert.That(highPressure.Icons[0], Is.EqualTo(new SpriteSpecifier.Texture(new ("/Textures/Interface/Alerts/Pressure/highpressure.png"))));
+            Assert.That(alertsSystem.TryGet("LowPressure", out lowPressure));
+            Assert.That(lowPressure!.Icons[0], Is.EqualTo(new SpriteSpecifier.Texture(new ("/Textures/Interface/Alerts/Pressure/lowpressure.png"))));
+            Assert.That(alertsSystem.TryGet("HighPressure", out highPressure));
+            Assert.That(highPressure!.Icons[0], Is.EqualTo(new SpriteSpecifier.Texture(new ("/Textures/Interface/Alerts/Pressure/highpressure.png"))));
         }
     }
 }
diff --git a/Content.Tests/Shared/Alert/AlertOrderPrototypeTests.cs b/Content.Tests/Shared/Alert/AlertOrderPrototypeTests.cs
index 56f76d46a92..efcd59acbbb 100644
--- a/Content.Tests/Shared/Alert/AlertOrderPrototypeTests.cs
+++ b/Content.Tests/Shared/Alert/AlertOrderPrototypeTests.cs
@@ -85,24 +85,24 @@ public void TestAlertOrderPrototype()
             var alerts = prototypeManager.EnumeratePrototypes<AlertPrototype>();
 
             // ensure they sort according to our expected criteria
-            var expectedOrder = new List<AlertType>();
-            expectedOrder.Add(AlertType.Handcuffed);
-            expectedOrder.Add(AlertType.Ensnared);
-            expectedOrder.Add(AlertType.HighPressure);
+            var expectedOrder = new List<string>();
+            expectedOrder.Add("Handcuffed");
+            expectedOrder.Add("Ensnared");
+            expectedOrder.Add("HighPressure");
             // stuff with only category + same category ordered by enum value
-            expectedOrder.Add(AlertType.Peckish);
-            expectedOrder.Add(AlertType.Hot);
-            expectedOrder.Add(AlertType.Stun);
-            expectedOrder.Add(AlertType.LowPressure);
-            expectedOrder.Add(AlertType.Cold);
-            // stuff at end of list ordered by enum value
-            expectedOrder.Add(AlertType.Weightless);
-            expectedOrder.Add(AlertType.PilotingShuttle);
+            expectedOrder.Add("Peckish");
+            expectedOrder.Add("Hot");
+            expectedOrder.Add("Stun");
+            expectedOrder.Add("LowPressure");
+            expectedOrder.Add("Cold");
+            // stuff at end of list ordered by ID
+            expectedOrder.Add("PilotingShuttle");
+            expectedOrder.Add("Weightless");
 
             var actual = alerts.ToList();
             actual.Sort(alertOrder);
 
-            Assert.That(actual.Select(a => a.AlertType).ToList(), Is.EqualTo(expectedOrder));
+            Assert.That(actual.Select(a => a.ID).ToList(), Is.EqualTo(expectedOrder));
         }
     }
 }
diff --git a/Content.Tests/Shared/Alert/AlertPrototypeTests.cs b/Content.Tests/Shared/Alert/AlertPrototypeTests.cs
index d95acb8aff4..43ae98b452b 100644
--- a/Content.Tests/Shared/Alert/AlertPrototypeTests.cs
+++ b/Content.Tests/Shared/Alert/AlertPrototypeTests.cs
@@ -39,9 +39,9 @@ public void OneTimeSetUp()
         [Test]
         public void TestAlertKey()
         {
-            Assert.That(new AlertKey(AlertType.HumanHealth, null), Is.Not.EqualTo(AlertKey.ForCategory(AlertCategory.Health)));
-            Assert.That((new AlertKey(null, AlertCategory.Health)), Is.EqualTo(AlertKey.ForCategory(AlertCategory.Health)));
-            Assert.That((new AlertKey(AlertType.Buckled, AlertCategory.Health)), Is.EqualTo(AlertKey.ForCategory(AlertCategory.Health)));
+            Assert.That(new AlertKey("HumanHealth", null), Is.Not.EqualTo(AlertKey.ForCategory("Health")));
+            Assert.That((new AlertKey(null, "Health")), Is.EqualTo(AlertKey.ForCategory("Health")));
+            Assert.That((new AlertKey("Buckled", "Health")), Is.EqualTo(AlertKey.ForCategory("Health")));
         }
 
         [TestCase(0, "/Textures/Interface/Alerts/Human/human.rsi/human0.png")]
diff --git a/Resources/ConfigPresets/EinsteinEngines/default.toml b/Resources/ConfigPresets/EinsteinEngines/default.toml
index b5b8dbbf64e..ac50e57b9b6 100644
--- a/Resources/ConfigPresets/EinsteinEngines/default.toml
+++ b/Resources/ConfigPresets/EinsteinEngines/default.toml
@@ -41,8 +41,7 @@ limit = 10.0
 fork_id = "EinsteinEngines"
 
 [server]
-rules_file   = "Rules.txt"
-rules_header = "ui-rules-header"
+rules_file   = "DefaultRuleset"
 
 [ooc]
 enable_during_round = true
diff --git a/Resources/Locale/en-US/guidebook/guides.ftl b/Resources/Locale/en-US/guidebook/guides.ftl
index e2090d5af0a..ae564f16710 100644
--- a/Resources/Locale/en-US/guidebook/guides.ftl
+++ b/Resources/Locale/en-US/guidebook/guides.ftl
@@ -72,6 +72,61 @@ guide-entry-revolutionaries = Revolutionaries
 guide-entry-minor-antagonists = Minor Antagonists
 guide-entry-space-ninja = Space Ninja
 
+guide-entry-rules = Server Rules
+guide-entry-rules-core-only = Core Only Ruleset
+guide-entry-rules-lrp = Standard Ruleset
+guide-entry-rules-mrp = MRP Ruleset
+guide-entry-rules-role-types = Role Types
+guide-entry-rules-core = Core Rules
+guide-entry-rules-c1 = C1
+guide-entry-rules-c2 = C2
+guide-entry-rules-c3 = C3
+guide-entry-rules-c4 = C4
+guide-entry-rules-c5 = C5
+guide-entry-rules-c6 = C6
+guide-entry-rules-c7 = C7
+guide-entry-rules-c8 = C8
+guide-entry-rules-c9 = C9
+guide-entry-rules-c10 = C10
+guide-entry-rules-c11 = C11
+guide-entry-rules-c12 = C12
+guide-entry-rules-c13 = C13
+guide-entry-rules-c14 = C14
+guide-entry-rules-roleplay = Roleplay Rules
+guide-entry-rules-r1 = R1
+guide-entry-rules-r2 = R2
+guide-entry-rules-r3 = R3
+guide-entry-rules-r4 = R4
+guide-entry-rules-r5 = R5
+guide-entry-rules-r6 = R6
+guide-entry-rules-r7 = R7
+guide-entry-rules-r8 = R8
+guide-entry-rules-r9 = R9
+guide-entry-rules-r10 = R10
+guide-entry-rules-r11 = R11
+guide-entry-rules-r12 = R12
+guide-entry-rules-r13 = R13
+guide-entry-rules-r14 = R14
+guide-entry-rules-r15 = R15
+guide-entry-rules-silicon = Silicon Rules
+guide-entry-rules-s1 = S1
+guide-entry-rules-s2 = S2
+guide-entry-rules-s3 = S3
+guide-entry-rules-s4 = S4
+guide-entry-rules-s5 = S5
+guide-entry-rules-s6 = S6
+guide-entry-rules-s7 = S7
+guide-entry-rules-s8 = S8
+guide-entry-rules-s9 = S9
+guide-entry-rules-s10 = S10
+guide-entry-rules-space-law = Space Law
+guide-entry-rules-sl-crime-list = Crime List
+guide-entry-rules-sl-controlled-substances = Controlled Substances
+guide-entry-rules-sl-restricted-gear = Restricted Gear
+guide-entry-rules-sl-restricted-weapons = Restricted Weapons
+guide-entry-rules-ban-types = Ban Types
+guide-entry-rules-ban-durations = Ban Durations
+
 guide-entry-writing = Writing
 guide-entry-glossary = Glossary
 
diff --git a/Resources/Locale/en-US/info/rules.ftl b/Resources/Locale/en-US/info/rules.ftl
index 28791fd7ecb..8aa0b746b1d 100644
--- a/Resources/Locale/en-US/info/rules.ftl
+++ b/Resources/Locale/en-US/info/rules.ftl
@@ -3,3 +3,6 @@
 ui-rules-header = Official Server Rules
 ui-rules-accept = I have read and agree to follow the rules
 ui-rules-wait = The accept button will be enabled after {$time} seconds.
+
+ui-rules-button-home = Home
+ui-rules-button-back = Back
diff --git a/Resources/Prototypes/Actions/borgs.yml b/Resources/Prototypes/Actions/borgs.yml
index 6d35c69cf6a..950a7c81524 100644
--- a/Resources/Prototypes/Actions/borgs.yml
+++ b/Resources/Prototypes/Actions/borgs.yml
@@ -2,7 +2,7 @@
   id: ActionViewLaws
   name: View Laws
   description: View the laws that you must follow.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     itemIconStyle: NoItem
diff --git a/Resources/Prototypes/Actions/crit.yml b/Resources/Prototypes/Actions/crit.yml
index 705ee6ee6b3..bc843796c47 100644
--- a/Resources/Prototypes/Actions/crit.yml
+++ b/Resources/Prototypes/Actions/crit.yml
@@ -3,7 +3,7 @@
   id: ActionCritSuccumb
   name: Succumb
   description: Accept your fate.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     itemIconStyle: NoItem
@@ -18,7 +18,7 @@
   id: ActionCritFakeDeath
   name: Fake Death
   description: Pretend to take your final breath while staying alive.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     itemIconStyle: NoItem
@@ -34,7 +34,7 @@
   id: ActionCritLastWords
   name: Say Last Words
   description: Whisper your last words to anyone nearby, and then succumb to your fate. You only have 30 characters to work with.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     itemIconStyle: NoItem
diff --git a/Resources/Prototypes/Actions/diona.yml b/Resources/Prototypes/Actions/diona.yml
index 11db30386a5..695285a524d 100644
--- a/Resources/Prototypes/Actions/diona.yml
+++ b/Resources/Prototypes/Actions/diona.yml
@@ -2,7 +2,7 @@
   id: DionaGibAction
   name: Gib Yourself!
   description: Split apart into 3 nymphs.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon:
@@ -16,7 +16,7 @@
   id: DionaReformAction
   name: Reform
   description: Reform back into a whole Diona.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon:
diff --git a/Resources/Prototypes/Actions/internals.yml b/Resources/Prototypes/Actions/internals.yml
index dd83a45332f..ebc29d2ebf0 100644
--- a/Resources/Prototypes/Actions/internals.yml
+++ b/Resources/Prototypes/Actions/internals.yml
@@ -2,7 +2,7 @@
   id: ActionToggleInternals
   name: Toggle Internals
   description: Breathe from the equipped gas tank. Also requires equipped breath mask.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon:
diff --git a/Resources/Prototypes/Actions/mech.yml b/Resources/Prototypes/Actions/mech.yml
index 2005133a70b..69ec71ba641 100644
--- a/Resources/Prototypes/Actions/mech.yml
+++ b/Resources/Prototypes/Actions/mech.yml
@@ -2,7 +2,7 @@
   id: ActionMechCycleEquipment
   name: Cycle
   description: Cycles currently selected equipment
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     itemIconStyle: NoItem
@@ -16,7 +16,7 @@
   id: ActionMechOpenUI
   name: Control Panel
   description: Opens the control panel for the mech
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     itemIconStyle: NoItem
@@ -30,7 +30,7 @@
   id: ActionMechEject
   name: Eject
   description: Ejects the pilot from the mech
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     itemIconStyle: NoItem
diff --git a/Resources/Prototypes/Actions/misc.yml b/Resources/Prototypes/Actions/misc.yml
index 60fec699210..1a8d3458dde 100644
--- a/Resources/Prototypes/Actions/misc.yml
+++ b/Resources/Prototypes/Actions/misc.yml
@@ -2,7 +2,7 @@
   id: ActionCancelEscape
   name: Stop escaping
   description: Calm down and sit peacefuly in your carrier's inventory
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: Actions/escapeinventory.rsi/cancel-escape.png
diff --git a/Resources/Prototypes/Actions/ninja.yml b/Resources/Prototypes/Actions/ninja.yml
index 5fe6f23b276..51bfc33c494 100644
--- a/Resources/Prototypes/Actions/ninja.yml
+++ b/Resources/Prototypes/Actions/ninja.yml
@@ -3,7 +3,7 @@
   id: ActionToggleNinjaGloves
   name: Toggle ninja gloves
   description: Toggles all glove actions on left click. Includes your doorjack, draining power, stunning enemies, downloading research and calling in a threat.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     priority: -13
@@ -14,7 +14,7 @@
   id: ActionCreateThrowingStar
   name: Create throwing star
   description: Channels suit power into creating a throwing star that deals extra stamina damage.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     useDelay: 0.5
@@ -29,7 +29,7 @@
   id: ActionRecallKatana
   name: Recall katana
   description: Teleports the Energy Katana linked to this suit to its wearer, cost based on distance.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     useDelay: 1
@@ -44,7 +44,7 @@
   id: ActionNinjaEmp
   name: EM Burst
   description: Disable any nearby technology with an electro-magnetic pulse.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon:
@@ -58,7 +58,7 @@
   id: ActionTogglePhaseCloak
   name: Phase cloak
   description: Toggles your suit's phase cloak. Beware that if you are hit, all abilities are disabled for 5 seconds, including your cloak!
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     # have to plan (un)cloaking ahead of time
@@ -71,7 +71,7 @@
   id: ActionEnergyKatanaDash
   name: Katana dash
   description: Teleport to anywhere you can see, if your Energy Katana is in your hand.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: WorldTargetAction
     icon:
diff --git a/Resources/Prototypes/Actions/polymorph.yml b/Resources/Prototypes/Actions/polymorph.yml
index 445dc8d9f54..de082ead8a4 100644
--- a/Resources/Prototypes/Actions/polymorph.yml
+++ b/Resources/Prototypes/Actions/polymorph.yml
@@ -2,14 +2,14 @@
   id: ActionRevertPolymorph
   name: Revert
   description: Revert back into your original form.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     event: !type:RevertPolymorphActionEvent
 
 - type: entity
   id: ActionPolymorph
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     event: !type:PolymorphActionEvent
@@ -19,7 +19,7 @@
   id: ActionPolymorphWizardSpider
   name: Spider Polymorph
   description: Polymorphs you into a Spider.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     useDelay: 60
@@ -34,7 +34,7 @@
   id: ActionPolymorphWizardRod
   name: Rod Form
   description: CLANG!
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     useDelay: 60
diff --git a/Resources/Prototypes/Actions/psionics.yml b/Resources/Prototypes/Actions/psionics.yml
index a372a480ac7..c5df4f70ad0 100644
--- a/Resources/Prototypes/Actions/psionics.yml
+++ b/Resources/Prototypes/Actions/psionics.yml
@@ -2,7 +2,7 @@
   id: ActionDispel
   name: action-name-dispel
   description: action-description-dispel
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: EntityTargetAction
       icon: Interface/VerbIcons/dispel.png
@@ -21,7 +21,7 @@
   id: ActionMassSleep
   name: action-name-mass-sleep
   description: action-description-mass-sleep
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: WorldTargetAction
       icon: Interface/VerbIcons/mass_sleep.png
@@ -35,7 +35,7 @@
   id: ActionMindSwap
   name: action-name-mind-swap
   description: action-description-mind-swap
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: EntityTargetAction
       icon: Interface/VerbIcons/mind_swap.png
@@ -53,7 +53,7 @@
   id: ActionMindSwapReturn
   name: action-name-mind-swap-return
   description: action-description-mind-swap-return
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: InstantAction
       icon: Interface/VerbIcons/mind_swap_return.png
@@ -65,7 +65,7 @@
   id: ActionNoosphericZap
   name: action-name-noospheric-zap
   description: action-description-noospheric-zap
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: EntityTargetAction
       icon: Interface/VerbIcons/noospheric_zap.png
@@ -82,7 +82,7 @@
   id: ActionPyrokinesis
   name: action-name-pyrokinesis
   description: action-description-pyrokinesis
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: EntityTargetAction
       icon: Interface/VerbIcons/pyrokinesis.png
@@ -96,7 +96,7 @@
   id: ActionMetapsionic
   name: action-name-metapsionic
   description: action-description-metapsionic
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: InstantAction
       icon: Interface/VerbIcons/metapsionic.png
@@ -107,7 +107,7 @@
   id: ActionPsionicRegeneration
   name: action-name-psionic-regeneration
   description: action-description-psionic-regeneration
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: InstantAction
       icon: Interface/VerbIcons/psionic_regeneration.png
@@ -118,7 +118,7 @@
   id: ActionTelegnosis
   name: action-name-telegnosis
   description: action-description-telegnosis
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: InstantAction
       icon: Interface/VerbIcons/telegnosis.png
@@ -129,7 +129,7 @@
   id: ActionPsionicInvisibility
   name: action-name-psionic-invisibility
   description: action-description-psionic-invisibility
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: InstantAction
       icon: Interface/VerbIcons/psionic_invisibility.png
@@ -140,7 +140,7 @@
   id: ActionPsionicInvisibilityUsed
   name: action-name-psionic-invisibility-off
   description: action-description-psionic-invisibility-off
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: InstantAction
       icon: Interface/VerbIcons/psionic_invisibility_off.png
@@ -150,7 +150,7 @@
   id: ActionHealingWord
   name: action-name-healing-word
   description: action-description-healing-word
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: EntityTargetAction
       icon: { sprite: Interface/Actions/psionics.rsi, state: healing_word }
@@ -187,7 +187,7 @@
   id: ActionRevivify
   name: action-name-revivify
   description: action-description-revivify
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: EntityTargetAction
       icon: { sprite: Interface/Actions/psionics.rsi, state: revivify }
@@ -226,7 +226,7 @@
   id: ActionShadeskip
   name: action-name-shadeskip
   description: action-description-shadeskip
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: { sprite : Interface/Actions/psionics.rsi, state: shadeskip }
@@ -261,7 +261,7 @@
   id: ActionTelekineticPulse
   name: action-name-telekinetic-pulse
   description: action-description-telekinetic-pulse
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: InstantAction
       icon: { sprite: Interface/Actions/psionics.rsi, state: telekinetic_pulse }
@@ -295,7 +295,7 @@
   id: ActionShadowkinShadeskip
   name: action-name-shadeskip
   description: action-description-shadowkin-shadeskip
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: InstantAction
       icon: { sprite: Interface/Actions/shadowkin_icons.rsi, state: shadeskip }
@@ -329,7 +329,7 @@
   id: ActionDarkSwap
   name: action-name-darkswap
   description: action-description-darkswap
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: InstantAction
       icon: { sprite: Interface/Actions/shadowkin_icons.rsi, state: darkswap }
@@ -343,7 +343,7 @@
   id: ActionPyrokineticFlare
   name: action-name-pyrokinetic-flare
   description: action-description-pyrokinetic-flare
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: InstantAction
       icon: { sprite: Interface/Actions/psionics.rsi, state: pyrokinetic_flare }
@@ -369,7 +369,7 @@
   id: ActionSummonImp
   name: action-name-summon-imp
   description: action-description-summon-imp
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: InstantAction
       icon: { sprite: Interface/Actions/psionics.rsi, state: summon_imp }
@@ -388,7 +388,7 @@
   id: ActionSummonRemilia
   name: action-name-summon-remilia
   description: action-description-summon-remilia
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: InstantAction
       icon: { sprite: Interface/Actions/psionics.rsi, state: summon_remilia }
diff --git a/Resources/Prototypes/Actions/revenant.yml b/Resources/Prototypes/Actions/revenant.yml
index da7b4ba56f2..1131ae2eadb 100644
--- a/Resources/Prototypes/Actions/revenant.yml
+++ b/Resources/Prototypes/Actions/revenant.yml
@@ -2,7 +2,7 @@
   id: ActionRevenantShop
   name: Shop
   description: Opens the ability shop.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: Interface/Actions/shop.png
@@ -12,7 +12,7 @@
   id: ActionRevenantDefile
   name: Defile
   description: Costs 30 Essence.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: Interface/Actions/defile.png
@@ -23,7 +23,7 @@
   id: ActionRevenantOverloadLights
   name: Overload Lights
   description: Costs 40 Essence.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: Interface/Actions/overloadlight.png
@@ -34,7 +34,7 @@
 #  id: ActionRevenantBlight
 #  name: Blight
 #  description: Costs 50 Essence.
-#  noSpawn: true
+#  categories: [ HideSpawnMenu ]
 #  components:
 #  - type: InstantAction
 #    icon: Interface/Actions/blight.png
@@ -45,7 +45,7 @@
   id: ActionRevenantMalfunction
   name: Malfunction
   description: Costs 60 Essence.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: Interface/Actions/malfunction.png
diff --git a/Resources/Prototypes/Actions/speech.yml b/Resources/Prototypes/Actions/speech.yml
index 39db04b1b31..053322904f4 100644
--- a/Resources/Prototypes/Actions/speech.yml
+++ b/Resources/Prototypes/Actions/speech.yml
@@ -2,7 +2,7 @@
   id: ActionConfigureMeleeSpeech
   name: Set Battlecry
   description: Set a custom battlecry for when you attack!
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     itemIconStyle: BigItem
diff --git a/Resources/Prototypes/Actions/spider.yml b/Resources/Prototypes/Actions/spider.yml
index 14b9fb6ccbb..3139d416dc6 100644
--- a/Resources/Prototypes/Actions/spider.yml
+++ b/Resources/Prototypes/Actions/spider.yml
@@ -2,7 +2,7 @@
   id: ActionSpiderWeb
   name: Spider Web
   description: Spawns a web that slows your prey down.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: Interface/Actions/web.png
@@ -13,7 +13,7 @@
   id: ActionSericulture
   name: Weave silk
   description: Weave a bit of silk for use in arts and crafts.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: Interface/Actions/web.png
diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml
index a7144cdda57..f69d6a794a3 100644
--- a/Resources/Prototypes/Actions/types.yml
+++ b/Resources/Prototypes/Actions/types.yml
@@ -13,7 +13,7 @@
   id: ActionScream
   name: Scream
   description: AAAAAAAAAAAAAAAAAAAAAAAAA
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     useDelay: 10
@@ -25,7 +25,7 @@
   id: ActionTurnUndead
   name: Turn Undead
   description: Succumb to your infection and become a zombie.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     checkCanInteract: false
@@ -37,7 +37,7 @@
   id: ActionToggleLight
   name: Toggle Light
   description: Turn the light on and off.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: { sprite: Objects/Tools/flashlight.rsi, state: flashlight }
@@ -48,7 +48,7 @@
   id: ActionOpenStorageImplant
   name: Open Storage Implant
   description: Opens the storage implant embedded under your skin
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     itemIconStyle: BigAction
@@ -63,7 +63,7 @@
   id: ActionActivateMicroBomb
   name: Activate Microbomb
   description: Activates your internal microbomb, completely destroying you and your equipment
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     checkCanInteract: false
@@ -80,7 +80,7 @@
   id: ActionActivateDeathAcidifier
   name: Activate Death-Acidifier
   description: Activates your death-acidifier, completely melting you and your equipment
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     checkCanInteract: false
@@ -96,7 +96,7 @@
   id: ActionActivateFreedomImplant
   name: Break Free
   description: Activating your freedom implant will free you from any hand restraints
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     charges: 3
@@ -112,7 +112,7 @@
   id: ActionOpenUplinkImplant
   name: Open Uplink
   description: Opens the syndicate uplink embedded under your skin
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     itemIconStyle: BigAction
@@ -126,7 +126,7 @@
   id: ActionActivateEmpImplant
   name: Activate EMP
   description: Triggers a small EMP pulse around you
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     checkCanInteract: false
@@ -143,7 +143,7 @@
   id: ActionActivateScramImplant
   name: SCRAM!
   description: Randomly teleports you within a large distance.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     checkCanInteract: false
@@ -160,7 +160,7 @@
   id: ActionActivateDnaScramblerImplant
   name: Scramble DNA
   description:  Randomly changes your name and appearance.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     charges: 1
@@ -175,7 +175,7 @@
   id: ActionMorphGeras
   name: Morph into Geras
   description:  Morphs you into a Geras - a miniature version of you which allows you to move fast, at the cost of your inventory.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     itemIconStyle: BigAction
@@ -190,7 +190,7 @@
   id: ActionToggleSuitPiece
   name: Toggle Suit Piece
   description: Remember to equip the important pieces of your suit before going into action.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     itemIconStyle: BigItem
@@ -201,7 +201,7 @@
   id: ActionCombatModeToggle
   name: "[color=red]Combat Mode[/color]"
   description: Enter combat mode
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     checkCanInteract: false
@@ -216,7 +216,7 @@
   parent: ActionCombatModeToggle
   name: "[color=red]Combat Mode[/color]"
   description: Enter combat mode
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     enabled: false
@@ -227,7 +227,7 @@
   id: ActionChangeVoiceMask
   name: Set name
   description: Change the name others hear to something else.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: { sprite: Interface/Actions/voice-mask.rsi, state: icon }
@@ -237,7 +237,7 @@
   id: ActionVendingThrow
   name: Dispense Item
   description: Randomly dispense an item from your stock.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     useDelay: 30
@@ -247,7 +247,7 @@
   id: ActionArtifactActivate
   name: Activate Artifact
   description: Immediately activates your current artifact node.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon:
@@ -260,7 +260,7 @@
   id: ActionToggleBlock
   name: Block
   description: Raise or lower your shield.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: { sprite: Objects/Weapons/Melee/shields.rsi, state: teleriot-icon }
@@ -271,7 +271,7 @@
   id: ActionClearNetworkLinkOverlays
   name: Clear network link overlays
   description: Clear network link overlays.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     clientExclusive: true
@@ -285,7 +285,7 @@
   id: ActionAnimalLayEgg
   name: Lay egg
   description: Uses hunger to lay an egg.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: { sprite: Objects/Consumable/Food/egg.rsi, state: icon }
@@ -296,7 +296,7 @@
   id: ActionSleep
   name: Sleep
   description: Go to sleep.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     checkCanInteract: false
@@ -308,7 +308,7 @@
   id: ShadowkinActionSleep
   name: action-name-shadowkin-rest
   description: action-description-shadowkin-rest
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     checkCanInteract: false
@@ -320,7 +320,7 @@
   id: ActionWake
   name: Wake up
   description: Stop sleeping.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: { sprite: Clothing/Head/Hats/pyjamasyndicatered.rsi, state: icon }
@@ -332,7 +332,7 @@
   id: ActionActivateHonkImplant
   name: Honk
   description: Activates your honking implant, which will produce the signature sound of the clown.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: { sprite: Objects/Fun/bikehorn.rsi, state: icon }
@@ -343,7 +343,7 @@
   id: ActionFireStarter
   name: Ignite
   description: Ignites enemies in a radius around you.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     priority: -1
@@ -355,7 +355,7 @@
   id: ActionToggleEyes
   name: Open/Close eyes
   description: Close your eyes to protect your peepers, or open your eyes to enjoy the pretty lights.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: Interface/Actions/eyeopen.png
@@ -369,7 +369,7 @@
   id: ActionToggleWagging
   name: action-name-toggle-wagging
   description: action-description-toggle-wagging
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: InstantAction
       icon: { sprite: Interface/Actions/wagging.rsi, state: icon }
@@ -382,7 +382,7 @@
   id: ActionFabricateLollipop
   name: action-name-fabricate-lollipop
   description: action-description-fabricate-lollipop
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: { sprite: Nyanotrasen/Objects/Consumable/Food/candy.rsi, state: lollipop }
@@ -395,7 +395,7 @@
   id: ActionFabricateGumball
   name: action-name-fabricate-gumball
   description: action-description-fabricate-gumball
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: { sprite: Nyanotrasen/Objects/Consumable/Food/candy.rsi, state: gumball }
diff --git a/Resources/Prototypes/Alerts/categories.yml b/Resources/Prototypes/Alerts/categories.yml
new file mode 100644
index 00000000000..1e79d2615bb
--- /dev/null
+++ b/Resources/Prototypes/Alerts/categories.yml
@@ -0,0 +1,38 @@
+- type: alertCategory
+  id: Pressure
+
+- type: alertCategory
+  id: Temperature
+
+- type: alertCategory
+  id: Breathing
+
+- type: alertCategory
+  id: Buckled
+
+- type: alertCategory
+  id: Health
+
+- type: alertCategory
+  id: Internals
+
+- type: alertCategory
+  id: Stamina
+
+- type: alertCategory
+  id: Piloting
+
+- type: alertCategory
+  id: Hunger
+
+- type: alertCategory
+  id: Thirst
+
+- type: alertCategory
+  id: Toxins
+
+- type: alertCategory
+  id: Battery
+
+- type: alertCategory
+  id: Mood
diff --git a/Resources/Prototypes/Body/Organs/Animal/animal.yml b/Resources/Prototypes/Body/Organs/Animal/animal.yml
index 2f50821df35..44369ffe73c 100644
--- a/Resources/Prototypes/Body/Organs/Animal/animal.yml
+++ b/Resources/Prototypes/Body/Organs/Animal/animal.yml
@@ -34,7 +34,7 @@
   id: OrganAnimalLungs
   parent: BaseAnimalOrgan
   name: lungs
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     layers:
@@ -65,7 +65,7 @@
   id: OrganAnimalStomach
   parent: BaseAnimalOrgan
   name: stomach
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     state: stomach
@@ -91,7 +91,7 @@
   id: OrganMouseStomach
   parent: OrganAnimalStomach
   name: stomach
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: SolutionContainerManager
     solutions:
@@ -102,7 +102,7 @@
   id: OrganAnimalLiver
   parent: BaseAnimalOrgan
   name: liver
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     state: liver
@@ -118,7 +118,7 @@
   id: OrganAnimalHeart
   parent: BaseAnimalOrgan
   name: heart
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     state: heart-on
@@ -135,7 +135,7 @@
   id: OrganAnimalKidneys
   parent: BaseAnimalOrgan
   name: kidneys
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     layers:
diff --git a/Resources/Prototypes/Body/Organs/Animal/bloodsucker.yml b/Resources/Prototypes/Body/Organs/Animal/bloodsucker.yml
index 8a1afc37bb1..10cf620addc 100644
--- a/Resources/Prototypes/Body/Organs/Animal/bloodsucker.yml
+++ b/Resources/Prototypes/Body/Organs/Animal/bloodsucker.yml
@@ -2,7 +2,7 @@
   id: OrganBloodsuckerStomach
   parent: OrganAnimalStomach
   name: stomach
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Metabolizer
     metabolizerTypes: [ Bloodsucker ]
@@ -11,7 +11,7 @@
   id: OrganBloodsuckerLiver
   parent: OrganAnimalLiver
   name: liver
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Metabolizer
     metabolizerTypes: [ Bloodsucker ]
@@ -20,7 +20,7 @@
   id: OrganBloodsuckerHeart
   parent: OrganAnimalHeart
   name: heart
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Metabolizer
     metabolizerTypes: [ Bloodsucker ]
diff --git a/Resources/Prototypes/Body/Organs/Animal/ruminant.yml b/Resources/Prototypes/Body/Organs/Animal/ruminant.yml
index 3c3062ddec0..6bd7c2d4024 100644
--- a/Resources/Prototypes/Body/Organs/Animal/ruminant.yml
+++ b/Resources/Prototypes/Body/Organs/Animal/ruminant.yml
@@ -2,7 +2,7 @@
   id: OrganAnimalRuminantStomach
   parent: OrganAnimalStomach
   name: ruminant stomach
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: SolutionContainerManager
     solutions:
diff --git a/Resources/Prototypes/Body/Organs/Friendstomach.yml b/Resources/Prototypes/Body/Organs/Friendstomach.yml
index a20bbbe75bc..8dc992b08fb 100644
--- a/Resources/Prototypes/Body/Organs/Friendstomach.yml
+++ b/Resources/Prototypes/Body/Organs/Friendstomach.yml
@@ -1,7 +1,7 @@
 - type: entity
   id: OrganFriendStomach
   parent: OrganAnimalStomach
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Stomach
   - type: SolutionContainerManager
diff --git a/Resources/Prototypes/Body/Organs/arachnid.yml b/Resources/Prototypes/Body/Organs/arachnid.yml
index c1e199e1121..29ca393d137 100644
--- a/Resources/Prototypes/Body/Organs/arachnid.yml
+++ b/Resources/Prototypes/Body/Organs/arachnid.yml
@@ -105,7 +105,7 @@
   parent: BaseHumanOrgan
   name: liver
   description: "Pairing suggestion: chianti and fava beans."
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     state: liver
@@ -122,7 +122,7 @@
   parent: BaseHumanOrgan
   name: kidneys
   description: "Filters toxins from the bloodstream."
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     layers:
diff --git a/Resources/Prototypes/Body/Organs/diona.yml b/Resources/Prototypes/Body/Organs/diona.yml
index a2133f7f90a..6442ff3ee22 100644
--- a/Resources/Prototypes/Body/Organs/diona.yml
+++ b/Resources/Prototypes/Body/Organs/diona.yml
@@ -127,7 +127,7 @@
 - type: entity
   id: OrganDionaBrainNymph
   parent: OrganDionaBrain
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: brain
   description: "The source of incredible, unending intelligence. Honk."
   components:
@@ -139,7 +139,7 @@
 - type: entity
   id: OrganDionaStomachNymph
   parent: OrganDionaStomach
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: stomach
   description: "Gross. This is hard to stomach."
   components:
@@ -149,7 +149,7 @@
 - type: entity
   id: OrganDionaLungsNymph
   parent: OrganDionaLungs
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: lungs
   description: "Filters oxygen from an atmosphere, which is then sent into the bloodstream to be used as an electron carrier."
   components:
@@ -160,7 +160,7 @@
 - type: entity
   id: OrganDionaNymphBrain
   parent: MobDionaNymph
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: diona nymph
   suffix: Brain
   description: Contains the brain of a formerly fully-formed Diona. Killing this would kill the Diona forever. You monster.
@@ -172,7 +172,7 @@
 - type: entity
   id: OrganDionaNymphStomach
   parent: MobDionaNymphAccent
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: diona nymph
   suffix: Stomach
   description: Contains the stomach of a formerly fully-formed Diona. It doesn't taste any better for it.
@@ -184,7 +184,7 @@
 - type: entity
   id: OrganDionaNymphLungs
   parent: MobDionaNymphAccent
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: diona nymph
   suffix: Lungs
   description: Contains the lungs of a formerly fully-formed Diona. Breathtaking.
diff --git a/Resources/Prototypes/Body/Organs/moth.yml b/Resources/Prototypes/Body/Organs/moth.yml
index 9b94e77b7eb..d1c9c164f02 100644
--- a/Resources/Prototypes/Body/Organs/moth.yml
+++ b/Resources/Prototypes/Body/Organs/moth.yml
@@ -1,7 +1,7 @@
 - type: entity
   id: OrganMothStomach
   parent: [OrganAnimalStomach, OrganHumanStomach]
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Stomach
     specialDigestible:
diff --git a/Resources/Prototypes/Body/Organs/reptilian.yml b/Resources/Prototypes/Body/Organs/reptilian.yml
index f8423582cc2..34c736aec8b 100644
--- a/Resources/Prototypes/Body/Organs/reptilian.yml
+++ b/Resources/Prototypes/Body/Organs/reptilian.yml
@@ -1,7 +1,7 @@
 - type: entity
   id: OrganReptilianStomach
   parent: OrganAnimalStomach
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Stomach
     specialDigestible:
diff --git a/Resources/Prototypes/Body/Parts/animal.yml b/Resources/Prototypes/Body/Parts/animal.yml
index 76eaf79812f..abd34c0ef5a 100644
--- a/Resources/Prototypes/Body/Parts/animal.yml
+++ b/Resources/Prototypes/Body/Parts/animal.yml
@@ -36,7 +36,7 @@
   id: HandsAnimal
   name: animal hands
   parent: PartAnimal
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     layers:
@@ -50,7 +50,7 @@
   id: LegsAnimal
   name: animal legs
   parent: PartAnimal
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     layers:
@@ -64,7 +64,7 @@
   id: FeetAnimal
   name: animal feet
   parent: PartAnimal
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     layers:
@@ -77,7 +77,7 @@
   id: TorsoAnimal
   name: animal torso
   parent: PartAnimal
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     layers:
diff --git a/Resources/Prototypes/Body/Parts/rat.yml b/Resources/Prototypes/Body/Parts/rat.yml
index 6a66eecc489..bd51e006f70 100644
--- a/Resources/Prototypes/Body/Parts/rat.yml
+++ b/Resources/Prototypes/Body/Parts/rat.yml
@@ -4,7 +4,7 @@
   id: TorsoRat
   name: "animal torso"
   parent: PartAnimal
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: BodyPart
     partType: Torso
diff --git a/Resources/Prototypes/Body/Parts/silicon.yml b/Resources/Prototypes/Body/Parts/silicon.yml
index 24d88276ccb..57cd1f02cf4 100644
--- a/Resources/Prototypes/Body/Parts/silicon.yml
+++ b/Resources/Prototypes/Body/Parts/silicon.yml
@@ -105,4 +105,4 @@
     partType: Torso
   - type: Tag
     tags:
-    - Trash
+    - Trash
\ No newline at end of file
diff --git a/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/backpack.yml b/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/backpack.yml
index da46ae75979..d66fb5d38b1 100644
--- a/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/backpack.yml
+++ b/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/backpack.yml
@@ -1,10 +1,10 @@
 - type: entity
   parent: ClothingBackpack
   id: ClothingBackpackFilled
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackClown
   id: ClothingBackpackClownFilled
   components:
@@ -15,7 +15,7 @@
       - id: CrayonRainbow
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSecurity
   id: ClothingBackpackSecurityFilled
   components:
@@ -24,7 +24,7 @@
       - id: MagazinePistol
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackBrigmedic
   id: ClothingBackpackBrigmedicFilled
   components:
@@ -40,7 +40,7 @@
       - id: MagazinePistol
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSecurity
   id: ClothingBackpackSecurityFilledDetective
   components:
@@ -52,12 +52,12 @@
 
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackMedical
   id: ClothingBackpackMedicalFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackMedical
   id: ClothingBackpackParamedicFilled
   components:
@@ -66,7 +66,7 @@
       - id: EmergencyRollerBedSpawnFolded
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackCaptain
   id: ClothingBackpackCaptainFilled
   components:
@@ -76,7 +76,7 @@
       #- name: StationCharter
       #- name: TelescopicBaton
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackEngineering
   id: ClothingBackpackChiefEngineerFilled
   components:
@@ -86,7 +86,7 @@
       #- id: TelescopicBaton
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackScience
   id: ClothingBackpackResearchDirectorFilled
   components:
@@ -96,7 +96,7 @@
       #- id: TelescopicBaton
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpack
   id: ClothingBackpackHOPFilled
   components:
@@ -106,7 +106,7 @@
       #- id: TelescopicBaton
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackMedical
   id: ClothingBackpackCMOFilled
   components:
@@ -116,7 +116,7 @@
       #- id: TelescopicBaton
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackCargo
   id: ClothingBackpackQuartermasterFilled
   components:
@@ -126,7 +126,7 @@
       #- id: TelescopicBaton
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSecurity
   id: ClothingBackpackHOSFilled
   components:
@@ -136,32 +136,32 @@
       - id: MagazinePistol
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackEngineering
   id: ClothingBackpackEngineeringFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackAtmospherics
   id: ClothingBackpackAtmosphericsFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackScience
   id: ClothingBackpackScienceFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackRobotics
   id: ClothingBackpackRoboticsFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackHydroponics
   id: ClothingBackpackHydroponicsFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackMime
   id: ClothingBackpackMimeFilled
   components:
@@ -170,22 +170,22 @@
       - id: RubberStampMime
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackChemistry
   id: ClothingBackpackChemistryFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpack
   id: ClothingBackpackChaplainFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpack
   id: ClothingBackpackMusicianFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpack
   id: ClothingBackpackLibrarianFilled
   components:
@@ -194,7 +194,7 @@
         - id: BookRandom
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpack
   id: ClothingBackpackDetectiveFilled
   components:
@@ -208,7 +208,7 @@
 # ERT
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackERTLeader
   id: ClothingBackpackERTLeaderFilled
   components:
@@ -223,7 +223,7 @@
         - id: MagazineMagnum
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackERTSecurity
   id: ClothingBackpackERTSecurityFilled
   components:
@@ -238,7 +238,7 @@
         - id: MagazinePistol
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackERTMedical
   id: ClothingBackpackERTMedicalFilled
   components:
@@ -253,7 +253,7 @@
         - id: EpinephrineChemistryBottle
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackERTEngineer
   id: ClothingBackpackERTEngineerFilled
   components:
@@ -272,7 +272,7 @@
         - id: SheetGlass
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackERTJanitor
   id: ClothingBackpackERTJanitorFilled
   components:
@@ -287,7 +287,7 @@
         - id: AdvMopItem
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackERTChaplain
   id: ClothingBackpackERTChaplainFilled
   components:
@@ -310,7 +310,6 @@
 # Death Squad
 
 - type: entity
-  noSpawn: false
   parent: ClothingBackpackERTSecurity
   id: ClothingBackpackDeathSquadFilled
   name: death squad backpack
@@ -337,12 +336,12 @@
 # Cargo
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackCargo
   id: ClothingBackpackCargoFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSalvage
   id: ClothingBackpackSalvageFilled
 
diff --git a/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/duffelbag.yml b/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/duffelbag.yml
index 07cbbeb6caa..525a4a13cec 100644
--- a/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/duffelbag.yml
+++ b/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/duffelbag.yml
@@ -1,10 +1,10 @@
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffel
   id: ClothingBackpackDuffelFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelClown
   id: ClothingBackpackDuffelClownFilled
   components:
@@ -13,7 +13,7 @@
       - id: RubberStampClown
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelBrigmedic
   id: ClothingBackpackDuffelBrigmedicFilled
   components:
@@ -29,7 +29,7 @@
         amount: 2
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelSecurity
   id: ClothingBackpackDuffelSecurityFilled
   components:
@@ -38,7 +38,7 @@
       - id: MagazinePistol
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelSecurity
   id: ClothingBackpackDuffelSecurityFilledDetective
   components:
@@ -48,12 +48,12 @@
       - id: ForensicScanner
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelMedical
   id: ClothingBackpackDuffelMedicalFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelMedical
   id: ClothingBackpackDuffelParamedicFilled
   components:
@@ -62,7 +62,7 @@
       - id: EmergencyRollerBedSpawnFolded
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelCaptain
   id: ClothingBackpackDuffelCaptainFilled
   components:
@@ -73,7 +73,7 @@
       #- name: TelescopicBaton
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelEngineering
   id: ClothingBackpackDuffelChiefEngineerFilled
   components:
@@ -83,7 +83,7 @@
       #- id: TelescopicBaton
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelScience
   id: ClothingBackpackDuffelResearchDirectorFilled
   components:
@@ -93,7 +93,7 @@
       #- id: TelescopicBaton
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffel
   id: ClothingBackpackDuffelHOPFilled
   components:
@@ -103,7 +103,7 @@
       #- id: TelescopicBaton
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelMedical
   id: ClothingBackpackDuffelCMOFilled
   components:
@@ -113,7 +113,7 @@
       #- id: TelescopicBaton
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelCargo
   id: ClothingBackpackDuffelQuartermasterFilled
   components:
@@ -123,7 +123,7 @@
       #- id: TelescopicBaton
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelSecurity
   id: ClothingBackpackDuffelHOSFilled
   components:
@@ -133,32 +133,32 @@
       - id: MagazinePistol
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelEngineering
   id: ClothingBackpackDuffelEngineeringFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelAtmospherics
   id: ClothingBackpackDuffelAtmosphericsFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelScience
   id: ClothingBackpackDuffelScienceFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelRobotics
   id: ClothingBackpackDuffelRoboticsFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelHydroponics
   id: ClothingBackpackDuffelHydroponicsFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelMime
   id: ClothingBackpackDuffelMimeFilled
   components:
@@ -167,12 +167,12 @@
       - id: RubberStampMime
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelChemistry
   id: ClothingBackpackDuffelChemistryFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffel
   id: ClothingBackpackDuffelChaplainFilled
   components:
@@ -182,7 +182,7 @@
       - id: RubberStampChaplain
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffel
   id: ClothingBackpackDuffelMusicianFilled
   components:
@@ -192,7 +192,7 @@
         - id: SaxophoneInstrument
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffel
   id: ClothingBackpackDuffelLibrarianFilled
   components:
@@ -201,7 +201,7 @@
         - id: BookRandom
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffel
   id: ClothingBackpackDuffelDetectiveFilled
   components:
@@ -213,11 +213,11 @@
       - id: HandLabeler
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelCargo
   id: ClothingBackpackDuffelCargoFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelSalvage
   id: ClothingBackpackDuffelSalvageFilled
diff --git a/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/satchel.yml b/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/satchel.yml
index e20e27e55ca..7854b34c8e7 100644
--- a/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/satchel.yml
+++ b/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/satchel.yml
@@ -1,10 +1,10 @@
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchel
   id: ClothingBackpackSatchelFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchel
   id: ClothingBackpackSatchelTools
   components:
@@ -27,7 +27,7 @@
       - id: RubberStampClown
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelBrigmedic
   id: ClothingBackpackSatchelBrigmedicFilled
   components:
@@ -42,7 +42,7 @@
         amount: 2
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelSecurity
   id: ClothingBackpackSatchelSecurityFilled
   components:
@@ -51,7 +51,7 @@
       - id: MagazinePistol
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelSecurity
   id: ClothingBackpackSatchelSecurityFilledDetective
   components:
@@ -61,12 +61,12 @@
       - id: ForensicScanner
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelMedical
   id: ClothingBackpackSatchelMedicalFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelMedical
   id: ClothingBackpackSatchelParamedicFilled
   components:
@@ -75,7 +75,7 @@
       - id: EmergencyRollerBedSpawnFolded
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelCaptain
   id: ClothingBackpackSatchelCaptainFilled
   components:
@@ -86,7 +86,7 @@
       #- name: TelescopicBaton
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelEngineering
   id: ClothingBackpackSatchelChiefEngineerFilled
   components:
@@ -96,7 +96,7 @@
       #- id: TelescopicBaton
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelScience
   id: ClothingBackpackSatchelResearchDirectorFilled
   components:
@@ -106,7 +106,7 @@
       #- id: TelescopicBaton
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchel
   id: ClothingBackpackSatchelHOPFilled
   components:
@@ -116,7 +116,7 @@
       #- id: TelescopicBaton
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelMedical
   id: ClothingBackpackSatchelCMOFilled
   components:
@@ -126,7 +126,7 @@
       #- id: TelescopicBaton
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelCargo
   id: ClothingBackpackSatchelQuartermasterFilled
   components:
@@ -136,7 +136,7 @@
       #- id: TelescopicBaton
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelSecurity
   id: ClothingBackpackSatchelHOSFilled
   components:
@@ -146,37 +146,37 @@
       - id: MagazinePistol
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelEngineering
   id: ClothingBackpackSatchelEngineeringFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelAtmospherics
   id: ClothingBackpackSatchelAtmosphericsFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelScience
   id: ClothingBackpackSatchelScienceFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelRobotics
   id: ClothingBackpackSatchelRoboticsFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelHydroponics
   id: ClothingBackpackSatchelHydroponicsFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelChemistry
   id: ClothingBackpackSatchelChemistryFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchel
   id: ClothingBackpackSatchelChaplainFilled
   components:
@@ -186,7 +186,7 @@
       - id: RubberStampChaplain
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchel
   id: ClothingBackpackSatchelMusicianFilled
   components:
@@ -196,7 +196,7 @@
         - id: SaxophoneInstrument
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchel
   id: ClothingBackpackSatchelLibrarianFilled
   components:
@@ -205,7 +205,7 @@
         - id: BookRandom
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchel
   id: ClothingBackpackSatchelDetectiveFilled
   components:
@@ -217,17 +217,17 @@
       - id: HandLabeler
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelCargo
   id: ClothingBackpackSatchelCargoFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelSalvage
   id: ClothingBackpackSatchelSalvageFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelMime
   id: ClothingBackpackSatchelMimeFilled
   components:
@@ -236,7 +236,7 @@
       - id: RubberStampMime
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelHolding
   id: ClothingBackpackSatchelHoldingAdmin
   components:
diff --git a/Resources/Prototypes/Catalog/Fills/Crates/fun.yml b/Resources/Prototypes/Catalog/Fills/Crates/fun.yml
index a6f12bc727e..4805651fda9 100644
--- a/Resources/Prototypes/Catalog/Fills/Crates/fun.yml
+++ b/Resources/Prototypes/Catalog/Fills/Crates/fun.yml
@@ -1,3 +1,101 @@
+- type: entityTable
+  id: AllPlushiesTable
+  table: !type:GroupSelector
+    children:
+    - !type:EntSelector
+      id: PlushieBee
+    - !type:EntSelector
+      id: PlushieNar
+      weight: 0.5
+    - !type:EntSelector
+      id: PlushieRatvar
+      weight: 0.5
+    - !type:EntSelector
+      id: PlushieNuke
+    - !type:EntSelector
+      id: PlushieSlime
+    - !type:EntSelector
+      id: PlushieSnake
+    - !type:GroupSelector
+      children:
+      - !type:EntSelector
+        id: PlushieLizard
+        weight: 9
+      - !type:EntSelector
+        id: PlushieSpaceLizard
+        weight: 1
+    - !type:GroupSelector
+      children:
+      - !type:EntSelector
+        id: PlushieCarp
+      - !type:EntSelector
+        id: PlushieHolocarp
+        weight: 0.25
+      - !type:EntSelector
+        id: PlushieMagicarp
+        weight: 0.25
+      - !type:EntSelector
+        id: PlushieRainbowCarp
+        weight: 0.15
+    - !type:EntSelector
+      id: PlushieVox
+    - !type:EntSelector
+      id: PlushieRouny
+    - !type:GroupSelector
+      children:
+      - !type:EntSelector
+        id: PlushieSharkBlue
+      - !type:EntSelector
+        id: PlushieSharkGrey
+      - !type:EntSelector
+        id: PlushieSharkPink
+    - !type:EntSelector
+      id: PlushieAtmosian
+    - !type:EntSelector
+      id: PlushieDiona
+    - !type:EntSelector
+      id: PlushieXeno
+    - !type:EntSelector
+      id: PlushieHampter
+    - !type:EntSelector
+      id: PlushieMoth
+    - !type:EntSelector
+      id: PlushieArachind
+    - !type:EntSelector
+      id: PlushiePenguin
+
+- type: entity
+  id: CrateFunPlushie
+  parent: CrateGenericSteel
+  name: plushie crate
+  description: A buncha soft plushies. Throw them around and then wonder how you're gonna explain this purchase to NT.
+  components:
+  - type: EntityTableContainerFill
+    containers:
+      entity_storage: !type:NestedSelector
+        tableId: AllPlushiesTable
+        rolls: !type:ConstantNumberSelector
+          value: 10
+
+- type: entity
+  id: CrateFunLizardPlushieBulk
+  parent: CrateGenericSteel
+  name: bulk lizard plushie crate
+  description: A buncha soft lizard plushies. Throw them around and then wonder how you're gonna explain this purchase to NT.
+  components:
+  - type: EntityTableContainerFill
+    containers:
+      entity_storage: !type:AllSelector
+        children:
+        - !type:EntSelector
+          id: PlushieLizard
+          amount: !type:ConstantNumberSelector
+            value: 3
+        - !type:EntSelector
+          id: PlushieSpaceLizard
+          amount: !type:ConstantNumberSelector
+            value: 3
+
 - type: entity
   id: CrateFunInstrumentsVariety
   parent: CrateGenericSteel
diff --git a/Resources/Prototypes/Catalog/Fills/Crates/salvage.yml b/Resources/Prototypes/Catalog/Fills/Crates/salvage.yml
index c1168f68d42..7313de7700b 100644
--- a/Resources/Prototypes/Catalog/Fills/Crates/salvage.yml
+++ b/Resources/Prototypes/Catalog/Fills/Crates/salvage.yml
@@ -28,7 +28,7 @@
 - type: entity
   id: CrateSalvageAssortedGoodies
   suffix: Filled, Salvage Random
-  noSpawn: true # You should use SalvageMaterialCrateSpawner instead
+  categories: [ HideSpawnMenu ] # You should use SalvageMaterialCrateSpawner instead
   parent: CrateGenericSteel
   components:
   - type: StorageFill
diff --git a/Resources/Prototypes/Catalog/Fills/Lockers/misc.yml b/Resources/Prototypes/Catalog/Fills/Lockers/misc.yml
index feaedd924a0..052ce95be2f 100644
--- a/Resources/Prototypes/Catalog/Fills/Lockers/misc.yml
+++ b/Resources/Prototypes/Catalog/Fills/Lockers/misc.yml
@@ -3,323 +3,236 @@
   suffix: Filled
   parent: LockerSyndicatePersonal
   components:
-  - type: StorageFill
-    contents:
-      - id: ClothingBeltMilitaryWebbing
-      - id: ClothingHandsGlovesCombat
-      - id: JetpackBlackFilled
-      - id: ClothingUniformJumpsuitOperative
-      - id: ClothingUniformJumpskirtOperative
-      - id: ClothingHeadsetAltSyndicate
-      - id: ClothingEyesHudSyndicate
+  - type: EntityTableContainerFill
+    containers:
+      entity_storage: !type:AllSelector
+        children:
+        - !type:EntSelector
+          id: ClothingBeltMilitaryWebbing
+        - !type:EntSelector
+          id: ClothingHandsGlovesCombat
+        - !type:EntSelector
+          id: JetpackBlackFilled
+        - !type:EntSelector
+          id: ClothingUniformJumpsuitOperative
+        - !type:EntSelector
+          id: ClothingUniformJumpskirtOperative
+        - !type:EntSelector
+          id: ClothingHeadsetAltSyndicate
+        - !type:EntSelector
+          id: ClothingEyesHudSyndicate
+
+- type: entityTable
+  id: FillLockerEmergencyStandard
+  table: !type:AllSelector
+    children:
+    - !type:EntSelector
+      id: ClothingMaskBreath
+    - !type:EntSelector
+      id: ClothingOuterSuitEmergency
+    - !type:GroupSelector
+      children:
+      - !type:EntSelector
+        id: EmergencyOxygenTankFilled
+      - !type:EntSelector
+        id: OxygenTankFilled
+    - !type:EntSelector
+      id: ToolboxEmergencyFilled
+      prob: 0.5
+    - !type:EntSelector
+      id: MedkitOxygenFilled
+      prob: 0.2
+    - !type:EntSelector
+      id: WeaponFlareGun
+      prob: 0.05
+    - !type:EntSelector
+      id: BoxMRE
+      prob: 0.1
 
 - type: entity
   id: ClosetEmergencyFilledRandom
   parent: ClosetEmergency
   suffix: Filled, Random
   components:
-  - type: StorageFill
-    contents:
-      - id: ClothingOuterSuitEmergency
-      - id: ClothingMaskBreath
-      - id: EmergencyOxygenTankFilled
-        prob: 0.80
-        orGroup: EmergencyTankOrRegularTank
-      - id: OxygenTankFilled
-        prob: 0.20
-        orGroup: EmergencyTankOrRegularTank
-      - id: ToolboxEmergencyFilled
-        prob: 0.4
-      - id: MedkitOxygenFilled
-        prob: 0.2
-      - id: WeaponFlareGun
-        prob: 0.05
-      - id: BoxMRE
-        prob: 0.1
+  - type: EntityTableContainerFill
+    containers:
+      entity_storage: !type:NestedSelector
+        tableId: FillLockerEmergencyStandard
 
 - type: entity
   id: ClosetWallEmergencyFilledRandom
   parent: ClosetWallEmergency
   suffix: Filled, Random
   components:
-  - type: StorageFill
-    contents:
-      - id: ClothingOuterSuitEmergency
-      - id: ClothingMaskBreath
-      - id: EmergencyOxygenTankFilled
-        prob: 0.80
-        orGroup: EmergencyTankOrRegularTank
-      - id: OxygenTankFilled
-        prob: 0.20
-        orGroup: EmergencyTankOrRegularTank
-      - id: ToolboxEmergencyFilled
-        prob: 0.4
-      - id: MedkitOxygenFilled
-        prob: 0.2
-      - id: WeaponFlareGun
-        prob: 0.05
-      - id: BoxMRE
-        prob: 0.1
+  - type: EntityTableContainerFill
+    containers:
+      entity_storage: !type:NestedSelector
+        tableId: FillLockerEmergencyStandard
 
 - type: entity
   id: ClosetEmergencyN2FilledRandom
   parent: ClosetEmergencyN2
   suffix: Filled, Random
   components:
-  - type: StorageFill
-    contents:
-      - id: ClothingMaskBreath
-      - id: EmergencyNitrogenTankFilled
-        prob: 0.80
-        orGroup: EmergencyTankOrRegularTank
-      - id: NitrogenTankFilled
-        prob: 0.20
-        orGroup: EmergencyTankOrRegularTank
+  - type: EntityTableContainerFill
+    containers:
+      entity_storage: !type:AllSelector
+        children:
+        - !type:EntSelector
+          id: ClothingMaskBreath
+        - !type:EntSelector
+          id: ClothingOuterSuitEmergency
+        - !type:GroupSelector
+          children:
+          - !type:EntSelector
+            id: EmergencyNitrogenTankFilled
+          - !type:EntSelector
+            id: NitrogenTankFilled
+
+- type: entityTable
+  id: FillLockerFireStandard
+  table: !type:AllSelector
+    children:
+    - !type:EntSelector
+      id: ClothingOuterSuitFire
+    - !type:EntSelector
+      id: ClothingHeadHelmetFire
+    - !type:EntSelector
+      id: ClothingMaskGas
+    - !type:GroupSelector
+      children:
+      - !type:EntSelector
+        id: EmergencyOxygenTankFilled
+      - !type:EntSelector
+        id: OxygenTankFilled
+    - !type:EntSelector
+      id: CrowbarRed
+    - !type:GroupSelector
+      children:
+      - !type:EntSelector
+        id: FireExtinguisher
+        weight: 98
+      - !type:EntSelector
+        id: SprayBottleWater #It's just budget cut after budget cut man
+        weight: 2
 
 - type: entity
   id: ClosetFireFilled
   parent: ClosetFire
   suffix: Filled
   components:
-    - type: StorageFill
-      contents:
-        - id: ClothingOuterSuitFire
-        - id: ClothingHeadHelmetFire
-        - id: ClothingMaskGas
-        - id: OxygenTankFilled
-        - id: FireExtinguisher
-          prob: 0.25
+  - type: EntityTableContainerFill
+    containers:
+      entity_storage: !type:NestedSelector
+        tableId: FillLockerFireStandard
+
 - type: entity
   id: ClosetWallFireFilledRandom
   parent: ClosetWallFire
   suffix: Filled
   components:
-    - type: StorageFill
-      contents:
-        - id: ClothingOuterSuitFire
-        - id: ClothingHeadHelmetFire
-        - id: ClothingMaskGas
-        - id: OxygenTankFilled
-        - id: FireExtinguisher
-          prob: 0.25
+  - type: EntityTableContainerFill
+    containers:
+      entity_storage: !type:NestedSelector
+        tableId: FillLockerFireStandard
+
+- type: entityTable
+  id: SyndieMaintLoot
+  table: !type:GroupSelector
+    children:
+    - !type:GroupSelector
+      children:
+      - !type:EntSelector
+        id: ClothingUniformJumpsuitOperative
+      - !type:EntSelector
+        id: ClothingUniformJumpskirtOperative
+    - !type:EntSelector
+      id: ClothingBackpackDuffelSyndicate
+    - !type:EntSelector
+      id: CyberPen
+    - !type:EntSelector
+      id: CigPackSyndicate
+    - !type:EntSelector
+      id: ClothingBackpackDuffelSyndicatePyjamaBundle
+    - !type:EntSelector
+      id: ClothingBeltMilitaryWebbing
+    - !type:EntSelector
+      id: ClothingShoesBootsCombatFilled
+    - !type:EntSelector
+      id: ToolboxSyndicateFilled
+    - !type:EntSelector
+      id: BalloonSyn
+    - !type:EntSelector
+      id: WeaponSniperMosin
+      weight: 2
+
+- type: entityTable
+  id: MaintenanceLockerLoot
+  table: !type:AllSelector
+    children:
+    - !type:EntSelector
+      id: StrangePill
+      prob: 0.20
+    # Tools
+    - !type:NestedSelector
+      tableId: MaintToolsTable
+      rolls: !type:RangeNumberSelector
+        range: 1, 5
+    # Fluff
+    - !type:NestedSelector
+      tableId: MaintFluffTable
+      prob: 0.33
+      rolls: !type:RangeNumberSelector
+        range: 0, 2
+    # Plushies
+    - !type:NestedSelector
+      tableId: AllPlushiesTable
+      prob: 0.10
+      rolls: !type:RangeNumberSelector
+        range: 1, 2
+    # Weapons
+    - !type:NestedSelector
+      tableId: MaintWeaponTable
+      prob: 0.075
+    # Syndie Loot
+    - !type:NestedSelector
+      tableId: SyndieMaintLoot
+      prob: 0.05
+
 - type: entity
   id: ClosetMaintenanceFilledRandom
   suffix: Filled, Random
   parent: ClosetMaintenance
   components:
-    - type: StorageFill
-      contents:
-        - id: Lantern
-          prob: 0.50
-        - id: Wirecutter
-          prob: 0.33
-        - id: Screwdriver
-          prob: 0.33
-        - id: Wrench
-          prob: 0.33
-        - id: Crowbar
-          prob: 0.50
-        - id: Welder
-          prob: 0.33
-        - id: Multitool
-          prob: 0.10
-        - id: Soap
-          prob: 0.44
-        - id: PlushieCarp
-          prob: 0.2
-          orGroup: carp
-        - id: PlushieHolocarp
-          prob: 0.05
-          orGroup: carp
-        - id: PlushieMagicarp
-          prob: 0.05
-          orGroup: carp
-        - id: PlushieRainbowCarp
-          prob: 0.03
-          orGroup: carp
-        - id: PlushieSlime
-          prob: 0.2
-        - id: PlushieSnake
-          prob: 0.2
-        - id: ClothingShoesSkates
-          prob: 0.1
-        - id: ClothingHandsGlovesColorYellow
-          prob: 0.05
-        - id: ClothingHandsGlovesFingerlessInsulated
-          prob: 0.07
-        - id: ClothingBeltUtility
-          prob: 0.10
-        - id: ClothingHeadHatCone
-          prob: 0.2
-        - id: WeaponFlareGun
-          prob: 0.1
-        - id: ClothingHandsGlovesColorYellowBudget
-          prob: 0.25
-        - id: StrangePill
-          prob: 0.20
-        - id: DeathPaint
-          prob: 0.05
-        - id: DeathPaintTwo
-          prob: 0.05
-        - id: DrinkMopwataBottleRandom
-          prob: 0.20
-        - id: ModularReceiver
-          prob: 0.1
-        - id: DrinkSpaceGlue
-          prob: 0.20
-        - id: DrinkSpaceLube
-          prob: 0.20
-        - id: BarberScissors
-          prob: 0.05
-        - id: BookRandomStory
-          prob: 0.1
-        # Syndicate loot
-        - id: null
-          prob: 0.95
-          orGroup: syndiemaintloot
-        - id: ClothingUniformJumpskirtOperative
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ClothingUniformJumpsuitOperative
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ClothingBackpackDuffelSyndicate
-          prob: 0.005
-          orGroup: syndiemaintloot
-        #- id: CyberPen # DeltaV - Nuh uh
-        #  prob: 0.005
-        #  orGroup: syndiemaintloot
-        #- id: CigPackSyndicate
-        #  prob: 0.005
-        #  orGroup: syndiemaintloot
-        - id: ClothingBackpackDuffelSyndicatePyjamaBundle
-          prob: 0.005
-          orGroup: syndiemaintloot
-        #- id: ClothingBeltMilitaryWebbing
-        #  prob: 0.005
-        #  orGroup: syndiemaintloot
-        #- id: ClothingShoesBootsCombatFilled
-        #  prob: 0.005
-        #  orGroup: syndiemaintloot
-        #- id: ToolboxSyndicateFilled
-        #  prob: 0.005
-        #  orGroup: syndiemaintloot
-        #- id: BalloonSyn
-        #  prob: 0.005
-        #  orGroup: syndiemaintloot
-        #- id: WeaponSniperMosin
-        #  prob: 0.0010
-        #  orGroup: syndiemaintloot
+  - type: EntityTableContainerFill
+    containers:
+      entity_storage: !type:NestedSelector
+        tableId: MaintenanceLockerLoot
 
 - type: entity
   id: ClosetWallMaintenanceFilledRandom
   parent: ClosetWall
   suffix: Filled, Random
   components:
-  - type: StorageFill
-    contents:
-        - id: Lantern
-          prob: 0.50
-        - id: Wirecutter
-          prob: 0.33
-        - id: Screwdriver
-          prob: 0.33
-        - id: Wrench
-          prob: 0.33
-        - id: Crowbar
-          prob: 0.50
-        - id: Welder
-          prob: 0.33
-        - id: Multitool
-          prob: 0.10
-        - id: Soap
-          prob: 0.44
-        - id: PlushieCarp
-          prob: 0.2
-          orGroup: carp
-        - id: PlushieHolocarp
-          prob: 0.05
-          orGroup: carp
-        - id: PlushieMagicarp
-          prob: 0.05
-          orGroup: carp
-        - id: PlushieRainbowCarp
-          prob: 0.03
-          orGroup: carp
-        - id: PlushieSlime
-          prob: 0.2
-        - id: PlushieSnake
-          prob: 0.2
-        - id: ClothingHandsGlovesColorYellow
-          prob: 0.05
-        - id: ClothingBeltQuiver
-          prob: 0.02
-        - id: ClothingBeltUtility
-          prob: 0.10
-        - id: ClothingHeadHatCone
-          prob: 0.2
-        - id: WeaponFlareGun
-          prob: 0.1
-        - id: ClothingHandsGlovesColorYellowBudget
-          prob: 0.25
-        - id: StrangePill
-          prob: 0.20
-        - id: DrinkSpaceGlue
-          prob: 0.20
-        - id: ModularReceiver
-          prob: 0.1
-        # Syndicate loot
-        - id: null
-          prob: 0.95
-          orGroup: syndiemaintloot
-        - id: ClothingUniformJumpskirtOperative
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ClothingUniformJumpsuitOperative
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ClothingBackpackDuffelSyndicate
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: CyberPen
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ClothingHeadHatOutlawHat
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ClothingEyesGlassesOutlawGlasses
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: CigPackSyndicate
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ClothingBackpackDuffelSyndicatePyjamaBundle
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ClothingBeltMilitaryWebbing
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ClothingShoesBootsCombatFilled
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: ToolboxSyndicateFilled
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: BalloonSyn
-          prob: 0.005
-          orGroup: syndiemaintloot
-        - id: WeaponSniperMosin
-          prob: 0.0010
-          orGroup: syndiemaintloot
+  - type: EntityTableContainerFill
+    containers:
+      entity_storage: !type:NestedSelector
+        tableId: MaintenanceLockerLoot
 
 - type: entity
   id: ClosetWallRadiationFilled
   suffix: Filled
   parent: ClosetWallRadiation
   components:
-  - type: StorageFill
-    contents:
-      - id: ClothingOuterSuitRad
-        amount: 2
-      - id: GeigerCounter
-        amount: 2
+  - type: EntityTableContainerFill
+    containers:
+      entity_storage: !type:AllSelector
+        children:
+        - !type:EntSelector
+          id: ClothingOuterSuitRad
+          amount: !type:ConstantNumberSelector
+            value: 2
+        - !type:EntSelector
+          id: GeigerCounter
+          amount: !type:ConstantNumberSelector
+            value: 2
\ No newline at end of file
diff --git a/Resources/Prototypes/DeltaV/Body/Organs/vulpkanin.yml b/Resources/Prototypes/DeltaV/Body/Organs/vulpkanin.yml
index cd4eeae1900..228d0dcf1ce 100644
--- a/Resources/Prototypes/DeltaV/Body/Organs/vulpkanin.yml
+++ b/Resources/Prototypes/DeltaV/Body/Organs/vulpkanin.yml
@@ -1,7 +1,7 @@
 - type: entity
   id: OrganVulpkaninStomach
   parent: OrganAnimalStomach
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Stomach
   - type: SolutionContainerManager
diff --git a/Resources/Prototypes/DeltaV/Catalog/Fills/Backpacks/StarterGear/backpack.yml b/Resources/Prototypes/DeltaV/Catalog/Fills/Backpacks/StarterGear/backpack.yml
index b73c1d5b4fc..d7cc68ad723 100644
--- a/Resources/Prototypes/DeltaV/Catalog/Fills/Backpacks/StarterGear/backpack.yml
+++ b/Resources/Prototypes/DeltaV/Catalog/Fills/Backpacks/StarterGear/backpack.yml
@@ -12,7 +12,7 @@
       - id: BaseBallBat
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackMedical
   id: ClothingBackpackParamedicFilledDV
   components:
@@ -23,7 +23,7 @@
       - id: Portafib
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackMedical
   id: ClothingBackpackPsychologistFilled
   components:
@@ -33,7 +33,7 @@
 
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpack
   id: ClothingBackpackLawyerFilled
   components:
diff --git a/Resources/Prototypes/DeltaV/Catalog/Fills/Backpacks/StarterGear/duffelbag.yml b/Resources/Prototypes/DeltaV/Catalog/Fills/Backpacks/StarterGear/duffelbag.yml
index 50ef77a316f..6ed6dca1edd 100644
--- a/Resources/Prototypes/DeltaV/Catalog/Fills/Backpacks/StarterGear/duffelbag.yml
+++ b/Resources/Prototypes/DeltaV/Catalog/Fills/Backpacks/StarterGear/duffelbag.yml
@@ -1,5 +1,5 @@
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelMedical
   id: ClothingBackpackDuffelParamedicFilledDV
   components:
@@ -10,7 +10,7 @@
       - id: Portafib
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelMedical
   id: ClothingBackpackDuffelPsychologistFilled
   components:
@@ -20,7 +20,7 @@
 
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffel
   id: ClothingBackpackDuffelLawyerFilled
   components:
diff --git a/Resources/Prototypes/DeltaV/Catalog/Fills/Backpacks/StarterGear/satchel.yml b/Resources/Prototypes/DeltaV/Catalog/Fills/Backpacks/StarterGear/satchel.yml
index 99a770e37e1..58d887f47bd 100644
--- a/Resources/Prototypes/DeltaV/Catalog/Fills/Backpacks/StarterGear/satchel.yml
+++ b/Resources/Prototypes/DeltaV/Catalog/Fills/Backpacks/StarterGear/satchel.yml
@@ -1,5 +1,5 @@
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelMedical
   id: ClothingBackpackSatchelParamedicFilledDV
   components:
@@ -10,7 +10,7 @@
       - id: Portafib
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelMedical
   id: ClothingBackpackSatchelPsychologistFilled
   components:
@@ -20,7 +20,7 @@
 
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchel
   id: ClothingBackpackSatchelLawyerFilled
   components:
diff --git a/Resources/Prototypes/DeltaV/Entities/Clothing/Head/hardsuit-helmets.yml b/Resources/Prototypes/DeltaV/Entities/Clothing/Head/hardsuit-helmets.yml
index bdef8822cc5..cfa7d9d26d1 100644
--- a/Resources/Prototypes/DeltaV/Entities/Clothing/Head/hardsuit-helmets.yml
+++ b/Resources/Prototypes/DeltaV/Entities/Clothing/Head/hardsuit-helmets.yml
@@ -2,7 +2,7 @@
 - type: entity
   parent: ClothingHeadHardsuitWithLightBase
   id: ClothingHeadHelmetHardsuitCombatStandard
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: combat hardsuit helmet
   description: An armoured helmet with a yellow visor and dual head-mounted lights.
   components:
@@ -37,7 +37,7 @@
 - type: entity
   parent: ClothingHeadHardsuitWithLightBase
   id: ClothingHeadHelmetHardsuitCombatMedical
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: medical combat hardsuit helmet
   description: A lightweight armoured helmet with full-face blue visor and head-mounted light.
   components:
@@ -72,7 +72,7 @@
 - type: entity
   parent: ClothingHeadHardsuitWithLightBase
   id: ClothingHeadHelmetHardsuitCombatRiot
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: riot combat hardsuit helmet
   description: A heavy armoured helmet with a sealed visor with yellow slits and dual head-mounted lights.
   components:
@@ -107,7 +107,7 @@
 - type: entity
   parent: ClothingHeadHardsuitWithLightBase
   id: ClothingHeadHelmetHardsuitCombatAdvanced
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: advanced combat hardsuit helmet
   description: A light but durable helmet with full-face protection and four head-mounted lights.
   components:
diff --git a/Resources/Prototypes/DeltaV/Entities/Markers/Spawners/ghost_roles.yml b/Resources/Prototypes/DeltaV/Entities/Markers/Spawners/ghost_roles.yml
index 2782fa4f941..f313d30f8f0 100644
--- a/Resources/Prototypes/DeltaV/Entities/Markers/Spawners/ghost_roles.yml
+++ b/Resources/Prototypes/DeltaV/Entities/Markers/Spawners/ghost_roles.yml
@@ -17,7 +17,7 @@
           state: full
 
 - type: entity # Part of PirateRadioSpawn
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   id: SpawnPointGhostSyndicateListener
   name: ghost role spawn point
   suffix: syndicate listener
diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/Player/human.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/Player/human.yml
index d539c58496a..5bc30734426 100644
--- a/Resources/Prototypes/DeltaV/Entities/Mobs/Player/human.yml
+++ b/Resources/Prototypes/DeltaV/Entities/Mobs/Player/human.yml
@@ -1,5 +1,5 @@
 - type: entity # Delta-V : Part of a mid-round PirateRadioSpawn
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: MobHumanSyndicateAgent
   id: MobHumanSyndicateListener
   name: Syndicate Listener
diff --git a/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml b/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml
index b3027839b1a..dda911ec6dc 100644
--- a/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml
+++ b/Resources/Prototypes/DeltaV/Entities/Mobs/Species/vulpkanin.yml
@@ -122,7 +122,7 @@
   name: Vulpkanin Dummy
   parent: MobHumanDummy
   id: MobVulpkaninDummy
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   description: A dummy vulpkanin meant to be used in character setup.
   components:
   - type: HumanoidAppearance
diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Misc/subdermal_implants.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Misc/subdermal_implants.yml
index 4e8392870cd..7dea5b47fe2 100644
--- a/Resources/Prototypes/DeltaV/Entities/Objects/Misc/subdermal_implants.yml
+++ b/Resources/Prototypes/DeltaV/Entities/Objects/Misc/subdermal_implants.yml
@@ -3,7 +3,7 @@
   id: BionicSyrinxImplant
   name: bionic syrinx implant
   description: This implant lets a harpy adjust their voice to whoever they can think of.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: SubdermalImplant
       implantAction: ActionSyrinxChangeVoiceMask
diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail.yml
index 6f96930078b..43e26db6352 100644
--- a/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail.yml
+++ b/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail.yml
@@ -1,6 +1,6 @@
 # DeltaV Mail
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailBooksAll # See Resources/Prototypes/Nyanotrasen/Entities/Markers/Spawners/Random/books.yml
   suffix: books
@@ -168,7 +168,7 @@
 #      orGroup: bookother
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailPumpkinPie
   suffix: pumpkinpie
@@ -178,7 +178,7 @@
     - id: FoodPiePumpkin
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailDVCosplayFakeWizard
   suffix: cosplay-wizard, fake as fuck
@@ -256,7 +256,7 @@
 
 # Frontier Mail, including Extended Nyano Mail. (Pony Express?)
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFAlcohol
   suffix: alcohol, extended
@@ -300,7 +300,7 @@
       amount: 2
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMailLarge
   id: MailNFBible
   suffix: bible, extended
@@ -312,7 +312,7 @@
     - id: ClothingOuterCoatMNKBlackTopCoat #DeltaV - Compensates for items that don't exist here
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFBikeHorn
   suffix: bike horn, random
@@ -327,7 +327,7 @@
       prob: 0.05
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMailLarge
   id: MailNFBuildABuddy
   suffix: Build-a-Buddy
@@ -349,7 +349,7 @@
       - id: PaperMailNFBuildABuddy
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMailLarge
   id: MailNFCake
   suffix: cake, extended
@@ -411,7 +411,7 @@
       amount: 2
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFCosplayWizard
   suffix: cosplay-wizard, extended
@@ -429,7 +429,7 @@
       prob: 0.1
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFCosplayMaid
   suffix: cosplay-maid, extended
@@ -446,7 +446,7 @@
       - id: ClothingHandsGlovesColorWhite
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFCosplayNurse
   suffix: cosplay-nurse, extended
@@ -458,7 +458,7 @@
       - id: Syringe
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFCheese
   suffix: cheese, extended
@@ -476,7 +476,7 @@
     - id: KnifePlastic
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFCigarettes
   suffix: cigs, random
@@ -511,7 +511,7 @@
     - id: CheapLighter
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFEMP
   suffix: emp
@@ -522,7 +522,7 @@
     - id: PaperMailNFEMPPreparedness
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFSmoke
   suffix: smoke
@@ -532,7 +532,7 @@
     - id: DelayedSmoke
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFGoldCigars
   suffix: cigars, premium
@@ -546,7 +546,7 @@
       prob: 0.95
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFCookies
   suffix: cookies, random
@@ -592,7 +592,7 @@
       orGroup: Cookie4
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFKnife
   suffix: knife, extended
@@ -609,7 +609,7 @@
       orGroup: Knife
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMailLarge
   id: MailNFSword
   suffix: sword
@@ -645,7 +645,7 @@
       prob: 0.001
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFMuffins
   suffix: muffins, random
@@ -703,7 +703,7 @@
       prob: 0.5
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFPAI
   suffix: PAI, extended
@@ -719,7 +719,7 @@
       prob: 0.01
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFPlushie
   suffix: plushie, extended
@@ -783,7 +783,7 @@
 
 # Random snacks, replaces MailChocolate (lousy animal organs)
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFSnacks
   suffix: snacks, random
@@ -896,7 +896,7 @@
       prob: 0.5
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFVagueThreat
   suffix: vague-threat
@@ -943,7 +943,7 @@
       orGroup: ThreateningObject
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFDonkPockets
   suffix: donk pockets, random
@@ -980,7 +980,7 @@
 
 # Needs a buff?
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFSodaPwrGame
   suffix: Pwrgame
@@ -993,7 +993,7 @@
 
 # Needs a buff?
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFSodaRedBool
   suffix: Red Bool
@@ -1006,7 +1006,7 @@
 
 # Needs a buff?
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFSodaSpaceCola
   suffix: Space Cola
@@ -1018,7 +1018,7 @@
 
 # Needs a buff?
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFSodaSpaceMountainWind
   suffix: Space Mountain Wind
@@ -1030,7 +1030,7 @@
 
 # Needs a buff?
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFSodaSpaceUp
   suffix: Space Up
@@ -1042,7 +1042,7 @@
 
 #TODO: we don't have rainbow joints or blunts (yet?). Uncomment when (if?) they are added.
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFJoints
   suffix: joints
@@ -1093,7 +1093,7 @@
 # Mmm, mail food
 # Needs a buff?
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFUnusualFood
   suffix: unusual food
@@ -1159,7 +1159,7 @@
 
 # Needs a buff?
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFBakedGoods
   suffix: baked goods
@@ -1306,7 +1306,7 @@
 
 # Needs a buff?
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFUnusualProduce
   suffix: unusual produce
@@ -1357,7 +1357,7 @@
       prob: 0.25
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFSoaps
   suffix: soap sampler
@@ -1371,7 +1371,7 @@
       orGroup: Ad
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFSoapsOmega
   suffix: soap sampler, omega
@@ -1386,7 +1386,7 @@
 
 # Could add spessman battle rules here
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFFigurineBulk # DeltaV - No longer Bulk
   suffix: figurine, bulk #DeltaV - Spams 3 boxes instead of using the bulk figurine prototype that Frontier uses
@@ -1398,7 +1398,7 @@
     - id: MysteryFigureBox
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFPen
   suffix: fancy pen
@@ -1426,7 +1426,7 @@
     - id: PaperMailNFPaperPusherAd
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMailLarge
   id: MailNFThrongler
   suffix: throngler
@@ -1436,7 +1436,7 @@
     - id: ThronglerToy
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFInstrumentSmall
   suffix: instrument, expanded
@@ -1483,7 +1483,7 @@
       prob: 0.01
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFUnusualClothing
   suffix: unusual clothing
@@ -1536,7 +1536,7 @@
         prob: 0.25
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFCritter
   suffix: critter
@@ -1589,7 +1589,7 @@
         prob: 0.01 # Rare
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFTacticalMaid
   suffix: tactical maid
@@ -1602,7 +1602,7 @@
       - id: ClothingHandsTacticalMaidGloves
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMailLarge
   id: MailNFKendoKit
   suffix: kendo kit
@@ -1620,7 +1620,7 @@
 
 # Base Nyano Mail
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailSake
   suffix: osake
@@ -1632,7 +1632,7 @@
     - id: DrinkTokkuri
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailBlockGameDIY
   suffix: blockgamediy
@@ -1642,7 +1642,7 @@
     - id: BlockGameArcadeComputerCircuitboard
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailCigars
   suffix: Cigars
@@ -1653,7 +1653,7 @@
     - id: Lighter
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailCrayon
   suffix: Crayon
@@ -1663,7 +1663,7 @@
     - id: CrayonBox
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailCosplayArc
   suffix: cosplay-arc
@@ -1674,7 +1674,7 @@
     - id: ClothingCostumeArcDress
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailCosplayGeisha
   suffix: cosplay-geisha
@@ -1687,7 +1687,7 @@
         amount: 3
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailCosplaySchoolgirl
   suffix: cosplay-schoolgirl
@@ -1712,7 +1712,7 @@
       orGroup: Color
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailFlowers
   suffix: flowers
@@ -1727,7 +1727,7 @@
     - id: FoodLily
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailSignallerKit
   suffix: signallerkit
@@ -1738,7 +1738,7 @@
     - id: RemoteSignaller
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNoir
   suffix: noir
@@ -1751,7 +1751,7 @@
     - id: ClothingOuterCoatGentle
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailRestraints
   suffix: restraints
@@ -1763,7 +1763,7 @@
     - id: ClothingEyesBlindfold
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailFishingCap
   suffix: fishingcap
@@ -1773,7 +1773,7 @@
     - id: ClothingHeadFishCap
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailFlashlight
   suffix: Flashlight
@@ -1783,7 +1783,7 @@
     - id: FlashlightLantern
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailSpaceVillainDIY
   suffix: spacevilliandiy
@@ -1793,7 +1793,7 @@
     - id: SpaceVillainArcadeComputerCircuitboard
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailSunglasses
   suffix: Sunglasses
@@ -1803,7 +1803,7 @@
     - id: ClothingEyesGlassesSunglasses
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailWinterCoat
   suffix: wintercoat
diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_civilian.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_civilian.yml
index 19a1ee3c536..8b461295053 100644
--- a/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_civilian.yml
+++ b/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_civilian.yml
@@ -1,6 +1,6 @@
 # Frontier Mail
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMailLarge
   id: MailNFInstrumentLarge
   suffix: instrument, large
@@ -53,7 +53,7 @@
 # Base Nyano Mail
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailBotanistChemicalBottles
   suffix: botanist chemicals
@@ -68,7 +68,7 @@
       prob: 0.4
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailBotanistMutagen
   suffix: mutagen
@@ -80,7 +80,7 @@
     - id: UnstableMutagenChemistryBottle
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailBotanistSeeds
   suffix: seeds
@@ -129,7 +129,7 @@
       orGroup: Seeds
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailClownGildedBikeHorn
   suffix: honk
@@ -140,7 +140,7 @@
     - id: BikeHornInstrument
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailClownHonkSupplement
   suffix: honk
@@ -153,7 +153,7 @@
     - id: FoodBanana
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailHoPBureaucracy
   suffix: hop paper
@@ -164,7 +164,7 @@
       maxAmount: 3
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailHoPSupplement
   suffix: hop supplement
@@ -176,7 +176,7 @@
     - id: Paper
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailMimeArtsCrafts
   suffix: arts and crafts
@@ -188,7 +188,7 @@
       maxAmount: 3
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailMimeBlankBook
   suffix: blank book
@@ -198,7 +198,7 @@
       - id: BookRandom
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailMimeBottleOfNothing
   suffix: bottle of nothing
@@ -208,7 +208,7 @@
       - id: DrinkBottleOfNothingFull
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailPassengerMoney
   suffix: passenger money
diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_command.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_command.yml
index 86dec46a654..d8f6183b9c6 100644
--- a/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_command.yml
+++ b/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_command.yml
@@ -1,6 +1,6 @@
 # Base Nyano Mail
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailCommandPinpointerNuclear
   suffix: pinpointer mail ops
@@ -10,7 +10,7 @@
     - id: PinpointerNuclear
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailStationRepNFNukeDisk
   suffix: nuke disk
@@ -22,7 +22,7 @@
       - id: PaperMailNFAntivirus
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailCommandNFPipebombIntern
   suffix: pipe and bomb
diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_engineering.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_engineering.yml
index af70fc621cb..2d3132e7f04 100644
--- a/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_engineering.yml
+++ b/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_engineering.yml
@@ -1,6 +1,6 @@
 # Base Nyano Mail
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailEngineeringCables
   suffix: cables
@@ -15,7 +15,7 @@
       orGroup: Cables
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailEngineeringKudzuDeterrent
   suffix: antikudzu
@@ -25,7 +25,7 @@
     - id: PlantBGoneSpray
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailEngineeringSheetGlass
   suffix: sheetglass
@@ -35,7 +35,7 @@
     - id: SheetGlass
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailEngineeringWelderReplacement
   suffix: welder
@@ -47,7 +47,7 @@
 # Frontier Mail
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFCircuitboardIndustrial
   suffix: industrial circuitboard
@@ -113,7 +113,7 @@
       prob: 0.1
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailNFCircuitboardService
   suffix: service circuitboard
@@ -155,7 +155,7 @@
       prob: 0.1
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMailLarge
   id: MailNFPowerTool
   suffix: power tool
diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_epistemology.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_epistemology.yml
index be6f9818e22..0006a0af023 100644
--- a/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_epistemology.yml
+++ b/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_epistemology.yml
@@ -1,5 +1,5 @@
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailEpistemologyBluespace
   suffix: bluespace
@@ -9,7 +9,7 @@
     - id: MaterialBluespace1
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailEpistemologyIngotGold
   suffix: ingotgold
@@ -20,7 +20,7 @@
       maxAmount: 3
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailEpistemologyResearchDisk
   suffix: researchdisk
@@ -38,7 +38,7 @@
       prob: 0.1
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailEpistemologyTinfoilHat
   suffix: tinfoilhat
diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_medical.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_medical.yml
index 735f840a201..f19a3950b91 100644
--- a/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_medical.yml
+++ b/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_medical.yml
@@ -1,6 +1,6 @@
 # Base Nyano Mail
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailMedicalBasicSupplies
   suffix: basicmedical
@@ -15,7 +15,7 @@
       maxAmount: 2
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailMedicalChemistrySupplement
   suffix: chemsupp
@@ -31,7 +31,7 @@
       maxAmount: 3
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailMedicalEmergencyPens
   suffix: medipens
@@ -42,7 +42,7 @@
       maxAmount: 3
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailMedicalMedicinePills
   suffix: medicinepills
@@ -57,7 +57,7 @@
       maxAmount: 2
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailMedicalSheetPlasma
   suffix: sheetplasma
@@ -67,7 +67,7 @@
     - id: SheetPlasma1
 
 #- type: entity
-#  noSpawn: true
+#  categories: [ HideSpawnMenu ]
 #  parent: BaseMail
 #  id: MailMedicalSpaceacillin
 #  suffix: spaceacillin
@@ -79,7 +79,7 @@
 # Awaiting diseases
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailMedicalStabilizers
   suffix: stabilizers
diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_security.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_security.yml
index eed846a0909..c1880eb4d4a 100644
--- a/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_security.yml
+++ b/Resources/Prototypes/DeltaV/Entities/Objects/Specific/Mail/mail_security.yml
@@ -1,6 +1,6 @@
 # Base Nyano Mail
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailSecurityDonuts
   suffix: donuts
@@ -10,7 +10,7 @@
     - id: FoodBoxDonut
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailSecurityFlashlight
   suffix: seclite
@@ -20,7 +20,7 @@
     - id: FlashlightSeclite
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailSecurityNonlethalsKit
   suffix: nonlethalskit
@@ -45,7 +45,7 @@
 # Will we ever readd hyperlinks books? Who knows!
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailWardenCrowdControl
   suffix: crowdcontrol
@@ -55,7 +55,7 @@
     - id: BoxBeanbag
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailDetectiveForensicSupplement # Deltav - Detective is in charge of investigating crimes.
   suffix: detectivesupplement # Deltav - Detective is in charge of investigating crimes.
@@ -67,7 +67,7 @@
 # Frontier Mail
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMailLarge
   id: MailSecurityNFMusket
   suffix: musket
@@ -83,7 +83,7 @@
 # Delta Mail
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailSecurityDVSpaceLaw
   suffix: spacelaw, extended
diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/replicated.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/replicated.yml
index 64bb4369f43..0132928af30 100644
--- a/Resources/Prototypes/DeltaV/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/replicated.yml
+++ b/Resources/Prototypes/DeltaV/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/replicated.yml
@@ -2,7 +2,7 @@
   id: BulletLightRifleReplicated
   name: replicated bullet (.20 rifle)
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Projectile
       damage:
diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/special.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/special.yml
index df2ccf2a94e..9233a901f9d 100644
--- a/Resources/Prototypes/DeltaV/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/special.yml
+++ b/Resources/Prototypes/DeltaV/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/special.yml
@@ -2,7 +2,7 @@
   id: BulletSpecial
   name: bullet (.38 special)
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -13,7 +13,7 @@
   id: BulletSpecialPractice
   name: bullet (.38 special practice)
   parent: BaseBulletPractice
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -24,7 +24,7 @@
   id: BulletSpecialRubber
   name: bullet (.38 special rubber)
   parent: BaseBulletRubber
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -37,7 +37,7 @@
   id: BulletSpecialIncendiary
   name: bullet (.38 special incendiary)
   parent: BaseBulletIncendiary
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -49,7 +49,7 @@
   id: BulletSpecialUranium
   name: bullet (.38 special uranium)
   parent: BaseBulletUranium
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -61,7 +61,7 @@
   id: BulletSpecialHoly
   name: bullet (.38 special holy)
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -73,7 +73,7 @@
   id: BulletSpecialMindbreaker
   name: bullet (.38 special mindbreaker)
   parent: BaseBulletPractice
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
diff --git a/Resources/Prototypes/DeltaV/Entities/Objects/Weapons/Guns/Projectiles/impacts.yml b/Resources/Prototypes/DeltaV/Entities/Objects/Weapons/Guns/Projectiles/impacts.yml
index 936ac9856ec..869da03ded5 100644
--- a/Resources/Prototypes/DeltaV/Entities/Objects/Weapons/Guns/Projectiles/impacts.yml
+++ b/Resources/Prototypes/DeltaV/Entities/Objects/Weapons/Guns/Projectiles/impacts.yml
@@ -1,6 +1,6 @@
 - type: entity
   id: BulletImpactEffectRedDisabler
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TimedDespawn
     lifetime: 0.2
diff --git a/Resources/Prototypes/DeltaV/GameRules/events.yml b/Resources/Prototypes/DeltaV/GameRules/events.yml
index db727601f84..2997ceb0734 100644
--- a/Resources/Prototypes/DeltaV/GameRules/events.yml
+++ b/Resources/Prototypes/DeltaV/GameRules/events.yml
@@ -1,7 +1,7 @@
 - type: entity
   id: XenoVents
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: true
@@ -33,7 +33,7 @@
 - type: entity
   id: XenoVentsWeak
   parent: XenoVents
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: true
@@ -59,7 +59,7 @@
 - type: entity
   id: MothroachSpawn
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     earliestStart: 15
@@ -74,7 +74,7 @@
 - type: entity
   id: PirateRadioSpawn
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     weight: 7.5
diff --git a/Resources/Prototypes/DeltaV/GameRules/midround.yml b/Resources/Prototypes/DeltaV/GameRules/midround.yml
index 82b47c05718..74edf7a97d5 100644
--- a/Resources/Prototypes/DeltaV/GameRules/midround.yml
+++ b/Resources/Prototypes/DeltaV/GameRules/midround.yml
@@ -1,5 +1,5 @@
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseGameRule
   id: ParadoxAnomaly
   components:
diff --git a/Resources/Prototypes/DeltaV/Objectives/paradox_anomaly.yml b/Resources/Prototypes/DeltaV/Objectives/paradox_anomaly.yml
index ec87795ab5b..b98ee1539c6 100644
--- a/Resources/Prototypes/DeltaV/Objectives/paradox_anomaly.yml
+++ b/Resources/Prototypes/DeltaV/Objectives/paradox_anomaly.yml
@@ -8,7 +8,7 @@
 
 # not using base kill/keep alive objectives since these intentionally conflict with eachother
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseParadoxAnomalyObjective
   id: ParadoxAnomalyKillObjective
   description: This universe doesn't have room for both of us.
@@ -23,7 +23,7 @@
     requireDead: true
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseParadoxAnomalyObjective
   id: ParadoxAnomalyFriendObjective
   description: Perhaps there is room, as friends.
@@ -37,7 +37,7 @@
   - type: KeepAliveCondition
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: [BaseParadoxAnomalyObjective, BaseLivingObjective]
   id: ParadoxAnomalyEscapeObjective
   name: Escape to centcom alive and unrestrained.
diff --git a/Resources/Prototypes/DeltaV/Objectives/traitor.yml b/Resources/Prototypes/DeltaV/Objectives/traitor.yml
index c5c5318b5d2..2f53b211d9e 100644
--- a/Resources/Prototypes/DeltaV/Objectives/traitor.yml
+++ b/Resources/Prototypes/DeltaV/Objectives/traitor.yml
@@ -1,5 +1,5 @@
 - type: entity # Logistics Officer steal objective.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseTraitorStealObjective
   id: LOLuckyBillStealObjective
   components:
@@ -10,7 +10,7 @@
     # owner: job-name-qm
 
 - type: entity # Head of Personnel steal objective.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseTraitorStealObjective
   id: HoPBookIanDossierStealObjective
   components:
@@ -21,7 +21,7 @@
     # owner: job-name-hop
 
 - type: entity # Clerk steal objective.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseTraitorStealObjective
   id: ClerkNotaryStealObjective
   components:
diff --git a/Resources/Prototypes/DeltaV/Roles/Jobs/NPC/syndicateNPCs.yml b/Resources/Prototypes/DeltaV/Roles/Jobs/NPC/syndicateNPCs.yml
index b5379c5014e..4dc4fc37aae 100644
--- a/Resources/Prototypes/DeltaV/Roles/Jobs/NPC/syndicateNPCs.yml
+++ b/Resources/Prototypes/DeltaV/Roles/Jobs/NPC/syndicateNPCs.yml
@@ -16,6 +16,6 @@
   name: grafted plate carrier
   suffix: Unremoveable
   description: An off-the-shelf plate carrier that has been cruelly grafted onto its wearers body
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Unremoveable
diff --git a/Resources/Prototypes/Entities/Clothing/Eyes/hud.yml b/Resources/Prototypes/Entities/Clothing/Eyes/hud.yml
index 835c405e192..8b2eea97e80 100644
--- a/Resources/Prototypes/Entities/Clothing/Eyes/hud.yml
+++ b/Resources/Prototypes/Entities/Clothing/Eyes/hud.yml
@@ -1,7 +1,7 @@
 - type: entity
   id: ShowSecurityIcons
   abstract: true
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: ShowJobIcons
   - type: ShowMindShieldIcons
diff --git a/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml b/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml
index e19217686f4..08a3eb568fe 100644
--- a/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml
+++ b/Resources/Prototypes/Entities/Clothing/Head/base_clothinghead.yml
@@ -59,7 +59,7 @@
   parent: ClothingHeadBase
   id: ClothingHeadLightBase
   name: base helmet with light
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     layers:
@@ -146,7 +146,7 @@
   # No parent since we aren't actually an item.
   id: ClothingHeadHardsuitBase
   name: base hardsuit helmet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     state: icon # default state used by most inheritors
@@ -189,7 +189,7 @@
   parent: ClothingHeadHardsuitBase
   id: ClothingHeadHardsuitWithLightBase
   name: base hardsuit helmet with light
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     layers:
@@ -241,7 +241,7 @@
   id: ClothingHeadHatHoodWinterBase
   name: base winter coat hood
   description: A hood, made to keep your head warm.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     state: icon
diff --git a/Resources/Prototypes/Entities/Clothing/Head/hardsuit-helmets.yml b/Resources/Prototypes/Entities/Clothing/Head/hardsuit-helmets.yml
index b563fbc0071..caccdd8745e 100644
--- a/Resources/Prototypes/Entities/Clothing/Head/hardsuit-helmets.yml
+++ b/Resources/Prototypes/Entities/Clothing/Head/hardsuit-helmets.yml
@@ -136,7 +136,7 @@
 - type: entity
   parent: ClothingHeadHardsuitBase
   id: ClothingHeadHelmetHardsuitMaxim
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: salvager maxim helmet
   description: A predication of decay washes over your mind.
   components:
diff --git a/Resources/Prototypes/Entities/Clothing/Head/hoods.yml b/Resources/Prototypes/Entities/Clothing/Head/hoods.yml
index 0bca209d51e..be7bda91008 100644
--- a/Resources/Prototypes/Entities/Clothing/Head/hoods.yml
+++ b/Resources/Prototypes/Entities/Clothing/Head/hoods.yml
@@ -87,7 +87,7 @@
 - type: entity
   parent: ClothingHeadBase
   id: ClothingHeadHatHoodChaplainHood
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: chaplain's hood
   description: Maximum piety in this star system.
   components:
@@ -179,7 +179,7 @@
 - type: entity
   parent: ClothingHeadBase
   id: ClothingHeadHatHoodIan
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: ian hood
   description: A hood to complete the 'Good boy' look.
   components:
@@ -194,7 +194,7 @@
 - type: entity
   parent: ClothingHeadBase
   id: ClothingHeadHatHoodCarp
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: carp hood
   description: A gnarly hood adorned with plastic space carp teeth.
   components:
@@ -229,7 +229,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterDefault
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: default winter coat hood
   components:
   - type: Sprite
@@ -240,7 +240,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterBartender
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: bartender winter coat hood
   components:
   - type: Sprite
@@ -251,7 +251,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterCaptain
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: captain's winter coat hood
   description: An expensive hood, to keep the captain's head warm.
   components:
@@ -263,7 +263,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterCargo
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: logistics winter coat hood # DeltaV - Logistics Department replacing Cargo
   components:
   - type: Sprite
@@ -274,7 +274,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterCE
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: chief engineer's winter coat hood
   components:
   - type: Sprite
@@ -285,7 +285,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterCentcom
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: Centcom winter coat hood
   description: A hood for keeping the central comander's head warm.
   components:
@@ -297,7 +297,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterChem
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: chemist winter coat hood
   components:
   - type: Sprite
@@ -308,7 +308,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterCMO
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: chief medical officer's winter coat hood
   components:
   - type: Sprite
@@ -319,7 +319,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterEngineer
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: engineer winter coat hood
   components:
   - type: Sprite
@@ -330,7 +330,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterHOP
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: head of personel's winter coat hood
   components:
   - type: Sprite
@@ -341,7 +341,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterHOS
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: head of security's winter coat hood
   components:
   - type: Sprite
@@ -352,7 +352,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterHydro
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: hydroponics coat hood
   components:
   - type: Sprite
@@ -363,7 +363,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterJani
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: janitor coat hood
   components:
   - type: Sprite
@@ -374,7 +374,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterMed
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: medic coat hood
   components:
   - type: Sprite
@@ -385,7 +385,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterMime
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: mime coat hood
   components:
   - type: Sprite
@@ -396,7 +396,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterMiner
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: miner coat hood
   components:
   - type: Sprite
@@ -407,7 +407,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterPara
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: paramedic coat hood
   components:
   - type: Sprite
@@ -418,7 +418,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterQM
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: logistics officer's coat hood # DeltaV - Logistics Department replacing Cargo
   components:
   - type: Sprite
@@ -429,7 +429,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterRD
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: mystagogue's coat hood # DeltaV - Epistemics Department replacing Science
   components:
   - type: Sprite
@@ -440,7 +440,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterRobo
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: robotics coat hood
   components:
   - type: Sprite
@@ -451,7 +451,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterSci
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: scientist coat hood
   components:
   - type: Sprite
@@ -462,7 +462,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterSec
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: security coat hood
   components:
   - type: Sprite
@@ -473,7 +473,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterSyndie
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: syndicate coat hood
   components:
   - type: Sprite
@@ -484,7 +484,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterWarden
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: warden's coat hood
   components:
   - type: Sprite
@@ -495,7 +495,7 @@
 - type: entity
   parent: ClothingHeadHatHoodWinterBase
   id: ClothingHeadHatHoodWinterWeb
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: web coat hood
   components:
   - type: Sprite
diff --git a/Resources/Prototypes/Entities/Clothing/Masks/base_clothingmask.yml b/Resources/Prototypes/Entities/Clothing/Masks/base_clothingmask.yml
index 3531a26a6c5..03400e4a82e 100644
--- a/Resources/Prototypes/Entities/Clothing/Masks/base_clothingmask.yml
+++ b/Resources/Prototypes/Entities/Clothing/Masks/base_clothingmask.yml
@@ -21,7 +21,7 @@
   id: ActionToggleMask
   name: Toggle Mask
   description: Handy, but prevents insertion of pie into your pie hole.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: { sprite: Clothing/Mask/gas.rsi, state: icon }
diff --git a/Resources/Prototypes/Entities/Clothing/Neck/misc.yml b/Resources/Prototypes/Entities/Clothing/Neck/misc.yml
index 11921b9a50d..a39c2e69159 100644
--- a/Resources/Prototypes/Entities/Clothing/Neck/misc.yml
+++ b/Resources/Prototypes/Entities/Clothing/Neck/misc.yml
@@ -70,7 +70,7 @@
 - type: entity
   id: ActionStethoscope
   name: Listen with stethoscope
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: EntityTargetAction
     icon:
diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/wintercoats.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/wintercoats.yml
index 73dc6c8c7f7..9ae3d3b7d55 100644
--- a/Resources/Prototypes/Entities/Clothing/OuterClothing/wintercoats.yml
+++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/wintercoats.yml
@@ -38,7 +38,7 @@
   parent: ClothingOuterWinterCoat
   id: ClothingOuterWinterCoatToggleable
   name: winter coat with hood
-  noSpawn: True
+  categories: [ HideSpawnMenu ]
   components:
   - type: ToggleableClothing
     clothingPrototype: ClothingHeadHatHoodWinterDefault
diff --git a/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml b/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml
index c4bdefd3a14..034064b9439 100644
--- a/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml
+++ b/Resources/Prototypes/Entities/Clothing/Shoes/magboots.yml
@@ -136,7 +136,7 @@
   id: ActionBaseToggleMagboots
   name: Toggle Magboots
   description: Toggles the magboots on and off.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     itemIconStyle: NoItem
@@ -145,7 +145,7 @@
 - type: entity
   id: ActionToggleMagboots
   parent: ActionBaseToggleMagboots
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: { sprite: Clothing/Shoes/Boots/magboots.rsi, state: icon }
@@ -154,7 +154,7 @@
 - type: entity
   id: ActionToggleMagbootsAdvanced
   parent: ActionBaseToggleMagboots
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: { sprite: Clothing/Shoes/Boots/magboots-advanced.rsi, state: icon }
@@ -163,7 +163,7 @@
 - type: entity
   id: ActionToggleMagbootsSci
   parent: ActionBaseToggleMagboots
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: { sprite: Clothing/Shoes/Boots/magboots-science.rsi, state: icon }
@@ -172,7 +172,7 @@
 - type: entity
   id: ActionToggleMagbootsSyndie
   parent: ActionBaseToggleMagboots
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: { sprite: Clothing/Shoes/Boots/magboots-syndicate.rsi, state: icon }
diff --git a/Resources/Prototypes/Entities/Clothing/Shoes/misc.yml b/Resources/Prototypes/Entities/Clothing/Shoes/misc.yml
index d5a695c7c0b..888bcd769cf 100644
--- a/Resources/Prototypes/Entities/Clothing/Shoes/misc.yml
+++ b/Resources/Prototypes/Entities/Clothing/Shoes/misc.yml
@@ -128,7 +128,7 @@
   id: ActionToggleSpeedBoots
   name: Toggle Speed Boots
   description: Toggles the speed boots on and off.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     itemIconStyle: NoItem
diff --git a/Resources/Prototypes/Entities/Debugging/clicktest.yml b/Resources/Prototypes/Entities/Debugging/clicktest.yml
index 4e9caaa14d8..baab1b834f6 100644
--- a/Resources/Prototypes/Entities/Debugging/clicktest.yml
+++ b/Resources/Prototypes/Entities/Debugging/clicktest.yml
@@ -5,7 +5,7 @@
 # These dots' texture detection should not interfere with the actual bounding box being tested.
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   id: ClickTestBase
   suffix: DEBUG
   components:
diff --git a/Resources/Prototypes/Entities/Debugging/debug_sweps.yml b/Resources/Prototypes/Entities/Debugging/debug_sweps.yml
index 0430dbf427e..c7faff4db2c 100644
--- a/Resources/Prototypes/Entities/Debugging/debug_sweps.yml
+++ b/Resources/Prototypes/Entities/Debugging/debug_sweps.yml
@@ -60,7 +60,7 @@
   id: BulletDebug
   name: bang, ded bullet
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   suffix: DEBUG
   components:
   - type: Tag
diff --git a/Resources/Prototypes/Entities/Debugging/tippy.yml b/Resources/Prototypes/Entities/Debugging/tippy.yml
index 292cbed0f1b..7f149c3b5dc 100644
--- a/Resources/Prototypes/Entities/Debugging/tippy.yml
+++ b/Resources/Prototypes/Entities/Debugging/tippy.yml
@@ -1,6 +1,6 @@
 - type: entity
   id: Tippy
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     netsync: false
diff --git a/Resources/Prototypes/Entities/Effects/ambient_sounds.yml b/Resources/Prototypes/Entities/Effects/ambient_sounds.yml
index f686779b3ee..81f7bbfc8a3 100644
--- a/Resources/Prototypes/Entities/Effects/ambient_sounds.yml
+++ b/Resources/Prototypes/Entities/Effects/ambient_sounds.yml
@@ -1,6 +1,6 @@
 - type: entity
   id: AmbientSoundSourceFlies
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: AmbientSound
     volume: -5
diff --git a/Resources/Prototypes/Entities/Effects/bluespace_flash.yml b/Resources/Prototypes/Entities/Effects/bluespace_flash.yml
index b05681def2d..3137bb5af8e 100644
--- a/Resources/Prototypes/Entities/Effects/bluespace_flash.yml
+++ b/Resources/Prototypes/Entities/Effects/bluespace_flash.yml
@@ -1,6 +1,6 @@
 - type: entity
   id: EffectFlashBluespace
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: PointLight
     radius: 10.5
@@ -14,7 +14,7 @@
 
 - type: entity
   id: EffectFlashTelekineticPulse
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: PointLight
     radius: 10
@@ -28,7 +28,7 @@
 
 - type: entity
   id: EffectFlashShadeskip
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: PointLight
     radius: 5
@@ -42,7 +42,7 @@
 
 - type: entity
   id: EffectFlashShadowkinShadeskip
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: PointLight
     radius: 5
@@ -56,7 +56,7 @@
 
 - type: entity
   id: EffectFlashShadowkinDarkSwapOn
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: PointLight
     radius: 5
@@ -70,7 +70,7 @@
 
 - type: entity
   id: EffectFlashShadowkinDarkSwapOff
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: PointLight
     radius: 5
@@ -84,7 +84,7 @@
 
 - type: entity
   id: EffectPyrokineticFlare
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       sprite: /Textures/Interface/Actions/psionics.rsi
diff --git a/Resources/Prototypes/Entities/Effects/chemistry_effects.yml b/Resources/Prototypes/Entities/Effects/chemistry_effects.yml
index 469bab32782..62cf5c95a0f 100644
--- a/Resources/Prototypes/Entities/Effects/chemistry_effects.yml
+++ b/Resources/Prototypes/Entities/Effects/chemistry_effects.yml
@@ -36,7 +36,7 @@
   parent: BaseFoam
   id: Smoke
   name: smoke
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Occluder
   - type: Sprite
@@ -52,7 +52,7 @@
   parent: BaseFoam
   id: Foam
   name: foam
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     color: "#ffffffcc"
@@ -87,7 +87,7 @@
 - type: entity
   id: MetalFoam
   name: metal foam
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: Foam
   components:
   - type: Sprite
@@ -111,7 +111,7 @@
 - type: entity
   id: IronMetalFoam
   name: iron metal foam
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: MetalFoam
   components:
   - type: SpawnOnDespawn
@@ -120,7 +120,7 @@
 - type: entity
   id: AluminiumMetalFoam
   name: aluminium metal foam
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: MetalFoam
   components:
   - type: SpawnOnDespawn
diff --git a/Resources/Prototypes/Entities/Effects/emp_effects.yml b/Resources/Prototypes/Entities/Effects/emp_effects.yml
index 890dd24d430..d1096b85f5e 100644
--- a/Resources/Prototypes/Entities/Effects/emp_effects.yml
+++ b/Resources/Prototypes/Entities/Effects/emp_effects.yml
@@ -1,6 +1,6 @@
 - type: entity
   id: EffectEmpPulse
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TimedDespawn
     lifetime: 0.8
@@ -23,7 +23,7 @@
 
 - type: entity
   id: EffectEmpDisabled
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TimedDespawn
     lifetime: 0.4
diff --git a/Resources/Prototypes/Entities/Effects/exclamation.yml b/Resources/Prototypes/Entities/Effects/exclamation.yml
index cfe1cbc7f22..d1f3e3ad063 100644
--- a/Resources/Prototypes/Entities/Effects/exclamation.yml
+++ b/Resources/Prototypes/Entities/Effects/exclamation.yml
@@ -1,7 +1,7 @@
 - type: entity
   id: Exclamation
   name: exclamation
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   save: false
   components:
     - type: Transform
@@ -22,7 +22,7 @@
 - type: entity
   id: WhistleExclamation
   name: exclamation
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       sprite: Structures/Storage/closet.rsi
diff --git a/Resources/Prototypes/Entities/Effects/explosion_light.yml b/Resources/Prototypes/Entities/Effects/explosion_light.yml
index b4b980dd1a5..d5e7452a795 100644
--- a/Resources/Prototypes/Entities/Effects/explosion_light.yml
+++ b/Resources/Prototypes/Entities/Effects/explosion_light.yml
@@ -1,5 +1,5 @@
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   id: ExplosionLight
   name: explosion light
   components:
diff --git a/Resources/Prototypes/Entities/Effects/hearts.yml b/Resources/Prototypes/Entities/Effects/hearts.yml
index 042fdb5e8ab..18f72c95d4a 100644
--- a/Resources/Prototypes/Entities/Effects/hearts.yml
+++ b/Resources/Prototypes/Entities/Effects/hearts.yml
@@ -1,6 +1,6 @@
 - type: entity
   id: EffectHearts
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TimedDespawn
     lifetime: 0.85
diff --git a/Resources/Prototypes/Entities/Effects/lightning.yml b/Resources/Prototypes/Entities/Effects/lightning.yml
index 7afd1c07a0c..f85f28d3b80 100644
--- a/Resources/Prototypes/Entities/Effects/lightning.yml
+++ b/Resources/Prototypes/Entities/Effects/lightning.yml
@@ -33,7 +33,7 @@
   name: lightning
   id: Lightning
   parent: BaseLightning
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Lightning
       canArc: true
@@ -42,7 +42,7 @@
   name: spooky lightning
   id: LightningRevenant
   parent: BaseLightning
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       sprite: /Textures/Effects/lightning.rsi
@@ -66,7 +66,7 @@
   name: charged lightning
   id: ChargedLightning
   parent: BaseLightning
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       sprite: /Textures/Effects/lightning.rsi
@@ -85,7 +85,7 @@
   name: lightning
   id: Spark
   parent: BaseLightning
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: /Textures/Effects/lightning.rsi
@@ -112,7 +112,7 @@
   name: supercharged lightning
   id: SuperchargedLightning
   parent: ChargedLightning
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       sprite: /Textures/Effects/lightning.rsi
@@ -138,7 +138,7 @@
   name: hypercharged lightning
   id: HyperchargedLightning
   parent: ChargedLightning
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       sprite: /Textures/Effects/lightning.rsi
diff --git a/Resources/Prototypes/Entities/Effects/mobspawn.yml b/Resources/Prototypes/Entities/Effects/mobspawn.yml
index 4529497021e..695f8a9e715 100644
--- a/Resources/Prototypes/Entities/Effects/mobspawn.yml
+++ b/Resources/Prototypes/Entities/Effects/mobspawn.yml
@@ -61,7 +61,7 @@
 
 - type: entity
   id: EffectAnomalyFloraBulb
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TimedDespawn
     lifetime: 0.4
diff --git a/Resources/Prototypes/Entities/Effects/radiation.yml b/Resources/Prototypes/Entities/Effects/radiation.yml
index 143ffa8559a..82828582cff 100644
--- a/Resources/Prototypes/Entities/Effects/radiation.yml
+++ b/Resources/Prototypes/Entities/Effects/radiation.yml
@@ -1,7 +1,7 @@
 - type: entity
   name: shimmering anomaly
   id: RadiationPulse
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   description: Looking at this anomaly makes you feel strange, like something is pushing at your eyes.
   components:
   - type: RadiationSource
diff --git a/Resources/Prototypes/Entities/Effects/rcd.yml b/Resources/Prototypes/Entities/Effects/rcd.yml
index 902429818e5..a663add9ca9 100644
--- a/Resources/Prototypes/Entities/Effects/rcd.yml
+++ b/Resources/Prototypes/Entities/Effects/rcd.yml
@@ -1,7 +1,7 @@
 - type: entity
   id: EffectRCDBase
   abstract: true
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Transform
     anchored: True
@@ -19,7 +19,7 @@
 - type: entity
   parent: EffectRCDBase
   id: EffectRCDDeconstructPreview
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     state: deconstructPreview
@@ -27,7 +27,7 @@
 - type: entity
   parent: EffectRCDBase
   id: EffectRCDConstruct0
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     state: construct0
@@ -37,7 +37,7 @@
 - type: entity
   parent: EffectRCDBase
   id: EffectRCDConstruct1
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     state: construct1
@@ -47,7 +47,7 @@
 - type: entity
   parent: EffectRCDBase
   id: EffectRCDConstruct2
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     state: construct2
@@ -57,7 +57,7 @@
 - type: entity
   parent: EffectRCDBase
   id: EffectRCDConstruct3
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     state: construct3
@@ -67,7 +67,7 @@
 - type: entity
   parent: EffectRCDBase
   id: EffectRCDConstruct4
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     state: construct4
@@ -77,7 +77,7 @@
 - type: entity
   parent: EffectRCDBase
   id: EffectRCDDeconstruct2
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     state: deconstruct2
@@ -87,7 +87,7 @@
 - type: entity
   parent: EffectRCDBase
   id: EffectRCDDeconstruct4
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     state: deconstruct4
@@ -97,7 +97,7 @@
 - type: entity
   parent: EffectRCDBase
   id: EffectRCDDeconstruct6
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     state: deconstruct6
@@ -107,7 +107,7 @@
 - type: entity
   parent: EffectRCDBase
   id: EffectRCDDeconstruct8
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     state: deconstruct8
diff --git a/Resources/Prototypes/Entities/Effects/shuttle.yml b/Resources/Prototypes/Entities/Effects/shuttle.yml
new file mode 100644
index 00000000000..72cc04cba11
--- /dev/null
+++ b/Resources/Prototypes/Entities/Effects/shuttle.yml
@@ -0,0 +1,12 @@
+- type: entity
+  id: FtlVisualizerEntity
+  categories: [ HideSpawnMenu ]
+  description: Visualizer for shuttles arriving. You shouldn't see this!
+  components:
+  - type: FtlVisualizer
+    sprite:
+      sprite: /Textures/Effects/medi_holo.rsi
+      state: medi_holo
+  - type: Tag
+    tags:
+    - HideContextMenu
diff --git a/Resources/Prototypes/Entities/Effects/sparks.yml b/Resources/Prototypes/Entities/Effects/sparks.yml
index d86522a9711..c12e3b0eca3 100644
--- a/Resources/Prototypes/Entities/Effects/sparks.yml
+++ b/Resources/Prototypes/Entities/Effects/sparks.yml
@@ -1,6 +1,6 @@
 - type: entity
   id: EffectSparks
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TimedDespawn
     lifetime: 0.5
@@ -20,7 +20,7 @@
 
 - type: entity
   id: EffectTeslaSparks
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TimedDespawn
     lifetime: 0.5
diff --git a/Resources/Prototypes/Entities/Effects/weapon_arc.yml b/Resources/Prototypes/Entities/Effects/weapon_arc.yml
index 25cd6e84ff2..0f3fab0e878 100644
--- a/Resources/Prototypes/Entities/Effects/weapon_arc.yml
+++ b/Resources/Prototypes/Entities/Effects/weapon_arc.yml
@@ -1,7 +1,7 @@
 - type: entity
   # Just fades out with no movement animation
   id: WeaponArcStatic
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: TimedDespawn
       lifetime: 2.0
@@ -18,7 +18,7 @@
 - type: entity
   # Plays the state animation then disappears with no fade or swing
   id: WeaponArcAnimated
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       sprite: Effects/arcs.rsi
@@ -34,7 +34,7 @@
 - type: entity
   id: WeaponArcThrust
   parent: WeaponArcStatic
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: WeaponArcVisuals
       animation: Thrust
@@ -42,7 +42,7 @@
 - type: entity
   id: WeaponArcSlash
   parent: WeaponArcStatic
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: WeaponArcVisuals
       animation: Slash
@@ -51,7 +51,7 @@
 - type: entity
   id: WeaponArcBite
   parent: WeaponArcStatic
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: WeaponArcVisuals
       fadeOut: false
@@ -63,7 +63,7 @@
 - type: entity
   id: WeaponArcClaw
   parent: WeaponArcStatic
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: WeaponArcVisuals
       fadeOut: false
@@ -75,7 +75,7 @@
 - type: entity
   id: WeaponArcDisarm
   parent: WeaponArcAnimated
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: WeaponArcVisuals
       fadeOut: false
@@ -87,7 +87,7 @@
 - type: entity
   id: WeaponArcFist
   parent: WeaponArcStatic
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       state: fist
@@ -95,7 +95,7 @@
 - type: entity
   id: WeaponArcPunch
   parent: WeaponArcStatic
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: WeaponArcVisuals
       fadeOut: false
@@ -107,7 +107,7 @@
 - type: entity
   id: WeaponArcKick
   parent: WeaponArcStatic
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: WeaponArcVisuals
       fadeOut: false
@@ -119,7 +119,7 @@
 - type: entity
   id: WeaponArcSmash
   parent: WeaponArcStatic
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: WeaponArcVisuals
       fadeOut: false
diff --git a/Resources/Prototypes/Entities/Markers/Spawners/Random/maintenance.yml b/Resources/Prototypes/Entities/Markers/Spawners/Random/maintenance.yml
index 32594392587..3430c761fb5 100644
--- a/Resources/Prototypes/Entities/Markers/Spawners/Random/maintenance.yml
+++ b/Resources/Prototypes/Entities/Markers/Spawners/Random/maintenance.yml
@@ -1,137 +1,473 @@
+- type: entityTable
+  id: MaintFluffTable
+  table: !type:GroupSelector
+    children:
+    # Common Group
+    - !type:GroupSelector
+      weight: 75
+      children:
+      # Smoker's specialty
+      - !type:AllSelector
+        children:
+        - !type:EntSelector
+          id: Lighter
+        - !type:EntSelector
+          id: CigCartonBlue
+      # Gar glasses
+      - !type:GroupSelector
+        children:
+        - !type:EntSelector
+          id: ClothingEyesGlassesGar
+        - !type:EntSelector
+          id: ClothingEyesGlassesGarOrange
+        - !type:EntSelector
+          id: ClothingEyesGlassesGarGiga
+      - !type:EntSelector
+        id: ClothingHeadHatCake
+      - !type:EntSelector
+        id: ClothingHeadHatSkub
+      - !type:EntSelector
+        id: ClothingHeadHatCone
+      - !type:EntSelector
+        id: ClothingNeckBling
+      - !type:EntSelector
+        id: ClothingHeadHelmetCosmonaut
+      - !type:EntSelector
+        id: ClothingHeadHelmetBasic
+      - !type:EntSelector
+        id: ClothingShoeSlippersDuck
+      - !type:EntSelector
+        id: ClothingUnderSocksBee
+      - !type:EntSelector
+        id: ClothingUnderSocksCoder
+      - !type:EntSelector
+        id: ClothingHeadHatSquid
+      # Animal Masks
+      - !type:GroupSelector
+        children:
+        - !type:EntSelector
+          id: ClothingMaskRat
+        - !type:EntSelector
+          id: ClothingMaskFox
+        - !type:EntSelector
+          id: ClothingMaskBee
+        - !type:EntSelector
+          id: ClothingMaskBear
+        - !type:EntSelector
+          id: ClothingMaskRaven
+        - !type:EntSelector
+          id: ClothingMaskJackal
+        - !type:EntSelector
+          id: ClothingMaskBat
+      - !type:EntSelector
+        id: ClothingBeltSuspenders
+      - !type:EntSelector
+        id: ClothingEyesEyepatch
+      - !type:EntSelector
+        id: ClothingEyesGlasses
+      - !type:EntSelector
+        id: ClothingHandsGlovesLatex
+      - !type:EntSelector
+        id: ClothingHandsGlovesFingerless
+      - !type:EntSelector
+        id: ClothingHandsGlovesColorBlack
+      - !type:EntSelector
+        id: ClothingHeadHatBeret
+      - !type:EntSelector
+        id: ClothingHeadHatBowlerHat
+      - !type:EntSelector
+        id: ClothingHeadHatFedoraBrown
+        weight: 0.5
+      - !type:EntSelector
+        id: ClothingHeadHatFedoraGrey
+        weight: 0.5
+      - !type:EntSelector
+        id: ClothingHeadHatFez
+      - !type:EntSelector
+        id: ClothingHeadHatPaper
+      - !type:EntSelector
+        id: ClothingHeadHatPirate
+      - !type:EntSelector
+        id: ClothingMaskSterile
+      - !type:EntSelector
+        id: ClothingNeckHeadphones
+      - !type:EntSelector
+        id: ClothingNeckTieRed
+      - !type:EntSelector
+        id: ClothingOuterCoatGentle
+      - !type:AllSelector
+        children:
+        - !type:EntSelector
+          id: ClothingOuterCoatJensen
+        - !type:EntSelector
+          id: ClothingEyesGlassesJensen
+      - !type:EntSelector
+        id: ClothingOuterCoatLab
+      - !type:AllSelector
+        children:
+        - !type:EntSelector
+          id: ClothingOuterCoatPirate
+        - !type:EntSelector
+          id: ClothingHeadHatPirateTricord
+      - !type:EntSelector
+        id: ClothingHeadHatTophat
+      - !type:EntSelector
+        id: ClothingOuterHoodieBlack
+        weight: 0.5
+      - !type:EntSelector
+        id: ClothingOuterHoodieGrey
+        weight: 0.5
+      - !type:GroupSelector
+        children:
+        - !type:EntSelector
+          id: ClothingOuterFlannelRed
+        - !type:EntSelector
+          id: ClothingOuterFlannelBlue
+        - !type:EntSelector
+          id: ClothingOuterFlannelGreen
+      - !type:EntSelector
+        id: ClothingOuterVestHazard
+      - !type:EntSelector
+        id: ClothingShoesBootsJack
+      - !type:EntSelector
+        id: ClothingShoesHighheelBoots
+      - !type:EntSelector
+        id: ClothingShoesBootsLaceup
+      - !type:EntSelector
+        id: ClothingShoesLeather
+      - !type:EntSelector
+        id: ClothingShoesBootsSalvage
+      - !type:EntSelector
+        id: ClothingShoesBootsWork
+      - !type:EntSelector
+        id: ClothingShoesTourist
+      - !type:EntSelector
+        id: ClothingUniformJumpsuitLoungewear
+      - !type:EntSelector
+        id: ClothingHeadHatCowboyRed
+    # Uncommon Group
+    - !type:GroupSelector
+      weight: 23
+      children:
+      - !type:EntSelector
+        id: ClothingNeckCloakHerald
+      - !type:EntSelector
+        id: ClothingHeadHelmetTemplar
+      # Cloaks
+      - !type:GroupSelector
+        children:
+        - !type:EntSelector
+          id: ClothingNeckCloakTrans
+        - !type:EntSelector
+          id: ClothingNeckCloakAdmin
+        - !type:EntSelector
+          id: ClothingNeckCloakMoth
+        - !type:EntSelector
+          id: ClothingNeckCloakVoid
+        - !type:EntSelector
+          id: ClothingNeckCloakGoliathCloak
+        - !type:EntSelector
+          id: ClothingNeckCloakAce
+        - !type:EntSelector
+          id: ClothingNeckCloakAro
+        - !type:EntSelector
+          id: ClothingNeckCloakBi
+        - !type:EntSelector
+          id: ClothingNeckCloakIntersex
+        - !type:EntSelector
+          id: ClothingNeckCloakLesbian
+        - !type:EntSelector
+          id: ClothingNeckCloakGay
+        - !type:EntSelector
+          id: ClothingNeckCloakEnby
+        - !type:EntSelector
+          id: ClothingNeckCloakPan
+      - !type:EntSelector
+        id: ToySkeleton
+      - !type:EntSelector
+        id: Basketball
+      - !type:EntSelector
+        id: Football
+      - !type:EntSelector
+        id: BalloonNT
+      - !type:EntSelector
+        id: BalloonCorgi
+      - !type:EntSelector
+        id: MysteryFigureBox
+      # Cult
+      - !type:AllSelector
+        children:
+        - !type:EntSelector
+          id: ClothingOuterRobesCult
+        - !type:EntSelector
+          id: ClothingShoesCult
+      - !type:EntSelector
+        id: ClothingHandsGlovesMercFingerless
+      - !type:EntSelector
+        id: ClothingHandsGlovesNitrile
+      - !type:EntSelector
+        id: ClothingHandsGlovesPowerglove
+      - !type:EntSelector
+        id: ClothingHeadHatAnimalHeadslime
+      - !type:EntSelector
+        id: ClothingHeadHatBeretMerc
+      - !type:EntSelector
+        id: ClothingHeadHatOutlawHat
+      - !type:EntSelector
+        id: ClothingHeadHatUshanka
+      - !type:EntSelector
+        id: ClothingHeadHatBunny
+      - !type:EntSelector
+        id: ClothingMaskNeckGaiter
+      - !type:EntSelector
+        id: ClothingNeckScarfStripedZebra
+      - !type:EntSelector
+        id: ClothingOuterGhostSheet
+      - !type:EntSelector
+        id: ClothingUniformJumpsuitAncient
+      - !type:EntSelector
+        id: ClothingUniformJumpsuitPirate
+      - !type:EntSelector
+        id: ClothingShoesBootsCowboyFancy
+      - !type:EntSelector
+        id: ClothingHeadHatCowboyBountyHunter
+      # Pins
+      - !type:GroupSelector
+        children:
+        - !type:EntSelector
+          id: ClothingNeckLGBTPin
+        - !type:EntSelector
+          id: ClothingNeckAromanticPin
+        - !type:EntSelector
+          id: ClothingNeckAsexualPin
+        - !type:EntSelector
+          id: ClothingNeckBisexualPin
+        - !type:EntSelector
+          id: ClothingNeckIntersexPin
+        - !type:EntSelector
+          id: ClothingNeckLesbianPin
+        - !type:EntSelector
+          id: ClothingNeckNonBinaryPin
+        - !type:EntSelector
+          id: ClothingNeckPansexualPin
+        - !type:EntSelector
+          id: ClothingNeckTransPin
+        - !type:EntSelector
+          id: ClothingNeckAutismPin
+        - !type:EntSelector
+          id: ClothingNeckGoldAutismPin
+    # Rare Group
+    - !type:GroupSelector
+      weight: 2
+      children:
+      - !type:EntSelector
+        id: Skub
+      - !type:EntSelector
+        id: PonderingOrb
+      - !type:EntSelector
+        id: CluwneHorn
+      - !type:EntSelector
+        id: ClothingShoesSkates
+      - !type:EntSelector
+        id: DrinkMugDog
+      - !type:EntSelector
+        id: CigarGold
+      - !type:EntSelector
+        id: ClothingUniformJumpsuitFamilyGuy
+
 - type: entity
   name: Maint Loot Spawner
   suffix: Fluff+Clothes
   id: MaintenanceFluffSpawner
   parent: MarkerBase
   components:
-    - type: Sprite
-      layers:
-        - state: red
-        - sprite: Clothing/Eyes/Glasses/gar.rsi
-          state: icon-super
-    - type: RandomSpawner
-      rarePrototypes:
-        - ClothingUniformJumpsuitFamilyGuy
-        - CigarGold
-        - ClothingNeckCloakHerald
-        - ClothingHeadHelmetTemplar
-        - ClothingNeckCloakTrans
-        - ClothingNeckCloakAdmin
-        - ClothingNeckCloakBoat # DeltaV - adds boat cloak to maints spawner. Makes more sense than admin cloak o.o
-        - ClothingNeckCloakMoth
-        - ClothingNeckCloakVoid
-        - ClothingNeckCloakGoliathCloak
-        - ClothingNeckCloakAce
-        - ClothingNeckCloakAro
-        - ClothingNeckCloakBi
-        - ClothingNeckCloakIntersex
-        - ClothingNeckCloakLesbian
-        - ClothingNeckCloakGay
-        - ClothingNeckCloakEnby
-        - ClothingNeckCloakPan
-        - ToySkeleton
-        - Basketball
-        - Football
-        - BalloonCorgi
-        - BalloonNT
-        - PonderingOrb
-        - Skub
-        - DrinkMugDog
-        - ClothingNeckLGBTPin
-        - ClothingNeckAromanticPin
-        - ClothingNeckAsexualPin
-        - ClothingNeckBisexualPin
-        - ClothingNeckIntersexPin
-        - ClothingNeckLesbianPin
-        - ClothingNeckNonBinaryPin
-        - ClothingNeckPansexualPin
-        - ClothingNeckTransPin
-        - CluwneHorn
-        - ClothingMaskRat
-        - MysteryFigureBox
-        - ClothingHandsGlovesMercFingerless
-        - ClothingHandsGlovesNitrile
-        - ClothingHandsGlovesPowerglove
-        - ClothingHeadHatAnimalHeadslime
-        - ClothingHeadHatBeretMerc
-        - ClothingHeadHatOutlawHat
-        - ClothingHeadHatUshanka
-        - ClothingHeadHatBunny
-        - ClothingMaskNeckGaiter
-        - ClothingNeckScarfStripedZebra
-        - ClothingOuterRobesCult
-        - ClothingOuterGhostSheet
-        - ClothingShoesCult
-        - ClothingUniformJumpsuitAncient
-        - ClothingUniformJumpsuitPirate
-        - ClothingShoesBootsCowboyFancy
-        - ClothingHeadHatCowboyBountyHunter
-        - ClothingNeckAutismPin
-        - ClothingNeckGoldAutismPin
-      rareChance: 0.01
-      prototypes:
-        - Lighter
-        - CigCartonBlue
-        - ClothingEyesGlassesGarGiga
-        - ClothingEyesGlassesGarOrange
-        - ClothingEyesGlassesGar
-        - ClothingHeadHatCake
-        - ClothingHeadHatSkub
-        - ClothingHeadHatCone
-        - ClothingNeckBling
-        - ClothingHeadHelmetCosmonaut
-        - ClothingHeadHelmetBasic
-        - ClothingShoeSlippersDuck
-        - ClothingUnderSocksBee
-        - ClothingUnderSocksCoder
-        - ClothingHeadHatSquid
-        - ClothingMaskFox
-        - ClothingMaskBee
-        - ClothingMaskBear
-        - ClothingMaskRaven
-        - ClothingMaskJackal
-        - ClothingMaskBat
-        - ClothingBeltSuspenders
-        - ClothingEyesEyepatch
-        - ClothingEyesGlasses
-        - ClothingHandsGlovesLatex
-        - ClothingHandsGlovesFingerless
-        - ClothingHandsGlovesColorBlack
-        - ClothingHeadHatBeret
-        - ClothingHeadHatBowlerHat
-        - ClothingHeadHatFedoraBrown
-        - ClothingHeadHatFedoraGrey
-        - ClothingHeadHatFez
-        - ClothingHeadHatPaper
-        - ClothingHeadHatPirate
-        - ClothingHeadHatPirateTricord
-        - ClothingHeadHatTophat
-        - ClothingMaskSterile
-        - ClothingNeckHeadphones
-        - ClothingNeckTieRed
-        - ClothingOuterCoatGentle
-        - ClothingOuterCoatJensen
-        - ClothingEyesGlassesJensen
-        - ClothingOuterCoatLab
-        - ClothingOuterCoatPirate
-        - ClothingOuterHoodieBlack
-        - ClothingOuterHoodieGrey
-        - ClothingOuterFlannelRed
-        - ClothingOuterFlannelBlue
-        - ClothingOuterFlannelGreen
-        - ClothingOuterVestHazard
-        - ClothingShoesBootsJack
-        - ClothingShoesHighheelBoots
-        - ClothingShoesBootsLaceup
-        - ClothingShoesLeather
-        - ClothingShoesBootsSalvage
-        - ClothingShoesBootsWork
-        - ClothingShoesTourist
-        - ClothingUniformJumpsuitLoungewear
-        - ClothingHeadHatCowboyRed
-      chance: 0.6
-      offset: 0.0
+  - type: Sprite
+    layers:
+      - state: red
+      - sprite: Clothing/Eyes/Glasses/gar.rsi
+        state: icon-super
+  - type: EntityTableSpawner
+    table: !type:NestedSelector
+      tableId: MaintFluffTable
+      prob: 0.6
 
+- type: entityTable
+  id: MaintToolsTable
+  table: !type:GroupSelector
+    children:
+    # Common Group
+    - !type:GroupSelector
+      weight: 75
+      children:
+      - !type:EntSelector
+        id: FlashlightLantern
+      - !type:EntSelector
+        id: ToolboxEmergencyFilled
+      - !type:GroupSelector
+        children:
+        - !type:EntSelector
+          id: OxygenTankFilled
+        - !type:EntSelector
+          id: DoubleEmergencyOxygenTankFilled
+      - !type:GroupSelector
+        children:
+        - !type:EntSelector
+          id: NitrogenTankFilled
+        - !type:EntSelector
+          id: DoubleEmergencyNitrogenTankFilled
+      - !type:EntSelector
+        id: EmergencyFunnyOxygenTankFilled
+        weight: 0.5
+      - !type:GroupSelector
+        weight: 3
+        children:
+        - !type:EntSelector
+          id: SheetSteel10
+        - !type:EntSelector
+          id: SheetPlastic10
+        - !type:EntSelector
+          id: SheetGlass10
+        - !type:EntSelector
+          id: PartRodMetal10
+        - !type:EntSelector
+          id: MaterialCardboard10
+          weight: 0.25
+        - !type:EntSelector
+          id: MaterialCloth10
+          weight: 0.25
+        - !type:EntSelector
+          id: MaterialWoodPlank10
+          weight: 0.25
+      - !type:EntSelector
+        id: Plunger
+      - !type:EntSelector
+        id: PowerCellMedium
+      - !type:EntSelector
+        id: PowerCellSmall
+      - !type:EntSelector
+        id: Soap
+      - !type:EntSelector
+        id: Wirecutter
+      - !type:EntSelector
+        id: Screwdriver
+      - !type:EntSelector
+        id: Wrench
+      - !type:EntSelector
+        id: Crowbar
+      - !type:EntSelector
+        id: Multitool
+      - !type:EntSelector
+        id: Shovel
+      - !type:EntSelector
+        id: Welder
+      - !type:EntSelector
+        id: GasAnalyzer
+      - !type:EntSelector
+        id: SprayPainter
+      - !type:EntSelector
+        id: Flare
+      - !type:EntSelector
+        id: Beaker
+      - !type:EntSelector
+        id: ClothingMaskGas
+      - !type:EntSelector
+        id: ClothingMaskBreath
+      - !type:EntSelector
+        id: DoorElectronics
+      - !type:EntSelector
+        id: APCElectronics
+      - !type:EntSelector
+        id: InflatableWallStack5
+      - !type:EntSelector
+        id: CableHVStack10
+      - !type:EntSelector
+        id: CableMVStack10
+      - !type:EntSelector
+        id: CableApcStack10
+      - !type:GroupSelector
+        children:
+        - !type:EntSelector
+          id: ClothingHandsGlovesColorYellowBudget
+          weight: 5
+        - !type:EntSelector
+          id: ClothingHandsGlovesFingerlessInsulated
+          weight: 0.5
+        - !type:EntSelector
+          id: ClothingHandsGlovesColorYellow
+          weight: 1
+      # Uncommon Group
+    - !type:GroupSelector
+      weight: 23
+      children:
+      - !type:EntSelector
+        id: ClothingHeadHatCone
+        weight: 2
+      - !type:EntSelector
+        id: BookRandomStory
+        weight: 0.25
+      - !type:EntSelector
+        id: ToolboxElectricalFilled
+      - !type:EntSelector
+        id: ToolboxMechanicalFilled
+      - !type:EntSelector
+        id: ClothingBeltUtility
+      - !type:EntSelector
+        id: ToolboxArtisticFilled
+      - !type:EntSelector
+        id: GeigerCounter
+      - !type:EntSelector
+        id: trayScanner
+      - !type:EntSelector
+        id: HandheldGPSBasic
+      - !type:EntSelector
+        id: HandLabeler
+      - !type:EntSelector
+        id: GlowstickBase
+      - !type:EntSelector
+        id: Bucket
+      - !type:EntSelector
+        id: RadioHandheld
+      - !type:EntSelector
+        id: AppraisalTool
+      - !type:EntSelector
+        id: ModularReceiver
+      - !type:EntSelector
+        id: WeaponFlareGun
+      - !type:EntSelector
+        id: BarberScissors
+      - !type:GroupSelector
+        children:
+        - !type:EntSelector
+          id: DrinkSpaceGlue
+        - !type:EntSelector
+          id: DrinkSpaceLube
+      # Rare Group
+    - !type:GroupSelector
+      weight: 2
+      children:
+      - !type:EntSelector
+        id: LanternFlash
+      - !type:EntSelector
+        id: PowerCellHigh
+      - !type:EntSelector
+        id: NetProbeCartridge
+      - !type:EntSelector
+        id: WelderIndustrial
+      - !type:EntSelector
+        id: SheetPlasteel10
+      - !type:EntSelector
+        id: ClothingMaskGasExplorer
+      - !type:EntSelector
+        id: TechnologyDisk
+      - !type:EntSelector
+        id: ResearchDisk5000
+      - !type:EntSelector
+        id: PetCarrier
+      - !type:EntSelector
+        id: DrinkMopwataBottleRandom
+      - !type:EntSelector
+        id: LidSalami
+        weight: 0.05
 
 - type: entity
   name: Maint Loot Spawner
@@ -139,80 +475,80 @@
   id: MaintenanceToolSpawner
   parent: MarkerBase
   components:
-    - type: Sprite
-      layers:
-        - state: red
-        - sprite: Objects/Power/power_cells.rsi
-          state: high
-    - type: RandomSpawner
-      rarePrototypes:
-        - LanternFlash
-        - PowerCellHigh
-        - NetProbeCartridge
-        - WelderIndustrial
-        - SheetPlasteel10
-        - ClothingMaskGasExplorer
-      rareChance: 0.08
-      prototypes:
-        - FlashlightLantern
-        - OxygenTankFilled
-        - DoubleEmergencyOxygenTankFilled
-        - ToolboxEmergencyFilled
-        - ToolboxArtisticFilled
-        - NitrogenTankFilled
-        - DoubleEmergencyNitrogenTankFilled
-        - EmergencyFunnyOxygenTankFilled
-        - ToolboxElectricalFilled
-        - ToolboxMechanicalFilled
-        - ClothingBeltUtility
-        - Shovel
-        - Welder
-        - WeaponFlareGun
-        - SheetSteel10
-        - SheetPlastic10
-        - SheetGlass10
-        - PartRodMetal10
-        - MaterialCardboard10
-        - MaterialCloth10
-        - MaterialWoodPlank10
-        - ResearchDisk
-        - DeathPaint
-        - Plunger
-        - SprayPaintBlue
-        - SprayPaintRed
-        - SprayPaintGreen
-        - SprayPaintOrange
-        - TechnologyDisk
-        - PowerCellMedium
-        - PowerCellSmall
-        - Wirecutter
-        - Screwdriver
-        - Wrench
-        - Crowbar
-        - NetworkConfigurator
-        - trayScanner
-        - GasAnalyzer
-        - SprayPainter
-        - AppraisalTool
-        - Flare
-        - HandheldGPSBasic
-        - HandLabeler
-        - GlowstickBase
-        - Bucket
-        - RadioHandheld
-        - GeigerCounter
-        - Beaker
-        - ClothingMaskGas
-        - ClothingMaskBreath
-        - DoorElectronics
-        - APCElectronics
-        - InflatableWallStack5
-        - CableHVStack10
-        - CableMVStack10
-        - CableApcStack10
-        - PetCarrier
-      chance: 0.6
-      offset: 0.0
+  - type: Sprite
+    layers:
+      - state: red
+      - sprite: Objects/Power/power_cells.rsi
+        state: high
+  - type: EntityTableSpawner
+    table: !type:NestedSelector
+      tableId: MaintToolsTable
+      prob: 0.6
+
+- type: entityTable
+  id: MaintWeaponTable
+  table: !type:GroupSelector
+    children:
+    # Common Group
+    - !type:GroupSelector
+      weight: 95
+      children:
+      - !type:EntSelector
+        id: Machete
+      - !type:EntSelector
+        id: BaseBallBat
+      - !type:EntSelector
+        id: CombatKnife
+      - !type:EntSelector
+        id: Spear
+      - !type:EntSelector
+        id: RifleStock
+      - !type:EntSelector
+        id: ModularReceiver
+      - !type:EntSelector
+        id: HydroponicsToolScythe
+    # Rare Group
+    - !type:GroupSelector
+      weight: 5
+      children:
+      - !type:EntSelector
+        id: Lighter
+      - !type:EntSelector
+        id: Matchbox
+      - !type:EntSelector
+        id: ClothingEyesBlindfold
+      - !type:EntSelector
+        id: ClothingMaskMuzzle
+      - !type:EntSelector
+        id: ClothingMaskGasSecurity
+      - !type:EntSelector
+        id: ShardGlass
+        weight: 2
+      - !type:EntSelector
+        id: Syringe
+      - !type:EntSelector
+        id: Mousetrap
+      - !type:GroupSelector
+        weight: 2
+        children:
+        - !type:EntSelector
+          id: Brutepack1
+        - !type:EntSelector
+          id: Ointment1
+        - !type:EntSelector
+          id: Gauze1
+      - !type:EntSelector
+        id: Bola
+      - !type:EntSelector
+        id: SurvivalKnife
+      - !type:EntSelector
+        id: ScalpelShiv
+      - !type:EntSelector
+        id: Shiv
+      - !type:EntSelector
+        id: SawImprov
+      - !type:EntSelector
+        id: HydroponicsToolMiniHoe
 
 - type: entity
   name: Maint Loot Spawner
@@ -220,51 +556,15 @@
   id: MaintenanceWeaponSpawner
   parent: MarkerBase
   components:
-    - type: Sprite
-      layers:
-        - state: red
-        - sprite: Objects/Weapons/Melee/machete.rsi
-          state: icon
-    - type: RandomSpawner
-      rarePrototypes:
-        - Machete
-        - BaseBallBat
-        - CombatKnife
-        - Spear
-        - RifleStock
-        - ModularReceiver
-        - HydroponicsToolScythe
-      rareChance: 0.05
-      prototypes:
-        - FlashlightLantern
-        - OxygenTankFilled
-        - DoubleEmergencyOxygenTankFilled
-        - NitrogenTankFilled
-        - DoubleEmergencyNitrogenTankFilled
-        - Lighter
-        - Matchbox
-        - Crowbar
-        - Shovel
-        - Welder
-        - WeaponFlareGun
-        - LidSalami
-        - ClothingEyesBlindfold
-        - ClothingMaskMuzzle
-        - ClothingMaskGasSecurity
-        - ShardGlass
-        - Syringe
-        - Mousetrap
-        - Brutepack1
-        - Ointment1
-        - Gauze1
-        - Bola
-        - SurvivalKnife
-        - ScalpelShiv
-        - Shiv
-        - SawImprov
-        - HydroponicsToolMiniHoe
-      chance: 0.6
-      offset: 0.0
+  - type: Sprite
+    layers:
+      - state: red
+      - sprite: Objects/Weapons/Melee/machete.rsi
+        state: icon
+  - type: EntityTableSpawner
+    table: !type:NestedSelector
+      tableId: MaintWeaponTable
+      prob: 0.6
 
 - type: entity
   name: Maint Loot Spawner
diff --git a/Resources/Prototypes/Entities/Markers/Spawners/corpses.yml b/Resources/Prototypes/Entities/Markers/Spawners/corpses.yml
index 3d337b39765..95064428dde 100644
--- a/Resources/Prototypes/Entities/Markers/Spawners/corpses.yml
+++ b/Resources/Prototypes/Entities/Markers/Spawners/corpses.yml
@@ -80,7 +80,7 @@
   name: Random Security Corpse Spawner
   id: RandomSecurityCorpseSpawner
   parent: SalvageHumanCorpseSpawner
-  noSpawn: true # DeltaV - Prevent security corpses from being mapped in
+  categories: [ HideSpawnMenu ] # DeltaV - Prevent security corpses from being mapped in
   components:
   - type: Sprite
     layers:
diff --git a/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml b/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml
index 4ade9a9fd45..3911a686266 100644
--- a/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml
+++ b/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml
@@ -62,7 +62,7 @@
         state: narsian
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   id: SpawnPointGhostNukeOperative
   name: ghost role spawn point
   suffix: nukeops
@@ -82,7 +82,7 @@
         state: radiation
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   id: SpawnPointLoneNukeOperative
   name: ghost role spawn point
   suffix: loneops
@@ -111,7 +111,7 @@
 - type: entity
   parent: MarkerBase
   id: SpawnPointGhostDragon
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: ghost role spawn point
   suffix: dragon
   components:
diff --git a/Resources/Prototypes/Entities/Markers/clientsideclone.yml b/Resources/Prototypes/Entities/Markers/clientsideclone.yml
index 56875c44142..39b9ee48fdb 100644
--- a/Resources/Prototypes/Entities/Markers/clientsideclone.yml
+++ b/Resources/Prototypes/Entities/Markers/clientsideclone.yml
@@ -1,7 +1,7 @@
 - type: entity
   name: clientsideclone
   id: clientsideclone
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
   - type: AnimationPlayer
diff --git a/Resources/Prototypes/Entities/Markers/construction_ghost.yml b/Resources/Prototypes/Entities/Markers/construction_ghost.yml
index be9cc915d91..04a8a095023 100644
--- a/Resources/Prototypes/Entities/Markers/construction_ghost.yml
+++ b/Resources/Prototypes/Entities/Markers/construction_ghost.yml
@@ -1,7 +1,7 @@
 - type: entity
   name: construction ghost
   id: constructionghost
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     color: '#3F38'
diff --git a/Resources/Prototypes/Entities/Markers/drag_shadow.yml b/Resources/Prototypes/Entities/Markers/drag_shadow.yml
index a1badb60bc5..19ffb6c36a6 100644
--- a/Resources/Prototypes/Entities/Markers/drag_shadow.yml
+++ b/Resources/Prototypes/Entities/Markers/drag_shadow.yml
@@ -1,7 +1,7 @@
 - type: entity
   name: drag shadow
   id: dragshadow
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Tag
     tags:
diff --git a/Resources/Prototypes/Entities/Markers/hover_entity.yml b/Resources/Prototypes/Entities/Markers/hover_entity.yml
index 8421a9d8c9e..2e8e1edb296 100644
--- a/Resources/Prototypes/Entities/Markers/hover_entity.yml
+++ b/Resources/Prototypes/Entities/Markers/hover_entity.yml
@@ -1,7 +1,7 @@
 - type: entity
   name: hover entity
   id: hoverentity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       layers:
diff --git a/Resources/Prototypes/Entities/Mobs/Corpses/corpses.yml b/Resources/Prototypes/Entities/Mobs/Corpses/corpses.yml
index 89ab3376c6a..848865cf3f1 100644
--- a/Resources/Prototypes/Entities/Mobs/Corpses/corpses.yml
+++ b/Resources/Prototypes/Entities/Mobs/Corpses/corpses.yml
@@ -68,7 +68,7 @@
   parent: SalvageHumanCorpse
   id: MobRandomSecurityCorpse
   suffix: Dead, Security
-  noSpawn: true # DeltaV - Prevent security corpses from being mapped in
+  categories: [ HideSpawnMenu ] # DeltaV - Prevent security corpses from being mapped in
   components:
   - type: Loadout
     prototypes:
diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/Rslimes.yml b/Resources/Prototypes/Entities/Mobs/NPCs/Rslimes.yml
index 3512b518150..7a41ac52ec8 100644
--- a/Resources/Prototypes/Entities/Mobs/NPCs/Rslimes.yml
+++ b/Resources/Prototypes/Entities/Mobs/NPCs/Rslimes.yml
@@ -222,7 +222,7 @@
 - type: entity
   id: reagentslimeVents
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: true
diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
index cbecfbf6a6f..24947413d87 100644
--- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
+++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml
@@ -1577,7 +1577,7 @@
 - type: entity
   name: guidebook monkey
   parent: MobMonkey
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   id: MobGuidebookMonkey
   description: A hopefully helpful monkey whose only purpose in life is for you to click on. Does this count as having a monkey give you a tutorial?
   components:
diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml
index f6a42dfb76b..022ae53289e 100644
--- a/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml
+++ b/Resources/Prototypes/Entities/Mobs/NPCs/regalrat.yml
@@ -169,7 +169,7 @@
   id: MobRatServant
   parent: [ SimpleMobBase, MobCombat ]
   description: He's da mini rat. He don't make da roolz.
-  noSpawn: true #Must be configured to a King or the AI breaks.
+  categories: [ HideSpawnMenu ] #Must be configured to a King or the AI breaks.
   components:
   - type: Carriable
     freeHandsRequired: 1
@@ -320,7 +320,7 @@
   id: ActionRatKingRaiseArmy
   name: Raise Army
   description: Spend some hunger to summon an allied rat to help defend you.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     useDelay: 4
@@ -333,7 +333,7 @@
   id: ActionRatKingDomain
   name: Rat King's Domain
   description: Spend some hunger to release a cloud of ammonia into the air.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     useDelay: 6
@@ -346,7 +346,7 @@
   id: ActionRatKingOrderStay
   name: Stay
   description: Command your army to stand in place.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     useDelay: 1
@@ -365,7 +365,7 @@
   id: ActionRatKingOrderFollow
   name: Follow
   description: Command your army to follow you around.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     useDelay: 1
@@ -384,7 +384,7 @@
   id: ActionRatKingOrderCheeseEm
   name: Cheese 'Em
   description: Command your army to attack whoever you point at.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     useDelay: 1
@@ -403,7 +403,7 @@
   id: ActionRatKingOrderLoose
   name: Loose
   description: Command your army to act at their own will.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     useDelay: 1
diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml
index 74bba2dec8a..dec2bd50bac 100644
--- a/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml
+++ b/Resources/Prototypes/Entities/Mobs/NPCs/slimes.yml
@@ -150,7 +150,7 @@
   description: A geras of a slime - the name is ironic, isn't it?
   id: MobSlimesGeras
   parent: BaseMobAdultSlimes
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   # they portable...
   - type: MovementSpeedModifier
diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/xenopet.yml b/Resources/Prototypes/Entities/Mobs/NPCs/xenopet.yml
index 23108e521bf..37cfa7b46de 100644
--- a/Resources/Prototypes/Entities/Mobs/NPCs/xenopet.yml
+++ b/Resources/Prototypes/Entities/Mobs/NPCs/xenopet.yml
@@ -341,7 +341,7 @@
 - type: entity
   id: neutralXenoVents
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: true
@@ -364,7 +364,7 @@
 - type: entity
   id: ArgocyteVents
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: true
@@ -403,7 +403,7 @@
 - type: entity
   id: MeatVents
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: true
@@ -428,7 +428,7 @@
 - type: entity
   id: TickVents
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: true
@@ -445,7 +445,7 @@
 - type: entity
   id: CarpVents
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: true
@@ -472,7 +472,7 @@
 - type: entity
   id: SpaceVents
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: true
@@ -495,7 +495,7 @@
 - type: entity
   id: LightVents
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: true
diff --git a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml
index dce408ed827..957817d4083 100644
--- a/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml
+++ b/Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml
@@ -2,7 +2,7 @@
   parent: MobObserver
   id: AdminObserver
   name: admin observer
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Eye
     visMask:
@@ -104,7 +104,7 @@
   id: ActionAGhostShowSolar
   name: Solar Control Interface
   description: View a solar control interface.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: { sprite: Structures/Machines/parts.rsi, state: box_0 }
@@ -117,7 +117,7 @@
   id: ActionAGhostShowCommunications
   name: Communications Interface
   description: View a communications interface.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: { sprite: Structures/Machines/parts.rsi, state: box_0 }
@@ -130,7 +130,7 @@
   id: ActionAGhostShowRadar
   name: Mass Scanner Interface
   description: View a mass scanner interface.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: { sprite: Structures/Machines/parts.rsi, state: box_0 }
@@ -143,7 +143,7 @@
   id: ActionAGhostShowCargo
   name: Cargo Ordering Interface
   description: View a cargo ordering interface.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: { sprite: Structures/Machines/parts.rsi, state: box_0 }
@@ -156,7 +156,7 @@
   id: ActionAGhostShowCrewMonitoring
   name: Crew Monitoring Interface
   description: View a crew monitoring interface.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: { sprite: Structures/Machines/parts.rsi, state: box_0 }
@@ -169,7 +169,7 @@
   id: ActionAGhostShowStationRecords
   name: Station Records Interface
   description: View a station records Interface
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: { sprite: Structures/Machines/parts.rsi, state: box_0 }
diff --git a/Resources/Prototypes/Entities/Mobs/Player/diona.yml b/Resources/Prototypes/Entities/Mobs/Player/diona.yml
index dfd5e9a1be7..85c5631de7a 100644
--- a/Resources/Prototypes/Entities/Mobs/Player/diona.yml
+++ b/Resources/Prototypes/Entities/Mobs/Player/diona.yml
@@ -15,7 +15,7 @@
 # Reformed Diona
 - type: entity
   parent: MobDiona
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   id: MobDionaReformed
   name: Reformed Diona
   components:
diff --git a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml
index cb9dc1c911a..81cd8a579b2 100644
--- a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml
+++ b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml
@@ -155,7 +155,7 @@
     - MinorAntagonists
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   id: DragonsBreathGun
   name: dragon's lung
   description: For dragon's breathing
@@ -204,7 +204,7 @@
   id: ActionSpawnRift
   name: Summon Carp Rift
   description: Summons a carp rift that will periodically spawns carps.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon:
@@ -218,7 +218,7 @@
   id: ActionDevour
   name: "[color=red]Devour[/color]"
   description: Attempt to break a structure with your jaws or swallow a creature.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: EntityTargetAction
     icon: { sprite : Interface/Actions/devour.rsi, state: icon }
@@ -227,7 +227,7 @@
     priority: 1
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   id: ActionDragonsBreath
   name: "[color=orange]Dragon's Breath[/color]"
   description: Spew out flames at anyone foolish enough to attack you!
diff --git a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml
index 7bb8547cfab..7d1139d3d79 100644
--- a/Resources/Prototypes/Entities/Mobs/Player/guardian.yml
+++ b/Resources/Prototypes/Entities/Mobs/Player/guardian.yml
@@ -261,7 +261,7 @@
   id: ActionToggleGuardian
   name: Toggle Guardian
   description: Either manifests the guardian or recalls it back into your body
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: Interface/Actions/manifest.png
diff --git a/Resources/Prototypes/Entities/Mobs/Player/human.yml b/Resources/Prototypes/Entities/Mobs/Player/human.yml
index aa87f81a833..4284632d288 100644
--- a/Resources/Prototypes/Entities/Mobs/Player/human.yml
+++ b/Resources/Prototypes/Entities/Mobs/Player/human.yml
@@ -39,7 +39,7 @@
 
 # Nuclear Operative
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: Nuclear Operative
   parent: MobHuman
   id: MobHumanNukeOp
@@ -50,7 +50,7 @@
       powerRollMultiplier: 7
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: MobHuman
   id: MobHumanLoneNuclearOperative
   name: Lone Operative
@@ -72,7 +72,7 @@
 
 # Space Ninja
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: Space Ninja
   parent: MobHuman
   id: MobHumanSpaceNinja
diff --git a/Resources/Prototypes/Entities/Mobs/Player/ipc.yml b/Resources/Prototypes/Entities/Mobs/Player/ipc.yml
index 85aec21ac66..d80eb4699d1 100644
--- a/Resources/Prototypes/Entities/Mobs/Player/ipc.yml
+++ b/Resources/Prototypes/Entities/Mobs/Player/ipc.yml
@@ -128,7 +128,7 @@
   name: Urist McPositronic
   parent: MobHumanDummy
   id: MobIPCDummy
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   description: A dummy IPC meant to be used in character setup.
   components:
   - type: HumanoidAppearance
diff --git a/Resources/Prototypes/Entities/Mobs/Player/observer.yml b/Resources/Prototypes/Entities/Mobs/Player/observer.yml
index c92595ffc9d..07398299f37 100644
--- a/Resources/Prototypes/Entities/Mobs/Player/observer.yml
+++ b/Resources/Prototypes/Entities/Mobs/Player/observer.yml
@@ -3,7 +3,7 @@
   id: MobObserver
   name: observer
   description: Boo!
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: CargoSellBlacklist
   - type: Sprite
@@ -60,7 +60,7 @@
   id: ActionGhostBoo
   name: Boo!
   description: Scare your crew members because of boredom!
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: Interface/Actions/scream.png
@@ -72,7 +72,7 @@
   id: ActionToggleLighting
   name: Toggle All Lighting
   description: Toggle all light rendering to better observe dark areas.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: Interface/VerbIcons/light.svg.192dpi.png
@@ -84,7 +84,7 @@
   id: ActionToggleFov
   name: Toggle FoV
   description: Toggles field-of-view in order to see what players see.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: Interface/VerbIcons/vv.svg.192dpi.png
@@ -96,7 +96,7 @@
   id: ActionToggleGhosts
   name: Toggle Ghosts
   description: Toggle the visibility of other ghosts.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: { sprite: Mobs/Ghosts/ghost_human.rsi, state: icon }
@@ -108,7 +108,7 @@
   id: ActionToggleGhostHearing
   name: Toggle Ghost Hearing
   description: Toggle between hearing all messages and hearing only radio & nearby messages.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     checkCanInteract: false
diff --git a/Resources/Prototypes/Entities/Mobs/Player/replay_observer.yml b/Resources/Prototypes/Entities/Mobs/Player/replay_observer.yml
index 07deef857c3..f309707132e 100644
--- a/Resources/Prototypes/Entities/Mobs/Player/replay_observer.yml
+++ b/Resources/Prototypes/Entities/Mobs/Player/replay_observer.yml
@@ -1,7 +1,7 @@
 - type: entity
   parent: MobObserver
   id: ReplayObserver
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   save: false
   components:
   - type: MovementSpeedModifier
diff --git a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml
index 0f8998bdec8..c3ccb0330c3 100644
--- a/Resources/Prototypes/Entities/Mobs/Player/silicon.yml
+++ b/Resources/Prototypes/Entities/Mobs/Player/silicon.yml
@@ -50,4 +50,4 @@
     slots:
       cell_slot:
         name: power-cell-slot-component-slot-name-default
-        startingItem: PowerCellHyper
+        startingItem: PowerCellHyper
\ No newline at end of file
diff --git a/Resources/Prototypes/Entities/Mobs/Species/arachne.yml b/Resources/Prototypes/Entities/Mobs/Species/arachne.yml
index ddbdc57e0ad..f3bd68709f9 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/arachne.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/arachne.yml
@@ -144,7 +144,7 @@
   name: Urist McHands
   parent: MobHumanDummy
   id: MobArachneDummy
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   description: A dummy arachne meant to be used in character setup.
   components:
   - type: Sprite
diff --git a/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml b/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml
index f512e71d325..88822ab0376 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml
@@ -136,7 +136,7 @@
 - type: entity
   parent: BaseSpeciesDummy
   id: MobArachnidDummy
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: HumanoidAppearance
     species: Arachnid
diff --git a/Resources/Prototypes/Entities/Mobs/Species/diona.yml b/Resources/Prototypes/Entities/Mobs/Species/diona.yml
index 07d621b139d..530bfe49b24 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/diona.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/diona.yml
@@ -127,7 +127,7 @@
 - type: entity
   parent: BaseSpeciesDummy
   id: MobDionaDummy
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Inventory
     templateId: diona
diff --git a/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml b/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml
index 7f315040356..72c1e782507 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/dwarf.yml
@@ -74,4 +74,4 @@
 - type: entity
   parent: BaseSpeciesDummy
   id: MobDwarfDummy
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
diff --git a/Resources/Prototypes/Entities/Mobs/Species/gingerbread.yml b/Resources/Prototypes/Entities/Mobs/Species/gingerbread.yml
index c514a6f1a05..b0a43551c8d 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/gingerbread.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/gingerbread.yml
@@ -44,7 +44,7 @@
 - type: entity
   parent: BaseSpeciesDummy
   id: MobGingerbreadDummy
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: HumanoidAppearance
     species: Gingerbread
diff --git a/Resources/Prototypes/Entities/Mobs/Species/harpy.yml b/Resources/Prototypes/Entities/Mobs/Species/harpy.yml
index 4ad6ea03cd9..b3fcd565c4b 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/harpy.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/harpy.yml
@@ -192,7 +192,7 @@
   id: ActionHarpyPlayMidi
   name: Play MIDI
   description: Sing your heart out! Right click yourself to set an instrument.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     checkCanInteract: false
@@ -204,7 +204,7 @@
   id: ActionSyrinxChangeVoiceMask
   name: Set name
   description: Change the name others hear to something else.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: DeltaV/Interface/Actions/harpy_syrinx.png
@@ -215,7 +215,7 @@
   id: ActionToggleFlight
   name: Fly
   description: Make use of your wings to fly. Beat the flightless bird allegations.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     checkCanInteract: false
diff --git a/Resources/Prototypes/Entities/Mobs/Species/human.yml b/Resources/Prototypes/Entities/Mobs/Species/human.yml
index ee2886602d1..4310fb1c65c 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/human.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/human.yml
@@ -33,4 +33,4 @@
 - type: entity
   parent: BaseSpeciesDummy
   id: MobHumanDummy
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
diff --git a/Resources/Prototypes/Entities/Mobs/Species/moth.yml b/Resources/Prototypes/Entities/Mobs/Species/moth.yml
index 5f684222264..2a4a157ecb3 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/moth.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/moth.yml
@@ -127,7 +127,7 @@
 - type: entity
   parent: BaseSpeciesDummy
   id: MobMothDummy
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: HumanoidAppearance
     species: Moth
diff --git a/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml b/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml
index ee5d6285659..45be82a448f 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/reptilian.yml
@@ -73,7 +73,7 @@
 - type: entity
   parent: BaseSpeciesDummy
   id: MobReptilianDummy
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   description: A dummy reptilian meant to be used in character setup.
   components:
   - type: HumanoidAppearance
diff --git a/Resources/Prototypes/Entities/Mobs/Species/shadowkin.yml b/Resources/Prototypes/Entities/Mobs/Species/shadowkin.yml
index 393cb0b8716..ff6edb49cc2 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/shadowkin.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/shadowkin.yml
@@ -243,7 +243,7 @@
   save: false
   parent: MobHumanDummy
   id: MobShadowkinDummy
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   description: A dummy shadowkin meant to be used in character setup.
   components:
     - type: HumanoidAppearance
diff --git a/Resources/Prototypes/Entities/Mobs/Species/skeleton.yml b/Resources/Prototypes/Entities/Mobs/Species/skeleton.yml
index cf3aa6b1caa..96c61856936 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/skeleton.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/skeleton.yml
@@ -108,7 +108,7 @@
 - type: entity
   parent: BaseSpeciesDummy
   id: MobSkeletonPersonDummy
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: HumanoidAppearance
       species: Skeleton
diff --git a/Resources/Prototypes/Entities/Mobs/Species/slime.yml b/Resources/Prototypes/Entities/Mobs/Species/slime.yml
index 81547a3fa36..ba04b6e5fac 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/slime.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/slime.yml
@@ -126,7 +126,7 @@
 - type: entity
   parent: MobHumanDummy
   id: MobSlimePersonDummy
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: HumanoidAppearance
       species: SlimePerson
diff --git a/Resources/Prototypes/Entities/Mobs/Species/vox.yml b/Resources/Prototypes/Entities/Mobs/Species/vox.yml
index 58e2b3b6463..62e04b55780 100644
--- a/Resources/Prototypes/Entities/Mobs/Species/vox.yml
+++ b/Resources/Prototypes/Entities/Mobs/Species/vox.yml
@@ -109,7 +109,7 @@
 - type: entity
   parent: BaseSpeciesDummy
   id: MobVoxDummy
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: HumanoidAppearance
     species: Vox
diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/box.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/box.yml
index aa9e70f3fa4..1854d6f71fa 100644
--- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/box.yml
+++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/box.yml
@@ -681,7 +681,7 @@
   id: FoodMealHappyHonkClown
   parent: HappyHonk
   suffix: random food spawner meal
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StorageFill
     contents:
diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/snacks.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/snacks.yml
index e5ae7723671..9e36d34af7c 100644
--- a/Resources/Prototypes/Entities/Objects/Consumable/Food/snacks.yml
+++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/snacks.yml
@@ -443,7 +443,7 @@
 # Trash
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseItem
   id: FoodPacketTrash
   description: This is rubbish.
@@ -466,7 +466,7 @@
     price: 0
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketTrash
   id: FoodPacketBoritosTrash
   name: boritos bag
@@ -475,7 +475,7 @@
     state: boritos-trash
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketTrash
   id: FoodPacketCnDsTrash
   name: C&Ds bag
@@ -484,7 +484,7 @@
     state: cnds-trash
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketTrash
   id: FoodPacketCheesieTrash
   name: cheesie honkers
@@ -493,7 +493,7 @@
     state: cheesiehonkers-trash
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketTrash
   id: FoodPacketChipsTrash
   name: chips
@@ -502,7 +502,7 @@
     state: chips-trash
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketTrash
   id: FoodPacketChocolateTrash
   name: chocolate wrapper
@@ -511,7 +511,7 @@
     state: chocolatebar-trash
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketTrash
   id: FoodPacketEnergyTrash
   name: energybar wrapper
@@ -520,7 +520,7 @@
     state: energybar-trash
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketTrash
   id: FoodPacketPistachioTrash
   name: pistachios packet
@@ -529,7 +529,7 @@
     state: pistachio-trash
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketTrash
   id: FoodPacketPopcornTrash
   name: popcorn box
@@ -538,7 +538,7 @@
     state: popcorn-trash
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketTrash
   id: FoodPacketRaisinsTrash
   name: 4no raisins
@@ -547,7 +547,7 @@
     state: raisins-trash
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketTrash
   id: FoodPacketSemkiTrash
   name: semki packet
@@ -556,7 +556,7 @@
     state: semki-trash
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketTrash
   id: FoodPacketSusTrash
   name: sus jerky
@@ -565,7 +565,7 @@
     state: susjerky-trash
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketTrash
   id: FoodPacketSyndiTrash
   name: syndi-cakes box
@@ -574,7 +574,7 @@
     state: syndicakes-trash
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketTrash
   id: FoodPacketChowMeinTrash
   name: empty chow mein box
@@ -583,7 +583,7 @@
     state: chinese1
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketTrash
   id: FoodPacketDanDanTrash
   name: empty dan dan box
@@ -592,7 +592,7 @@
     state: chinese2
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketTrash
   id: FoodCookieFortune
   name: cookie fortune
@@ -605,7 +605,7 @@
     descriptionSegments: [CookieFortuneDescriptions]
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketTrash
   id: FoodPacketMRETrash
   name: MRE wrapper
@@ -1025,7 +1025,7 @@
 
 # trash
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketTrash
   id: FoodPacketLunacakeTrash
   name: lunacake wrapper
@@ -1036,7 +1036,7 @@
   - type: Item
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketLunacakeTrash
   id: FoodPacketMochicakeTrash
   name: mochicake wrapper
@@ -1046,7 +1046,7 @@
   - type: Item
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketLunacakeTrash
   id: FoodPacketMooncakeTrash
   name: mooncake wrapper
@@ -1056,7 +1056,7 @@
   - type: Item
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketLunacakeTrash
   id: FoodPacketTidegobsTrash
   name: tidegobs trash
@@ -1066,7 +1066,7 @@
   - type: Item
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketLunacakeTrash
   id: FoodPacketSaturnosTrash
   name: saturn-os trash
@@ -1076,7 +1076,7 @@
   - type: Item
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketLunacakeTrash
   id: FoodPacketJoveGelloTrash
   name: jove gello trash
@@ -1086,7 +1086,7 @@
   - type: Item
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketLunacakeTrash
   id: FoodPacketPlutoniumrodsTrash
   name: plutonium rods trash
@@ -1096,7 +1096,7 @@
   - type: Item
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketLunacakeTrash
   id: FoodPacketMarsFroukaTrash
   name: mars frouka trash
@@ -1106,7 +1106,7 @@
   - type: Item
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketLunacakeTrash
   id: FoodPacketVenusTrash
   name: venus hot cakes trash
@@ -1116,7 +1116,7 @@
   - type: Item
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketLunacakeTrash
   id: FoodPacketOortrocksTrash
   name: oort rocks trash
@@ -1127,7 +1127,7 @@
 
 # weebo vend
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketLunacakeTrash
   id: FoodPacketRedalertnutsTrash
   name: red alert nuts packet
@@ -1137,7 +1137,7 @@
   - type: Item
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketLunacakeTrash
   id: FoodPacketStickTrash
   name: stick
@@ -1148,7 +1148,7 @@
 
 #
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketLunacakeTrash
   id: FoodPacketProteinbarTrash
   name: protein bar wrapper
diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Vapes/vape.yml b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Vapes/vape.yml
index 0049c389b5b..19c104b9076 100644
--- a/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Vapes/vape.yml
+++ b/Resources/Prototypes/Entities/Objects/Consumable/Smokeables/Vapes/vape.yml
@@ -1,6 +1,6 @@
 - type: entity
   id: Vape
-  noSpawn: true # DeltaV - Disable the Vape
+  categories: [ HideSpawnMenu ] # DeltaV - Disable the Vape
   parent: BaseVape
   name: vape
   description: "Like a cigar, but for tough teens. (WARNING:Pour only water into the vape)"
diff --git a/Resources/Prototypes/Entities/Objects/Decoration/present.yml b/Resources/Prototypes/Entities/Objects/Decoration/present.yml
index 06836df342c..e07f0e05840 100644
--- a/Resources/Prototypes/Entities/Objects/Decoration/present.yml
+++ b/Resources/Prototypes/Entities/Objects/Decoration/present.yml
@@ -435,7 +435,7 @@
 
 - type: entity
   id: PresentTrash
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseItem
   name: Wrapping Paper
   description: Carefully folded, taped, and tied with a bow. Then ceremoniously ripped apart and tossed on the floor.
diff --git a/Resources/Prototypes/Entities/Objects/Devices/chameleon_projector.yml b/Resources/Prototypes/Entities/Objects/Devices/chameleon_projector.yml
index e0212810210..ce7b6a4bde4 100644
--- a/Resources/Prototypes/Entities/Objects/Devices/chameleon_projector.yml
+++ b/Resources/Prototypes/Entities/Objects/Devices/chameleon_projector.yml
@@ -22,7 +22,7 @@
       entity: ChameleonDisguise
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMob
   id: ChameleonDisguise
   name: Urist McKleiner
@@ -49,7 +49,7 @@
 
 # actions
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   id: ActionDisguiseNoRot
   name: Toggle Rotation
   description: Use this to prevent your disguise from rotating, making it easier to hide in some scenarios.
@@ -59,7 +59,7 @@
     event: !type:DisguiseToggleNoRotEvent
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   id: ActionDisguiseAnchor
   name: Toggle Anchored
   description: For many objects you will want to be anchored to not be completely obvious.
diff --git a/Resources/Prototypes/Entities/Objects/Devices/translator_implants.yml b/Resources/Prototypes/Entities/Objects/Devices/translator_implants.yml
index 0e7fce95686..97e244798b9 100644
--- a/Resources/Prototypes/Entities/Objects/Devices/translator_implants.yml
+++ b/Resources/Prototypes/Entities/Objects/Devices/translator_implants.yml
@@ -3,7 +3,7 @@
   id: BasicTauCetiBasicTranslatorImplant
   name: basic common translator implant
   description: Provides your illiterate friends the ability to understand the common galactic tongue.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TranslatorImplant
     understood:
@@ -14,7 +14,7 @@
   id: TauCetiBasicTranslatorImplant
   name: advanced common translator implant
   description: A more advanced version of the translator implant, teaches your illiterate friends the ability to both speak and understand the galactic tongue!
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TranslatorImplant
     understood:
@@ -27,7 +27,7 @@
   id: BubblishTranslatorImplant
   name: bubblish translator implant
   description: An implant that helps you speak and understand the language of slimes! Special vocal chords not included.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TranslatorImplant
     understood:
@@ -42,7 +42,7 @@
   id: NekomimeticTranslatorImplant
   name: nekomimetic translator implant
   description: A translator implant intially designed to help domestic cat owners understand their pets, now granting the ability to understand and speak to Felinids!
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TranslatorImplant
     understood:
@@ -57,7 +57,7 @@
   id: DraconicTranslatorImplant
   name: draconic translator implant
   description: A translator implant giving the ability to speak to dragons! Subsequently, also allows to communicate with the Unathi.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TranslatorImplant
     understood:
@@ -72,7 +72,7 @@
   id: CanilunztTranslatorImplant
   name: canilunzt translator implant
   description: A translator implant that helps you communicate with your local yeepers. Yeep!
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TranslatorImplant
     understood:
@@ -87,7 +87,7 @@
   id: SolCommonTranslatorImplant
   name: sol-common translator implant
   description: An implant giving the ability to understand and speak SolCommon. Raaagh!
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TranslatorImplant
     understood:
@@ -102,7 +102,7 @@
   id: RootSpeakTranslatorImplant
   name: root-speak translator implant
   description: An implant that lets you speak for the trees. Or to the trees.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TranslatorImplant
     understood:
@@ -117,7 +117,7 @@
   id: MofficTranslatorImplant
   name: Moffic translator implant
   description: An implant designed to help domesticate mothroaches. Subsequently, allows you to communicate with the moth people.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TranslatorImplant
     understood:
@@ -132,7 +132,7 @@
   id: ValyrianStandardTranslatorImplant
   name: valyrian standard translator implant
   description: An implant giving the ability to understand and speak Valyrian Standard. Chirp!
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TranslatorImplant
     understood:
@@ -147,7 +147,7 @@
   id: AzazibaTranslatorImplant
   name: azaziba translator implant
   description: An implant giving the ability to understand and speak Azaziba. # Intended for Admins Only, this item is for lore reasons not obtainable.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TranslatorImplant
     understood:
diff --git a/Resources/Prototypes/Entities/Objects/Devices/translators.yml b/Resources/Prototypes/Entities/Objects/Devices/translators.yml
index 99b6d4305b6..3a30afc4a7f 100644
--- a/Resources/Prototypes/Entities/Objects/Devices/translators.yml
+++ b/Resources/Prototypes/Entities/Objects/Devices/translators.yml
@@ -1,6 +1,6 @@
 # Translator that doesn't need power to work
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   id: TranslatorUnpowered
   parent: BaseItem
   name: translator
@@ -32,7 +32,7 @@
 
 # Base translator that uses a power cell. Starts with an empty slot.
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   id: TranslatorPoweredBase
   parent: [ TranslatorUnpowered, PowerCellSlotMediumItem ]
   components:
@@ -45,14 +45,14 @@
 
 # Normal translator with medium power cell in it
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   id: Translator
   parent: [ PowerCellSlotMediumItem, TranslatorPoweredBase ]
   suffix: Powered
 
 # Normal translator with a high power cell and special appearance
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   id: TranslatorForeigner
   parent: [ PowerCellSlotHighItem, TranslatorPoweredBase ]
   name: foreigner's translator
diff --git a/Resources/Prototypes/Entities/Objects/Fun/Tabletop/backgammon.yml b/Resources/Prototypes/Entities/Objects/Fun/Tabletop/backgammon.yml
index ab404b88a3e..965b25dbc00 100644
--- a/Resources/Prototypes/Entities/Objects/Fun/Tabletop/backgammon.yml
+++ b/Resources/Prototypes/Entities/Objects/Fun/Tabletop/backgammon.yml
@@ -18,7 +18,7 @@
   id: BackgammonBoardTabletop
   name: backgammon
   parent: BaseBoardTabletop
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Fun/Tabletop/backgammon_tabletop.rsi
diff --git a/Resources/Prototypes/Entities/Objects/Fun/Tabletop/base.yml b/Resources/Prototypes/Entities/Objects/Fun/Tabletop/base.yml
index 351396b5b91..50e208cc43f 100644
--- a/Resources/Prototypes/Entities/Objects/Fun/Tabletop/base.yml
+++ b/Resources/Prototypes/Entities/Objects/Fun/Tabletop/base.yml
@@ -28,7 +28,7 @@
   id: BaseBoardTabletop
   name: baseboard
   abstract: true
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Tag
     tags:
diff --git a/Resources/Prototypes/Entities/Objects/Fun/Tabletop/checkers.yml b/Resources/Prototypes/Entities/Objects/Fun/Tabletop/checkers.yml
index 69302548d18..6c206ca26f7 100644
--- a/Resources/Prototypes/Entities/Objects/Fun/Tabletop/checkers.yml
+++ b/Resources/Prototypes/Entities/Objects/Fun/Tabletop/checkers.yml
@@ -24,7 +24,7 @@
   id: *checkerboard
   name: checkerboard
   parent: BaseBoardTabletop
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Fun/Tabletop/chessboard_tabletop.rsi
diff --git a/Resources/Prototypes/Entities/Objects/Fun/Tabletop/chess.yml b/Resources/Prototypes/Entities/Objects/Fun/Tabletop/chess.yml
index b31b7803bae..aeba3918c49 100644
--- a/Resources/Prototypes/Entities/Objects/Fun/Tabletop/chess.yml
+++ b/Resources/Prototypes/Entities/Objects/Fun/Tabletop/chess.yml
@@ -20,7 +20,7 @@
   id: ChessBoardTabletop
   name: chessboard
   parent: BaseBoardTabletop
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Fun/Tabletop/chessboard_tabletop.rsi
diff --git a/Resources/Prototypes/Entities/Objects/Fun/Tabletop/dnd.yml b/Resources/Prototypes/Entities/Objects/Fun/Tabletop/dnd.yml
index 51f17d55b99..9b00ef5e018 100644
--- a/Resources/Prototypes/Entities/Objects/Fun/Tabletop/dnd.yml
+++ b/Resources/Prototypes/Entities/Objects/Fun/Tabletop/dnd.yml
@@ -86,7 +86,7 @@
   parent: BaseBoardTabletop
   id: GrassBoardTabletop
   name: grass battlemap
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Fun/Tabletop/Battlemaps/grassbm_tabletop.rsi
@@ -98,7 +98,7 @@
   parent: BaseBoardTabletop
   id: MoonBoardTabletop
   name: grass battlemap
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Fun/Tabletop/Battlemaps/moonbm_tabletop.rsi
@@ -108,7 +108,7 @@
   parent: BaseBoardTabletop
   id: SandBoardTabletop
   name: sand battlemap
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Fun/Tabletop/Battlemaps/sandbm_tabletop.rsi
@@ -118,7 +118,7 @@
   parent: BaseBoardTabletop
   id: SnowBoardTabletop
   name: snow battlemap
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Fun/Tabletop/Battlemaps/snowbm_tabletop.rsi
@@ -128,7 +128,7 @@
   parent: BaseBoardTabletop
   id: ShipBoardTabletop
   name: ship battlemap
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Fun/Tabletop/Battlemaps/shipbm_tabletop.rsi
diff --git a/Resources/Prototypes/Entities/Objects/Fun/Tabletop/parchis.yml b/Resources/Prototypes/Entities/Objects/Fun/Tabletop/parchis.yml
index bb5fdf1f0bf..b608f4e7876 100644
--- a/Resources/Prototypes/Entities/Objects/Fun/Tabletop/parchis.yml
+++ b/Resources/Prototypes/Entities/Objects/Fun/Tabletop/parchis.yml
@@ -20,7 +20,7 @@
   id: ParchisBoardTabletop
   name: parchís
   parent: BaseBoardTabletop
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Fun/Tabletop/parchis_tabletop.rsi
diff --git a/Resources/Prototypes/Entities/Objects/Fun/pai.yml b/Resources/Prototypes/Entities/Objects/Fun/pai.yml
index b73767fd811..5c0bbc84454 100644
--- a/Resources/Prototypes/Entities/Objects/Fun/pai.yml
+++ b/Resources/Prototypes/Entities/Objects/Fun/pai.yml
@@ -152,7 +152,7 @@
   id: ActionPAIPlayMidi
   name: Play MIDI
   description: Open your portable MIDI interface to soothe your owner.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     checkCanInteract: false
@@ -165,7 +165,7 @@
   id: ActionPAIOpenMap
   name: Open Map
   description: Open your map interface and guide your owner.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: InstantAction
       checkCanInteract: false
diff --git a/Resources/Prototypes/Entities/Objects/Fun/spray_paint.yml b/Resources/Prototypes/Entities/Objects/Fun/spray_paint.yml
index 1b417f6cde0..d7383fdddac 100644
--- a/Resources/Prototypes/Entities/Objects/Fun/spray_paint.yml
+++ b/Resources/Prototypes/Entities/Objects/Fun/spray_paint.yml
@@ -4,7 +4,7 @@
   id: PaintBase
   name: spray paint
   description: A tin of spray paint.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Appearance
   - type: Sprite
diff --git a/Resources/Prototypes/Entities/Objects/Misc/buffering.yml b/Resources/Prototypes/Entities/Objects/Misc/buffering.yml
index dbd35344594..c97ffe89edd 100644
--- a/Resources/Prototypes/Entities/Objects/Misc/buffering.yml
+++ b/Resources/Prototypes/Entities/Objects/Misc/buffering.yml
@@ -1,6 +1,6 @@
 - type: entity
   id: BufferingIcon
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Misc/buffering.rsi
diff --git a/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml b/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml
index 32d8ea7540a..6194be48488 100644
--- a/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml
+++ b/Resources/Prototypes/Entities/Objects/Misc/fire_extinguisher.yml
@@ -69,7 +69,7 @@
   name: extinguisher spray
   id: ExtinguisherSpray
   parent: Vapor
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Effects/extinguisherSpray.rsi
diff --git a/Resources/Prototypes/Entities/Objects/Misc/paper.yml b/Resources/Prototypes/Entities/Objects/Misc/paper.yml
index c72f7a2e91e..9c54e14f50f 100644
--- a/Resources/Prototypes/Entities/Objects/Misc/paper.yml
+++ b/Resources/Prototypes/Entities/Objects/Misc/paper.yml
@@ -268,7 +268,7 @@
 - type: entity
   parent: Paper
   id: PaperWritten
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Paper
   - type: Sprite
diff --git a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml
index a0f5e254d5f..36b1970f3e5 100644
--- a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml
+++ b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml
@@ -17,7 +17,7 @@
   id: SadTromboneImplant
   name: sad trombone implant
   description: This implant plays a sad tune when the user dies.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: SubdermalImplant
       whitelist:
@@ -37,7 +37,7 @@
   id: LightImplant
   name: light implant
   description: This implant emits light from the user's skin on activation.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: SubdermalImplant
       implantAction: ActionToggleLight
@@ -59,7 +59,7 @@
   id: BikeHornImplant
   name: bike horn implant
   description: This implant lets the user honk anywhere at any time.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: SubdermalImplant
       implantAction: ActionActivateHonkImplant
@@ -80,7 +80,7 @@
   id: TrackingImplant
   name: tracking implant
   description: This implant has a tracking device attached to the suit sensor network, as well as a condition monitor for the Security radio channel.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: SubdermalImplant
       whitelist:
@@ -110,7 +110,7 @@
   id: StorageImplant
   name: storage implant
   description: This implant grants hidden storage within a person's body using bluespace technology.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: SubdermalImplant
       implantAction: ActionOpenStorageImplant
@@ -134,7 +134,7 @@
   id: FreedomImplant
   name: freedom implant
   description: This implant lets the user break out of hand restraints up to three times before ceasing to function anymore.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: SubdermalImplant
       implantAction: ActionActivateFreedomImplant
@@ -147,7 +147,7 @@
   id: UplinkImplant
   name: uplink implant
   description: This implant lets the user access a hidden Syndicate uplink at will.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: SubdermalImplant
     implantAction: ActionOpenUplinkImplant
@@ -168,7 +168,7 @@
   id: EmpImplant
   name: EMP implant
   description: This implant creates an electromagnetic pulse when activated.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: SubdermalImplant
       implantAction: ActionActivateEmpImplant
@@ -183,7 +183,7 @@
   id: ScramImplant
   name: scram implant
   description: This implant randomly teleports the user within a large radius when activated.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: SubdermalImplant
       implantAction: ActionActivateScramImplant
@@ -195,7 +195,7 @@
   id: DnaScramblerImplant
   name: DNA scrambler implant
   description: This implant lets the user randomly change their appearance and name once.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: SubdermalImplant
       implantAction: ActionActivateDnaScramblerImplant
@@ -210,7 +210,7 @@
   id: MicroBombImplant
   name: micro-bomb implant
   description: This implant detonates the user upon activation or upon death.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: SubdermalImplant
       permanent: true
@@ -240,7 +240,7 @@
   id: MacroBombImplant
   name: macro-bomb implant
   description: This implant creates a large explosion on death after a preprogrammed countdown.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: SubdermalImplant
       permanent: true
@@ -275,7 +275,7 @@
   id: DeathAcidifierImplant
   name: death-acidifier implant
   description: This implant melts the user and their equipment upon death.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: SubdermalImplant
     permanent: true
@@ -299,7 +299,7 @@
   id: DeathRattleImplant
   name: death rattle implant
   description: This implant will inform the Syndicate radio channel should the user fall into critical condition or die.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: SubdermalImplant
       permanent: true
@@ -319,7 +319,7 @@
   id: MindShieldImplant
   name: mind-shield implant
   description: This implant will ensure loyalty to Nanotrasen and prevent mind control devices.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
    - type: SubdermalImplant
      permanent: true
diff --git a/Resources/Prototypes/Entities/Objects/Power/lights.yml b/Resources/Prototypes/Entities/Objects/Power/lights.yml
index b18a0feaa52..289736f6717 100644
--- a/Resources/Prototypes/Entities/Objects/Power/lights.yml
+++ b/Resources/Prototypes/Entities/Objects/Power/lights.yml
@@ -234,7 +234,7 @@
   name: exterior light tube
   description: A high power high energy bulb for the depths of space. May contain mercury.
   id: ExteriorLightTube
-  noSpawn: true # DeltaV - Don't map these
+  categories: [ HideSpawnMenu ] # DeltaV - Don't map these
   components:
   - type: LightBulb
     color: "#B4FCF0"
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml b/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml
index 23ce5a36ee2..61866b149db 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Chapel/bibles.yml
@@ -93,7 +93,7 @@
   id: ActionBibleSummon
   name: Summon familiar
   description: Summon a familiar that will aid you and gain humanlike intelligence once inhabited by a soul.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: { sprite: Clothing/Head/Hats/witch.rsi, state: icon }
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Forensics/forensics.yml b/Resources/Prototypes/Entities/Objects/Specific/Forensics/forensics.yml
index 9b3be6d7780..aba4cd40efe 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Forensics/forensics.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Forensics/forensics.yml
@@ -21,7 +21,7 @@
 
 - type: entity
   id: ScentTrackEffect
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TimedDespawn
     lifetime: 1
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml
index a5e08e9f542..0e84e54a642 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml
@@ -150,7 +150,7 @@
 - type: entity
   name: soaplet
   id: SoapletSyndie
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: Soap
   description: A tiny piece of syndicate soap.
   components:
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/spray.yml b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/spray.yml
index cddf7f6075a..998d3ecf03e 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/spray.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/spray.yml
@@ -104,7 +104,7 @@
 - type: entity
   id: Vapor
   name: "vapor"
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: SolutionContainerManager
     solutions:
@@ -136,7 +136,7 @@
 - type: entity
   id: BigVapor
   parent: Vapor
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Effects/chempuff.rsi
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Mail/Items/misc.yml b/Resources/Prototypes/Entities/Objects/Specific/Mail/Items/misc.yml
index 6f1c86e00ba..6f9c3157e96 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Mail/Items/misc.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Mail/Items/misc.yml
@@ -3,7 +3,7 @@
 - type: entity
   id: DelayedSmoke
   parent: BaseItem
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: delayed smoke
   suffix: "(10s)"
   components:
@@ -16,7 +16,7 @@
 
 - type: entity
   id: AdminInstantEffectEMP7
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   suffix: EMP, 7 meters
   parent: AdminInstantEffectBase
   components:
@@ -27,7 +27,7 @@
 - type: entity
   id: DelayedEMP
   parent: BaseItem
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: delayed EMP (7 meters)
   components:
   - type: Sprite #DeltaV: Apparently these want sprites, probably because they're baseitems
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Mail/base_mail_large.yml b/Resources/Prototypes/Entities/Objects/Specific/Mail/base_mail_large.yml
index a5dcefacba5..f31b2aa78fd 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Mail/base_mail_large.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Mail/base_mail_large.yml
@@ -73,7 +73,7 @@
     isLarge: true
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMailLarge
   id: MailLargeAdminFun
   suffix: adminfun
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml b/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml
index 960e37a6ccc..53a7a8f075a 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml
@@ -32,7 +32,7 @@
   id: ActionBorgSwapModule
   name: Swap Module
   description: Select this module, enabling you to use the tools it provides.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     itemIconStyle: BigItem
diff --git a/Resources/Prototypes/Entities/Objects/Specific/chemical-containers.yml b/Resources/Prototypes/Entities/Objects/Specific/chemical-containers.yml
index 4bd71f898db..d6855d630c6 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/chemical-containers.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/chemical-containers.yml
@@ -88,7 +88,7 @@
   parent: Jug
   name: jug (carbon)
   id: JugCarbon
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-carbon
@@ -103,7 +103,7 @@
   parent: Jug
   name: jug (iodine)
   id: JugIodine
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-iodine
@@ -118,7 +118,7 @@
   parent: Jug
   name: jug (fluorine)
   id: JugFluorine
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-fluorine
@@ -133,7 +133,7 @@
   parent: Jug
   name: jug (chlorine)
   id: JugChlorine
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-chlorine
@@ -148,7 +148,7 @@
   parent: Jug
   name: jug (aluminium)
   id: JugAluminium
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-aluminium
@@ -163,7 +163,7 @@
   parent: Jug
   name: jug (phosphorus)
   id: JugPhosphorus
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-phosphorus
@@ -178,7 +178,7 @@
   parent: Jug
   name: jug (sulfur)
   id: JugSulfur
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-sulfur
@@ -193,7 +193,7 @@
   parent: Jug
   name: jug (silicon)
   id: JugSilicon
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-silicon
@@ -208,7 +208,7 @@
   parent: Jug
   name: jug (hydrogen)
   id: JugHydrogen
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-hydrogen
@@ -223,7 +223,7 @@
   parent: Jug
   name: jug (lithium)
   id: JugLithium
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-lithium
@@ -238,7 +238,7 @@
   parent: Jug
   name: jug (sodium)
   id: JugSodium
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-sodium
@@ -253,7 +253,7 @@
   parent: Jug
   name: jug (potassium)
   id: JugPotassium
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-potassium
@@ -268,7 +268,7 @@
   parent: Jug
   name: jug (radium)
   id: JugRadium
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-radium
@@ -283,7 +283,7 @@
   parent: Jug
   name: jug (iron)
   id: JugIron
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-iron
@@ -298,7 +298,7 @@
   parent: Jug
   name: jug (copper)
   id: JugCopper
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-copper
@@ -313,7 +313,7 @@
   parent: Jug
   name: jug (gold)
   id: JugGold
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-gold
@@ -328,7 +328,7 @@
   parent: Jug
   name: jug (mercury)
   id: JugMercury
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-mercury
@@ -343,7 +343,7 @@
   parent: Jug
   name: jug (silver)
   id: JugSilver
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-silver
@@ -358,7 +358,7 @@
   parent: Jug
   name: jug (ethanol)
   id: JugEthanol
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-ethanol
@@ -373,7 +373,7 @@
   parent: Jug
   name: jug (sugar)
   id: JugSugar
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-sugar
@@ -388,7 +388,7 @@
   parent: Jug
   name: jug (nitrogen)
   id: JugNitrogen
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-nitrogen
@@ -403,7 +403,7 @@
   parent: Jug
   name: jug (oxygen)
   id: JugOxygen
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-oxygen
@@ -418,7 +418,7 @@
   parent: Jug
   name: jug (Plant-B-Gone)
   id: JugPlantBGone
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-plant-b-gone
@@ -433,7 +433,7 @@
   parent: Jug
   name: jug (welding fuel)
   id: JugWeldingFuel
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Label
       currentLabel: reagent-name-welding-fuel
diff --git a/Resources/Prototypes/Entities/Objects/Tools/fulton.yml b/Resources/Prototypes/Entities/Objects/Tools/fulton.yml
index 5255e5f303b..49d8c18c7c1 100644
--- a/Resources/Prototypes/Entities/Objects/Tools/fulton.yml
+++ b/Resources/Prototypes/Entities/Objects/Tools/fulton.yml
@@ -88,7 +88,7 @@
 
 - type: entity
   id: FultonEffect
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: fulton effect
   components:
   - type: TimedDespawn
diff --git a/Resources/Prototypes/Entities/Objects/Tools/glowstick.yml b/Resources/Prototypes/Entities/Objects/Tools/glowstick.yml
index 2320764d9ff..3081f60989e 100644
--- a/Resources/Prototypes/Entities/Objects/Tools/glowstick.yml
+++ b/Resources/Prototypes/Entities/Objects/Tools/glowstick.yml
@@ -115,7 +115,7 @@
   name: light pulse test
   parent: BaseItem
   id: LightBehaviourTest1
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       sprite: Objects/Misc/glowstick.rsi
@@ -146,7 +146,7 @@
   name: color cycle test
   parent: BaseItem
   id: LightBehaviourTest2
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       sprite: Objects/Misc/glowstick.rsi
@@ -178,7 +178,7 @@
   name: multi-behaviour light test
   parent: BaseItem
   id: LightBehaviourTest3
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       sprite: Objects/Misc/glowstick.rsi
@@ -218,7 +218,7 @@
   name: light fade in test
   parent: BaseItem
   id: LightBehaviourTest4
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       sprite: Objects/Misc/glowstick.rsi
@@ -249,7 +249,7 @@
   name: light pulse radius test
   parent: BaseItem
   id: LightBehaviourTest5
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       sprite: Objects/Misc/glowstick.rsi
@@ -280,7 +280,7 @@
   name: light randomize radius test
   parent: BaseItem
   id: LightBehaviourTest6
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       sprite: Objects/Misc/glowstick.rsi
diff --git a/Resources/Prototypes/Entities/Objects/Tools/jetpacks.yml b/Resources/Prototypes/Entities/Objects/Tools/jetpacks.yml
index bd05b8d0f39..8ece2e1a862 100644
--- a/Resources/Prototypes/Entities/Objects/Tools/jetpacks.yml
+++ b/Resources/Prototypes/Entities/Objects/Tools/jetpacks.yml
@@ -1,6 +1,6 @@
 - type: entity
   id: JetpackEffect
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TimedDespawn
     lifetime: 2
@@ -69,7 +69,7 @@
   id: ActionToggleJetpack
   name: Toggle jetpack
   description: Toggles the jetpack, giving you movement outside the station.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon:
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Bombs/funny.yml b/Resources/Prototypes/Entities/Objects/Weapons/Bombs/funny.yml
index 630354f23d9..1aac4424149 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Bombs/funny.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Bombs/funny.yml
@@ -52,7 +52,7 @@
 
 - type: entity
   id: HotPotatoEffect
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TimedDespawn
     lifetime: 0.6
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/antimateriel.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/antimateriel.yml
index 31d7b65fe8b..65b7dbc1655 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/antimateriel.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/antimateriel.yml
@@ -1,5 +1,5 @@
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseBullet
   id: BulletAntiMateriel
   name: bullet (.60 anti-materiel)
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/caseless_rifle.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/caseless_rifle.yml
index 5ce0bf82fe9..f6a3ad590e4 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/caseless_rifle.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/caseless_rifle.yml
@@ -2,7 +2,7 @@
   id: BulletCaselessRifle
   name: bullet (.25 caseless)
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -13,7 +13,7 @@
   id: BulletCaselessRiflePractice
   name: bullet (.25 caseless practice)
   parent: BaseBulletPractice
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -24,7 +24,7 @@
   id: BulletCaselessRifleRubber
   name: bullet (.25 caseless rubber)
   parent: BaseBulletRubber
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/grenade.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/grenade.yml
index 36d41e391ac..351e68a6200 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/grenade.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/grenade.yml
@@ -1,7 +1,7 @@
 - type: entity
   id: PelletClusterRubber
   name: pellet (ball, Rubber)
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseBullet
   components:
   - type: Sprite
@@ -20,7 +20,7 @@
 - type: entity
   id: PelletClusterLethal
   name: pellet (ball, Lethal)
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseBullet
   components:
   - type: Sprite
@@ -37,7 +37,7 @@
 - type: entity
   id: PelletClusterIncendiary
   name: pellet (ball, incendiary)
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseBulletIncendiary
   components:
   - type: Sprite
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/heavy_rifle.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/heavy_rifle.yml
index be6a07e486d..d37555c3443 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/heavy_rifle.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/heavy_rifle.yml
@@ -2,7 +2,7 @@
   id: BulletHeavyRifle
   name: bullet (.20 rifle)
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -13,7 +13,7 @@
   id: BulletMinigun
   name: minigun bullet (.10 rifle)
   parent: BulletHeavyRifle
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/light_rifle.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/light_rifle.yml
index 3a0df2ac6c7..7e0a19c448d 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/light_rifle.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/light_rifle.yml
@@ -2,7 +2,7 @@
   id: BulletLightRifle
   name: bullet (.20 rifle)
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -13,7 +13,7 @@
   id: BulletLightRiflePractice
   name: bullet (.20 rifle practice)
   parent: BaseBulletPractice
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -24,7 +24,7 @@
   id: BulletLightRifleRubber
   name: bullet (.20 rifle rubber)
   parent: BaseBulletRubber
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -35,7 +35,7 @@
   id: BulletLightRifleIncendiary
   parent: BaseBulletIncendiary
   name: bullet (.20 rifle incendiary)
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -47,7 +47,7 @@
   id: BulletLightRifleUranium
   parent: BaseBulletUranium
   name: bullet (.20 rifle uranium)
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/magnum.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/magnum.yml
index 1b5cf7890ba..ab45d6837f4 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/magnum.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/magnum.yml
@@ -2,7 +2,7 @@
   id: BulletMagnum
   name: bullet (.45 magnum)
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -13,7 +13,7 @@
   id: BulletMagnumPractice
   name: bullet (.45 magnum practice)
   parent: BaseBulletPractice
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -24,7 +24,7 @@
   id: BulletMagnumRubber
   name: bullet (.45 magnum rubber)
   parent: BaseBulletRubber
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -37,7 +37,7 @@
   id: BulletMagnumIncendiary
   parent: BaseBulletIncendiary
   name: bullet (.45 magnum incendiary)
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -49,7 +49,7 @@
   id: BulletMagnumAP
   name: bullet (.45 magnum armor-piercing)
   parent: BaseBulletAP
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -61,7 +61,7 @@
   id: BulletMagnumUranium
   name: bullet (.45 magnum uranium)
   parent: BaseBulletUranium
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/pistol.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/pistol.yml
index 086a8dc914f..ef3807700da 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/pistol.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/pistol.yml
@@ -2,7 +2,7 @@
   id: BulletPistol
   name: bullet (.35 auto)
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -13,7 +13,7 @@
   id: BulletPistolPractice
   name: bullet (.35 auto practice)
   parent: BaseBulletPractice
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -24,7 +24,7 @@
   id: BulletPistolRubber
   name: bullet (.35 auto rubber)
   parent: BaseBulletRubber
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -35,7 +35,7 @@
   id: BulletPistolIncendiary
   parent: BaseBulletIncendiary
   name: bullet (.35 auto incendiary)
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -47,7 +47,7 @@
   id: BulletPistolUranium
   parent: BaseBulletUranium
   name: bullet (.35 auto uranium)
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/rifle.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/rifle.yml
index 2113916cf52..3e1d49ddc02 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/rifle.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/rifle.yml
@@ -2,7 +2,7 @@
   id: BulletRifle
   name: bullet (0.20 rifle)
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -13,7 +13,7 @@
   id: BulletRiflePractice
   name: bullet (0.20 rifle practice)
   parent: BaseBulletPractice
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -24,7 +24,7 @@
   id: BulletRifleRubber
   name: bullet (0.20 rifle rubber)
   parent: BaseBulletRubber
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -35,7 +35,7 @@
   id: BulletRifleIncendiary
   parent: BaseBulletIncendiary
   name: bullet (0.20 rifle incendiary)
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -47,7 +47,7 @@
   id: BulletRifleUranium
   parent: BaseBulletUranium
   name: bullet (0.20 rifle uranium)
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/shotgun.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/shotgun.yml
index e119a846c9c..6e4570e1a16 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/shotgun.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Ammunition/Projectiles/shotgun.yml
@@ -1,7 +1,7 @@
 - type: entity
   id: PelletShotgunSlug
   name: pellet (.50 slug)
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseBullet
   components:
   - type: Sprite
@@ -15,7 +15,7 @@
 - type: entity
   id: PelletShotgunBeanbag
   name: beanbag (.50)
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseBullet
   components:
   - type: Sprite
@@ -31,7 +31,7 @@
 - type: entity
   id: PelletShotgun
   name: pellet (.50)
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseBullet
   components:
   - type: Sprite
@@ -45,7 +45,7 @@
 - type: entity
   id: PelletShotgunIncendiary
   name: pellet (.50 incendiary)
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseBulletIncendiary
   components:
   - type: Sprite
@@ -62,7 +62,7 @@
 - type: entity
   id: PelletShotgunPractice
   name: pellet (.50 practice)
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseBulletPractice
   components:
   - type: Sprite
@@ -76,7 +76,7 @@
 - type: entity
   id: PelletShotgunImprovised
   name: improvised pellet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseBullet
   components:
   - type: Sprite
@@ -92,7 +92,7 @@
 - type: entity
   id: PelletShotgunTranquilizer
   name: pellet (.50 tranquilizer)
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseBulletPractice
   components:
   - type: Sprite
@@ -119,7 +119,7 @@
 - type: entity
   id: PelletShotgunFlare
   name: pellet (.50 flare)
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Physics
     bodyType: Dynamic
@@ -166,7 +166,7 @@
 - type: entity
   id: PelletShotgunUranium
   name: pellet (.50 uranium)
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseBullet
   components:
   - type: Sprite
@@ -181,7 +181,7 @@
 - type: entity
   id: PelletGrapeshot #tally fucking ho
   name: grapeshot pellet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseBullet
   components:
   - type: Sprite
@@ -200,7 +200,7 @@
   id: PelletGlass
   name: glass shard
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     noRot: false
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/hitscan.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/hitscan.yml
index 94cac9bcec3..a602f120813 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/hitscan.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/hitscan.yml
@@ -1,7 +1,7 @@
 # Used to animate the hitscan effects because effectsystem doesn't support it
 - type: entity
   id: HitscanEffect
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TimedDespawn
     lifetime: 2.0
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/impacts.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/impacts.yml
index d70b05bf61d..9be9e43e943 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/impacts.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/impacts.yml
@@ -1,6 +1,6 @@
 - type: entity
   id: BulletImpactEffect
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TimedDespawn
     lifetime: 0.25
@@ -18,7 +18,7 @@
 
 - type: entity
   id: BulletImpactEffectDisabler
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: TimedDespawn
       lifetime: 0.2
@@ -36,7 +36,7 @@
 
 - type: entity
   id: BulletImpactEffectOrangeDisabler
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TimedDespawn
     lifetime: 0.2
@@ -55,7 +55,7 @@
 
 - type: entity
   id: BulletImpactEffectKinetic
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TimedDespawn
     lifetime: 0.2
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/magic.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/magic.yml
index b3abbfdfd3f..a230673e907 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/magic.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/magic.yml
@@ -3,7 +3,7 @@
   name: fireball
   description: You better GITTAH WEIGH.
   parent: BulletRocket
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: PointLight
     color: "#E25822"
@@ -31,7 +31,7 @@
     fireStacks: 0.35
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseBulletTrigger
   id: ProjectileDragonsBreath
   name: dragon's breath
@@ -68,7 +68,7 @@
   name: fireball
   description: Hovering blob of flame.
   parent: ProjectileFireball
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TimedDespawn
     lifetime: 30
@@ -82,7 +82,7 @@
 - type: entity
   id: ProjectilePolyboltBase
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Weapons/Guns/Projectiles/magic.rsi
@@ -99,7 +99,7 @@
   parent: ProjectilePolyboltBase
   name: carp polybolt
   description: Nooo, I don't wanna be fish!
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: PolymorphOnCollide
     polymorph: WizardForcedCarp
@@ -112,7 +112,7 @@
   parent: ProjectilePolyboltBase
   name: monkey polybolt
   description: Nooo, I don't wanna be monkey!
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: PolymorphOnCollide
     polymorph: WizardForcedMonkey
@@ -125,7 +125,7 @@
   parent: ProjectilePolyboltBase
   name: door polybolt
   description: Nooo, I don't wanna be door!
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Weapons/Guns/Projectiles/magic.rsi
@@ -146,7 +146,7 @@
   name: healing bolt
   description: I COMMAND YOU TO LIVE!
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Weapons/Guns/Projectiles/magic.rsi
@@ -166,7 +166,7 @@
   id: BulletInstakillMagic
   name: magical lead cylinder
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   description: This looks familiar.
   components:
   - type: Projectile
@@ -180,7 +180,7 @@
   parent: ProjectilePolyboltBase
   name: cluwne polybolt
   description: knoH KnoH!
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: PolymorphOnCollide
     polymorph: WizardForcedCluwne
@@ -193,7 +193,7 @@
   parent: BaseBullet
   name: Icicle
   description: Brrrrr.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Structures/Specific/Anomalies/ice_anom.rsi
@@ -209,7 +209,7 @@
   id: ProjectilePolyboltBread
   name: bread polybolt
   description: Nooo, I don't wanna be bread!
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: PolymorphOnCollide
     polymorph: BreadMorph
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/meteors.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/meteors.yml
index 6bdac1e85f0..60154a4fea5 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/meteors.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/meteors.yml
@@ -1,7 +1,7 @@
 - type: entity
   id: MeteorLarge
   name: meteor
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     noRot: false
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml
index 55e09014ec9..acf22532232 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml
@@ -1,6 +1,6 @@
 - type: entity
   id: MuzzleFlashEffect
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TimedDespawn
     lifetime: 0.4
@@ -69,7 +69,7 @@
 - type: entity
   id: BaseBulletTrigger # Trigger-on-collide bullets
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TriggerOnCollide
     fixtureID: projectile
@@ -93,7 +93,7 @@
   id: BaseBulletPractice
   name: base bullet practice
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Weapons/Guns/Projectiles/projectiles2.rsi
@@ -108,7 +108,7 @@
   id: BaseBulletRubber
   name: base bullet rubber
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Weapons/Guns/Projectiles/projectiles2.rsi
@@ -127,7 +127,7 @@
   id: BaseBulletIncendiary
   name: base bullet incendiary
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Projectile
     damage:
@@ -145,7 +145,7 @@
   id: BaseBulletAP
   name: base bullet armor-piercing
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Weapons/Guns/Projectiles/projectiles2.rsi
@@ -161,7 +161,7 @@
   id: BaseBulletUranium
   name: base bullet uranium
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Weapons/Guns/Projectiles/projectiles2.rsi
@@ -177,7 +177,7 @@
   name: taser bolt
   id: BulletTaser
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: FlyBySound
     sound:
@@ -219,7 +219,7 @@
   name : disabler bolt
   id: BulletDisabler
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Reflective
     reflective:
@@ -262,7 +262,7 @@
   name : disabler bolt practice
   id: BulletDisablerPractice
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: FlyBySound
     sound:
@@ -302,7 +302,7 @@
   name: emitter bolt
   id: EmitterBolt
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Structures/Power/Generation/Singularity/emitter.rsi
@@ -339,7 +339,7 @@
   name: watcher bolt
   id: WatcherBolt
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: FlyBySound
     sound:
@@ -379,7 +379,7 @@
   name: magmawing watcher bolt
   id: WatcherBoltMagmawing
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Weapons/Guns/Projectiles/projectiles_tg.rsi
@@ -398,7 +398,7 @@
   id: BulletKinetic
   name: kinetic bolt
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   description: Not too bad, but you still don't want to get hit by it.
   components:
   - type: Reflective
@@ -423,7 +423,7 @@
 - type: entity
   id: BulletKineticShuttle
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     noRot: false
@@ -451,7 +451,7 @@
   id: BulletCharge
   name: charge bolt
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   description: Marks a target for additional damage.
   components:
   - type: Reflective
@@ -485,7 +485,7 @@
   parent: BaseBullet
   id: AnomalousParticleDelta
   name: delta particles
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: PointLight
     enabled: true
@@ -526,7 +526,7 @@
 - type: entity
   parent: AnomalousParticleDelta
   id: AnomalousParticleDeltaStrong
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: AnomalousParticle
     particleType: Delta
@@ -539,7 +539,7 @@
   parent: AnomalousParticleDelta
   id: AnomalousParticleEpsilon
   name: epsilon particles
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: PointLight
     enabled: true
@@ -556,7 +556,7 @@
 - type: entity
   parent: AnomalousParticleEpsilon
   id: AnomalousParticleEpsilonStrong
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: AnomalousParticle
     particleType: Epsilon
@@ -569,7 +569,7 @@
   parent: AnomalousParticleDelta
   id: AnomalousParticleZeta
   name: zeta particles
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: PointLight
     enabled: true
@@ -586,7 +586,7 @@
 - type: entity
   parent: AnomalousParticleZeta
   id: AnomalousParticleZetaStrong
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: AnomalousParticle
     particleType: Zeta
@@ -599,7 +599,7 @@
   parent: AnomalousParticleDelta
   id: AnomalousParticleOmegaStrong
   name: omega particles
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: PointLight
     enabled: true
@@ -628,7 +628,7 @@
   parent: AnomalousParticleDelta
   id: AnomalousParticleSigma
   name: sigma particles
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: PointLight
     enabled: true
@@ -646,7 +646,7 @@
   parent: AnomalousParticleSigma
   id: AnomalousParticleSigmaStrong
   name: sigma particles
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: AnomalousParticle
     particleType: Sigma
@@ -656,7 +656,7 @@
   id: BulletRocket
   name: rocket
   parent: BaseBulletTrigger
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Weapons/Guns/Projectiles/projectiles2.rsi
@@ -678,7 +678,7 @@
   id: BulletWeakRocket
   name: weak rocket
   parent: BaseBulletTrigger
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Weapons/Guns/Projectiles/projectiles2.rsi
@@ -700,7 +700,7 @@
   id: BulletGrenadeBaton
   name: baton grenade
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Weapons/Guns/Projectiles/projectiles2.rsi
@@ -720,7 +720,7 @@
   id: BulletGrenadeBlast
   name: blast grenade
   parent: BaseBulletTrigger
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Weapons/Guns/Projectiles/projectiles2.rsi
@@ -737,7 +737,7 @@
   id: BulletGrenadeFlash
   name: flash grenade
   parent: BaseBulletTrigger
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Weapons/Guns/Projectiles/projectiles2.rsi
@@ -756,7 +756,7 @@
   id: BulletGrenadeFrag
   name: frag grenade
   parent: BaseBulletTrigger
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Weapons/Guns/Projectiles/projectiles2.rsi
@@ -773,7 +773,7 @@
   id: BulletGrenadeEMP
   name: EMP rocket
   parent: BaseBulletTrigger
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Weapons/Guns/Projectiles/projectiles2.rsi
@@ -794,7 +794,7 @@
   id: BulletCap
   name: cap bullet
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Fun/toys.rsi
@@ -810,7 +810,7 @@
   id: BulletAcid
   name: acid spit
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Projectile
       damage:
@@ -826,7 +826,7 @@
 - type: entity
   id: BulletWaterShot
   name: water
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Clickable
   - type: Physics
@@ -869,7 +869,7 @@
   id: BulletCannonBall
   name: cannonball
   parent: BaseBulletTrigger
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     sprite: Objects/Weapons/Guns/Projectiles/projectiles2.rsi
@@ -890,7 +890,7 @@
 - type: entity
   id: GrapplingHook
   name: grappling hook
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: EmbeddableProjectile
       deleteOnRemove: true
@@ -926,7 +926,7 @@
   name : disabler bolt smg
   id: BulletDisablerSmg
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Reflective
     reflective:
@@ -969,7 +969,7 @@
   name: tesla gun lightning
   id: TeslaGunBullet
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: TimedDespawn
     lifetime: 5
@@ -1006,7 +1006,7 @@
   name: energy bolt
   id: BulletEnergyGunLaser
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Reflective
     reflective:
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/grenades.yml b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/grenades.yml
index acbaac29222..b39303d9ede 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/grenades.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/grenades.yml
@@ -83,7 +83,7 @@
 
 - type: entity
   id: GrenadeFlashEffect
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: PointLight
       enabled: true
@@ -125,7 +125,7 @@
   description: Go out on your own terms!
   parent: GrenadeBase
   id: SelfDestructSeq
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: ExplodeOnTrigger
   - type: Explosive
diff --git a/Resources/Prototypes/Entities/Stations/nanotrasen.yml b/Resources/Prototypes/Entities/Stations/nanotrasen.yml
index 329542a267a..9f4adce96ae 100644
--- a/Resources/Prototypes/Entities/Stations/nanotrasen.yml
+++ b/Resources/Prototypes/Entities/Stations/nanotrasen.yml
@@ -27,7 +27,7 @@
     - BaseStationNanotrasen
     - BaseRandomStation
     - BaseStationMail # Nyano component, required for station mail to function
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Transform
 
@@ -37,7 +37,7 @@
     - BaseStation
     - BaseStationAlertLevels
     - BaseStationNanotrasen
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Transform
 
@@ -48,7 +48,7 @@
   - BaseStationJobsSpawning
   - BaseStationRecords
   - BaseStationNanotrasen
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Transform
 
@@ -73,6 +73,6 @@
     - BaseStationNanotrasen
     - BaseRandomStation
     - BaseStationMail
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Transform
diff --git a/Resources/Prototypes/Entities/Stations/syndicate.yml b/Resources/Prototypes/Entities/Stations/syndicate.yml
index a6494169146..c863ef73523 100644
--- a/Resources/Prototypes/Entities/Stations/syndicate.yml
+++ b/Resources/Prototypes/Entities/Stations/syndicate.yml
@@ -11,6 +11,6 @@
   parent:
   - BaseStation
   - BaseStationSyndicate
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Transform
diff --git a/Resources/Prototypes/Entities/Stations/test.yml b/Resources/Prototypes/Entities/Stations/test.yml
index 0f2e40537a2..9eec6979e7a 100644
--- a/Resources/Prototypes/Entities/Stations/test.yml
+++ b/Resources/Prototypes/Entities/Stations/test.yml
@@ -6,6 +6,6 @@
     - BaseStationJobsSpawning
     - BaseStationRecords
     - BaseStationAlertLevels
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Transform
diff --git a/Resources/Prototypes/Entities/Structures/Furniture/chairs.yml b/Resources/Prototypes/Entities/Structures/Furniture/chairs.yml
index 14b3270ba88..d65d652ff42 100644
--- a/Resources/Prototypes/Entities/Structures/Furniture/chairs.yml
+++ b/Resources/Prototypes/Entities/Structures/Furniture/chairs.yml
@@ -358,6 +358,8 @@
   components:
   - type: Foldable
     folded: true
+  - type: Strap
+    enabled: False
 
 - type: entity
   name: steel bench
diff --git a/Resources/Prototypes/Entities/Structures/Furniture/rollerbeds.yml b/Resources/Prototypes/Entities/Structures/Furniture/rollerbeds.yml
index 161ea25bc43..1cfe98f0f65 100644
--- a/Resources/Prototypes/Entities/Structures/Furniture/rollerbeds.yml
+++ b/Resources/Prototypes/Entities/Structures/Furniture/rollerbeds.yml
@@ -55,6 +55,7 @@
       rotation: -90
       buckleOffset: "0,0.15"
       unbuckleOffset: "0,0.15"
+      buckleOnInteractHand: False
     - type: Appearance
     - type: GenericVisualizer
       visuals:
@@ -79,6 +80,8 @@
   components:
   - type: Foldable
     folded: true
+  - type: Strap
+    enabled: False
 
 - type: entity
   id: CheapRollerBed
@@ -105,6 +108,8 @@
   components:
   - type: Foldable
     folded: true
+  - type: Strap
+    enabled: False
 
 - type: entity
   id: EmergencyRollerBed
@@ -131,3 +136,5 @@
   components:
   - type: Foldable
     folded: true
+  - type: Strap
+    enabled: False
diff --git a/Resources/Prototypes/Entities/Structures/Piping/Disposal/pipes.yml b/Resources/Prototypes/Entities/Structures/Piping/Disposal/pipes.yml
index 7aee5896472..3c1334169d8 100644
--- a/Resources/Prototypes/Entities/Structures/Piping/Disposal/pipes.yml
+++ b/Resources/Prototypes/Entities/Structures/Piping/Disposal/pipes.yml
@@ -61,7 +61,7 @@
 
 - type: entity
   id: DisposalHolder
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: disposal holder
   components:
   - type: DisposalHolder
diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/particles.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/particles.yml
index 8d889ee5cbb..9d3ce9c931f 100644
--- a/Resources/Prototypes/Entities/Structures/Power/Generation/PA/particles.yml
+++ b/Resources/Prototypes/Entities/Structures/Power/Generation/PA/particles.yml
@@ -3,7 +3,7 @@
   description: Accelerated particles.
   id: ParticlesProjectile
   parent: BaseBullet
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       layers:
@@ -54,7 +54,7 @@
   description: Accelerated negative particles.
   id: AntiParticlesProjectile
   parent: ParticlesProjectile
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     layers:
diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/ame.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/ame.yml
index 4e4ef8bdbcf..b7d6b5a128d 100644
--- a/Resources/Prototypes/Entities/Structures/Power/Generation/ame.yml
+++ b/Resources/Prototypes/Entities/Structures/Power/Generation/ame.yml
@@ -108,7 +108,7 @@
     mediumVoltageNode: ame
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: AmeController
   id: AmeControllerUnanchored
   suffix: Unanchored
diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/generators.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/generators.yml
index ed70b310915..1faee965d4e 100644
--- a/Resources/Prototypes/Entities/Structures/Power/Generation/generators.yml
+++ b/Resources/Prototypes/Entities/Structures/Power/Generation/generators.yml
@@ -144,7 +144,7 @@
 # Construction Frames
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   id: BaseGeneratorWallmountFrame
   name: wallmount generator frame
   description: A construction frame for a wallmount generator.
diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/solar.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/solar.yml
index 601c7c360a5..382846938b2 100644
--- a/Resources/Prototypes/Entities/Structures/Power/Generation/solar.yml
+++ b/Resources/Prototypes/Entities/Structures/Power/Generation/solar.yml
@@ -1,6 +1,6 @@
 - type: entity
   id: SolarPanelBasePhysSprite
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: solar panel
   placement:
     mode: SnapgridCenter
diff --git a/Resources/Prototypes/Entities/Structures/Power/Generation/teg.yml b/Resources/Prototypes/Entities/Structures/Power/Generation/teg.yml
index 9a378c26a44..0245839e466 100644
--- a/Resources/Prototypes/Entities/Structures/Power/Generation/teg.yml
+++ b/Resources/Prototypes/Entities/Structures/Power/Generation/teg.yml
@@ -183,7 +183,7 @@
 - # Spawned by the client-side circulator examine code to indicate the inlet/outlet direction.
   type: entity
   id: TegCirculatorArrow
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       sprite: Markers/teg_arrow.rsi
diff --git a/Resources/Prototypes/Entities/Structures/Power/apc.yml b/Resources/Prototypes/Entities/Structures/Power/apc.yml
index 01147f439f3..9412d454476 100644
--- a/Resources/Prototypes/Entities/Structures/Power/apc.yml
+++ b/Resources/Prototypes/Entities/Structures/Power/apc.yml
@@ -1,5 +1,5 @@
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   id: BaseAPC
   name: APC
   description: A control terminal for the area's electrical systems.
@@ -143,7 +143,7 @@
 
 # APC under construction
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   id: APCFrame
   name: APC frame
   description: A control terminal for the area's electrical systems, lacking the electronics.
diff --git a/Resources/Prototypes/Entities/Structures/Power/substation.yml b/Resources/Prototypes/Entities/Structures/Power/substation.yml
index 489cfff6597..f96ef97187f 100644
--- a/Resources/Prototypes/Entities/Structures/Power/substation.yml
+++ b/Resources/Prototypes/Entities/Structures/Power/substation.yml
@@ -117,7 +117,7 @@
 # Compact Wall Substation Base
 - type: entity
   id: BaseSubstationWall
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: wallmount substation
   description: A substation designed for compact shuttles and spaces.
   placement:
@@ -260,7 +260,7 @@
 # Construction Frame
 - type: entity
   id: BaseSubstationWallFrame
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: wallmount substation frame
   description: A substation frame for construction
   placement:
diff --git a/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml b/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml
index 1d184ad45eb..a773bef2334 100644
--- a/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml
+++ b/Resources/Prototypes/Entities/Structures/Storage/Canisters/gas_canisters.yml
@@ -698,7 +698,7 @@
 - type: entity
   parent: GasCanisterBrokenBase
   id: StorageCanisterBroken
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       state: yellow-1
@@ -706,7 +706,7 @@
 - type: entity
   parent: GasCanisterBrokenBase
   id: AirCanisterBroken
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     state: grey-1
@@ -714,7 +714,7 @@
 - type: entity
   parent: GasCanisterBrokenBase
   id: OxygenCanisterBroken
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     state: blue-1
@@ -722,7 +722,7 @@
 - type: entity
   parent: GasCanisterBrokenBase
   id: NitrogenCanisterBroken
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       state: red-1
@@ -730,7 +730,7 @@
 - type: entity
   parent: GasCanisterBrokenBase
   id: CarbonDioxideCanisterBroken
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       state: black-1
@@ -738,7 +738,7 @@
 - type: entity
   parent: GasCanisterBrokenBase
   id: PlasmaCanisterBroken
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       state: orange-1
@@ -746,7 +746,7 @@
 - type: entity
   parent: GasCanisterBrokenBase
   id: TritiumCanisterBroken
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       state: green-1
@@ -755,7 +755,7 @@
   parent: GasCanisterBrokenBase
   id: WaterVaporCanisterBroken
   name: broken water vapor canister
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       state: water_vapor-1
@@ -763,7 +763,7 @@
 - type: entity
   parent: GasCanisterBrokenBase
   id: AmmoniaCanisterBroken
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       state: greenys-1
@@ -771,7 +771,7 @@
 - type: entity
   parent: GasCanisterBrokenBase
   id: NitrousOxideCanisterBroken
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Sprite
       state: redws-1
@@ -779,7 +779,7 @@
 - type: entity
   parent: GasCanisterBrokenBase
   id: FrezonCanisterBroken
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     state: frezon-1
diff --git a/Resources/Prototypes/Entities/Structures/Storage/Crates/base_structurecrates.yml b/Resources/Prototypes/Entities/Structures/Storage/Crates/base_structurecrates.yml
index 01c226cb0fd..52deca8e096 100644
--- a/Resources/Prototypes/Entities/Structures/Storage/Crates/base_structurecrates.yml
+++ b/Resources/Prototypes/Entities/Structures/Storage/Crates/base_structurecrates.yml
@@ -1,7 +1,7 @@
 - type: entity
   parent: BaseStructureDynamic
   id: CrateGeneric
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: crate
   description: A large container for items.
   components:
@@ -94,7 +94,7 @@
 - type: entity
   parent: CrateGeneric
   id: CrateBaseWeldable
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Weldable
   - type: ResistLocker
diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/bar_sign.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/bar_sign.yml
index 8e957abfe7f..2f644f373d4 100644
--- a/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/bar_sign.yml
+++ b/Resources/Prototypes/Entities/Structures/Wallmounts/Signs/bar_sign.yml
@@ -36,7 +36,7 @@
 #- type: entity
 #  id: LargeBarSign
 #  name: large bar sign
-#  noSpawn: true
+#  categories: [ HideSpawnMenu ]
 #  components:
 #  - type: Clickable
 #  - type: InteractionOutline
diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/switch.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/switch.yml
index 8fe6064b951..dbcb0763442 100644
--- a/Resources/Prototypes/Entities/Structures/Wallmounts/switch.yml
+++ b/Resources/Prototypes/Entities/Structures/Wallmounts/switch.yml
@@ -224,7 +224,7 @@
   id: LockableButton
   name: lockable button
   parent: SignalButtonDirectional
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Appearance
   - type: Lock
@@ -484,7 +484,7 @@
 - type: entity
   id: ButtonFrame
   name: button frame
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   description: It's a frame to help distinguish switches visually.
   placement:
     mode: SnapgridCenter
diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/timer.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/timer.yml
index dd7eb5bea82..8e6b0863d67 100644
--- a/Resources/Prototypes/Entities/Structures/Wallmounts/timer.yml
+++ b/Resources/Prototypes/Entities/Structures/Wallmounts/timer.yml
@@ -83,7 +83,7 @@
 # Construction Frame
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   id: TimerFrame
   name: timer frame
   description: A construction frame for a timer.
diff --git a/Resources/Prototypes/Entities/Virtual/beam.yml b/Resources/Prototypes/Entities/Virtual/beam.yml
index 7ded09c3fff..fa249f0dd3d 100644
--- a/Resources/Prototypes/Entities/Virtual/beam.yml
+++ b/Resources/Prototypes/Entities/Virtual/beam.yml
@@ -2,7 +2,7 @@
 - type: entity
   id: VirtualBeamEntityController
   name: BEAM ENTITY YOU SHOULD NOT SEE THIS
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: Beam
     - type: TimedDespawn
diff --git a/Resources/Prototypes/Entities/Virtual/electrocution.yml b/Resources/Prototypes/Entities/Virtual/electrocution.yml
index ac65245191e..c45e0b3ca9e 100644
--- a/Resources/Prototypes/Entities/Virtual/electrocution.yml
+++ b/Resources/Prototypes/Entities/Virtual/electrocution.yml
@@ -12,7 +12,7 @@
 - type: entity
   id: VirtualElectrocutionLoadHVPower
   parent: VirtualElectrocutionLoadBase
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: NodeContainer
     nodes:
@@ -26,7 +26,7 @@
 - type: entity
   id: VirtualElectrocutionLoadMVPower
   parent: VirtualElectrocutionLoadBase
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: NodeContainer
     nodes:
@@ -40,7 +40,7 @@
 - type: entity
   id: VirtualElectrocutionLoadApc
   parent: VirtualElectrocutionLoadBase
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: NodeContainer
     nodes:
diff --git a/Resources/Prototypes/Entities/Virtual/stripping_hidden.yml b/Resources/Prototypes/Entities/Virtual/stripping_hidden.yml
index 538e502a402..6e46340f973 100644
--- a/Resources/Prototypes/Entities/Virtual/stripping_hidden.yml
+++ b/Resources/Prototypes/Entities/Virtual/stripping_hidden.yml
@@ -5,7 +5,7 @@
   id: StrippingHiddenEntity
   name: Hidden Entity
   description: There is something in this pocket. #Or maybe they ar... nah... too obvious a joke.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     texture: Interface/VerbIcons/information.svg.192dpi.png
diff --git a/Resources/Prototypes/Entities/Virtual/tether.yml b/Resources/Prototypes/Entities/Virtual/tether.yml
index ce953854d65..9459fd20887 100644
--- a/Resources/Prototypes/Entities/Virtual/tether.yml
+++ b/Resources/Prototypes/Entities/Virtual/tether.yml
@@ -1,6 +1,6 @@
 - type: entity
   id: TetherEntity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Physics
     bodyType: Dynamic
diff --git a/Resources/Prototypes/Entities/Virtual/virtual_item.yml b/Resources/Prototypes/Entities/Virtual/virtual_item.yml
index ed742435501..20f311db704 100644
--- a/Resources/Prototypes/Entities/Virtual/virtual_item.yml
+++ b/Resources/Prototypes/Entities/Virtual/virtual_item.yml
@@ -2,7 +2,7 @@
 - type: entity
   id: VirtualItem
   name: VIRTUAL ITEM YOU SHOULD NOT SEE THIS
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Item
   - type: VirtualItem
diff --git a/Resources/Prototypes/Entities/World/Debris/asteroids.yml b/Resources/Prototypes/Entities/World/Debris/asteroids.yml
index 061288d010b..1541190cd21 100644
--- a/Resources/Prototypes/Entities/World/Debris/asteroids.yml
+++ b/Resources/Prototypes/Entities/World/Debris/asteroids.yml
@@ -55,7 +55,7 @@
   id: AsteroidDebrisSmall
   parent: BaseAsteroidDebris
   name: Asteroid Debris Small
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: MapGrid
     - type: BlobFloorPlanBuilder
@@ -65,7 +65,7 @@
   id: AsteroidDebrisMedium
   parent: BaseAsteroidDebris
   name: Asteroid Debris Medium
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: MapGrid
     - type: BlobFloorPlanBuilder
@@ -75,7 +75,7 @@
   id: AsteroidDebrisLarge
   parent: BaseAsteroidDebris
   name: Asteroid Debris Large
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: MapGrid
     - type: BlobFloorPlanBuilder
@@ -85,7 +85,7 @@
   id: AsteroidDebrisLarger
   parent: BaseAsteroidDebris
   name: Asteroid Debris Larger
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: MapGrid
     - type: BlobFloorPlanBuilder
@@ -96,7 +96,7 @@
   id: AsteroidSalvageSmall
   parent: BaseAsteroidDebris
   name: Salvage Asteroid Small
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: MapGrid
     - type: BlobFloorPlanBuilder
@@ -108,7 +108,7 @@
   id: AsteroidSalvageMedium
   parent: BaseAsteroidDebris
   name: Salvage Asteroid Medium
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: MapGrid
     - type: BlobFloorPlanBuilder
@@ -120,7 +120,7 @@
   id: AsteroidSalvageLarge
   parent: BaseAsteroidDebris
   name: Salvage Asteroid Large
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: MapGrid
     - type: BlobFloorPlanBuilder
@@ -132,7 +132,7 @@
   id: AsteroidSalvageHuge
   parent: BaseAsteroidDebris
   name: Salvage Asteroid Huge
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: MapGrid
     - type: BlobFloorPlanBuilder
diff --git a/Resources/Prototypes/Entities/World/Debris/wrecks.yml b/Resources/Prototypes/Entities/World/Debris/wrecks.yml
index 4c5a3be48f7..743d92e1bac 100644
--- a/Resources/Prototypes/Entities/World/Debris/wrecks.yml
+++ b/Resources/Prototypes/Entities/World/Debris/wrecks.yml
@@ -53,7 +53,7 @@
   id: ScrapDebrisSmall
   parent: BaseScrapDebris
   name: Scrap Debris Small
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: MapGrid
     - type: BlobFloorPlanBuilder
@@ -63,7 +63,7 @@
   id: ScrapDebrisMedium
   parent: BaseScrapDebris
   name: Scrap Debris Medium
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: MapGrid
     - type: BlobFloorPlanBuilder
@@ -73,7 +73,7 @@
   id: ScrapDebrisLarge
   parent: BaseScrapDebris
   name: Scrap Debris Large
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: MapGrid
     - type: BlobFloorPlanBuilder
diff --git a/Resources/Prototypes/Entities/World/chunk.yml b/Resources/Prototypes/Entities/World/chunk.yml
index b60fd0d91be..c7052662674 100644
--- a/Resources/Prototypes/Entities/World/chunk.yml
+++ b/Resources/Prototypes/Entities/World/chunk.yml
@@ -5,7 +5,7 @@
   description: |
     It's rude to stare.
     It's also a bit odd you're looking at the abstract representation of the grid of reality.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: WorldChunk
     - type: Sprite
diff --git a/Resources/Prototypes/GameRules/cargo_gifts.yml b/Resources/Prototypes/GameRules/cargo_gifts.yml
index 3787f4e6034..e74ce0cb65e 100644
--- a/Resources/Prototypes/GameRules/cargo_gifts.yml
+++ b/Resources/Prototypes/GameRules/cargo_gifts.yml
@@ -1,7 +1,7 @@
 - type: entity
   id: GiftsPizzaPartySmall
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     weight: 5
@@ -21,7 +21,7 @@
 - type: entity
   id: GiftsPizzaPartyLarge
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: StationEvent
       weight: 2
@@ -40,7 +40,7 @@
 - type: entity
   id: GiftsEngineering
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: StationEvent
       weight: 5
@@ -63,7 +63,7 @@
 - type: entity
   id: GiftsVendingRestock
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: StationEvent
       weight: 4
@@ -86,7 +86,7 @@
 - type: entity
   id: GiftsJanitor
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: StationEvent
       weight: 6
@@ -106,7 +106,7 @@
 - type: entity
   id: GiftsMedical
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: StationEvent
       weight: 8
@@ -128,7 +128,7 @@
 - type: entity
   id: GiftsSpacingSupplies
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: StationEvent
       weight: 4
@@ -150,7 +150,7 @@
 - type: entity
   id: GiftsFireProtection
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: StationEvent
       weight: 4
@@ -170,7 +170,7 @@
 - type: entity
   id: GiftsSecurityGuns
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: StationEvent
       weight: 3
@@ -191,7 +191,7 @@
 - type: entity
   id: GiftsSecurityRiot
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: StationEvent
       weight: 4
diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml
index 4923e399dd0..801dcc4b859 100644
--- a/Resources/Prototypes/GameRules/events.yml
+++ b/Resources/Prototypes/GameRules/events.yml
@@ -1,7 +1,7 @@
 - type: entity
   id: AnomalySpawn
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     weight: 8
@@ -12,7 +12,7 @@
 - type: entity
   id: BluespaceArtifact
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     weight: 8
@@ -23,7 +23,7 @@
 - type: entity
   id: BluespaceLocker
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     weight: 2
@@ -35,7 +35,7 @@
 - type: entity
   id: BreakerFlip
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     weight: 7
@@ -46,7 +46,7 @@
 - type: entity
   id: BureaucraticError
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: true
@@ -58,7 +58,7 @@
 - type: entity
   id: ClericalError
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: true
@@ -70,7 +70,7 @@
 - type: entity
   parent: BaseGameRule
   id: ClosetSkeleton
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     weight: 5
@@ -82,7 +82,7 @@
 - type: entity
   parent: BaseGameRule
   id: DragonSpawn
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     weight: 6.5
@@ -96,7 +96,7 @@
 - type: entity
   parent: BaseGameRule
   id: NinjaSpawn
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     weight: 6
@@ -109,7 +109,7 @@
 - type: entity
   parent: BaseGameRule
   id: RevenantSpawn
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     weight: 7.5
@@ -123,7 +123,7 @@
 #- type: entity
 #  id: FalseAlarm
 #  parent: BaseGameRule
-#  noSpawn: true
+#  categories: [ HideSpawnMenu ]
 #  components:
 #  - type: StationEvent
 #    weight: 15
@@ -133,7 +133,7 @@
 - type: entity
   id: GasLeak
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: true
@@ -145,7 +145,7 @@
 - type: entity
   id: KudzuGrowth
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     earliestStart: 15
@@ -158,7 +158,7 @@
 - type: entity
   id: MeteorSwarm
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     earliestStart: 30
@@ -173,7 +173,7 @@
 - type: entity
   id: MouseMigration
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: true
@@ -198,7 +198,7 @@
 - type: entity
   id: CockroachMigration
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: true
@@ -215,7 +215,7 @@
 - type: entity
   id: PowerGridCheck
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     weight: 5
@@ -229,7 +229,7 @@
 - type: entity
   id: RandomSentience
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     weight: 6
@@ -240,7 +240,7 @@
 - type: entity
   parent: BaseGameRule
   id: SolarFlare
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     weight: 8
@@ -267,7 +267,7 @@
 - type: entity
   id: VentClog
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: true
@@ -281,7 +281,7 @@
 - type: entity
   id: SlimesSpawn
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: true
@@ -302,7 +302,7 @@
 - type: entity
   id: SpiderSpawn
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: true
@@ -319,7 +319,7 @@
 - type: entity
   id: SpiderClownSpawn
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: false
@@ -336,7 +336,7 @@
 - type: entity
   id: ZombieOutbreak
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     earliestStart: 50
@@ -370,7 +370,7 @@
 - type: entity
   id: LoneOpsSpawn
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     earliestStart: 35
@@ -404,7 +404,7 @@
 - type: entity
   id: SleeperAgentsRule
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     earliestStart: 25
@@ -427,7 +427,7 @@
 - type: entity
   id: MassHallucinations
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     weight: 7
@@ -444,7 +444,7 @@
 - type: entity
   id: ImmovableRodSpawn
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: false
@@ -455,7 +455,7 @@
   - type: ImmovableRodRule
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseGameRule
   id: IonStorm
   components:
@@ -468,7 +468,7 @@
 - type: entity
   id: MimicVendorRule
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: StationEvent
       earliestStart: 0
diff --git a/Resources/Prototypes/GameRules/midround.yml b/Resources/Prototypes/GameRules/midround.yml
index db1a76adc08..fca0073b4e5 100644
--- a/Resources/Prototypes/GameRules/midround.yml
+++ b/Resources/Prototypes/GameRules/midround.yml
@@ -3,7 +3,7 @@
 - type: entity
   id: Ninja
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: GenericAntagRule
     agentName: ninja-round-end-agent-name
@@ -19,7 +19,7 @@
 
 # stores configuration for dragon
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseGameRule
   id: Dragon
   components:
@@ -30,7 +30,7 @@
     - DragonSurviveObjective
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseGameRule
   id: Thief
   components:
@@ -55,7 +55,7 @@
         sound: "/Audio/Misc/thief_greeting.ogg"
 
 #- type: entity
-#  noSpawn: true
+#  categories: [ HideSpawnMenu ]
 #  parent: BaseGameRule
 #  id: Exterminator
 #  components:
diff --git a/Resources/Prototypes/GameRules/roundstart.yml b/Resources/Prototypes/GameRules/roundstart.yml
index 39bea004d02..4ae53c9b37a 100644
--- a/Resources/Prototypes/GameRules/roundstart.yml
+++ b/Resources/Prototypes/GameRules/roundstart.yml
@@ -1,12 +1,12 @@
 - type: entity
   id: BaseGameRule
   abstract: true
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: GameRule
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseGameRule
   id: SubGamemodesRule
   components:
@@ -18,7 +18,7 @@
 - type: entity
   id: DeathMatch31
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: DeathMatchRule
     rewardSpawns:
@@ -48,7 +48,7 @@
 - type: entity
   id: InactivityTimeRestart
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InactivityRule
     inactivityMaxTime: 600
@@ -57,7 +57,7 @@
 - type: entity
   id: MaxTimeRestart
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: MaxTimeRestartRule
     roundMaxTime: 300
@@ -66,7 +66,7 @@
 - type: entity
   id: Nukeops
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: GameRule
     minPlayers: 35
@@ -136,7 +136,7 @@
 - type: entity
   id: Traitor
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: GameRule
     minPlayers: 5
@@ -157,7 +157,7 @@
 - type: entity
   id: Revolutionary
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: GameRule
     minPlayers: 15
@@ -182,21 +182,21 @@
 - type: entity
   id: Sandbox
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: SandboxRule
 
 - type: entity
   id: Secret
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: SecretRule
 
 - type: entity
   id: Zombie
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: GameRule
     minPlayers: 20
@@ -229,21 +229,21 @@
 - type: entity
   id: BasicStationEventScheduler
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: BasicStationEventScheduler
 
 - type: entity
   id: RampingStationEventScheduler
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: RampingStationEventScheduler
 
 - type: entity
   id: HellshiftStationEventScheduler
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: RampingStationEventScheduler
     chaosModifier: 4 # By default, one event each 30-10 seconds after two hours. Changing CVars will cause this to deviate.
@@ -253,7 +253,7 @@
 - type: entity
   id: IrregularStationEventScheduler
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: OscillatingStationEventScheduler
     minChaos: 0.8
@@ -266,7 +266,7 @@
 - type: entity
   id: IrregularExtendedStationEventScheduler
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: OscillatingStationEventScheduler
     minChaos: 0.8
@@ -281,7 +281,7 @@
 - type: entity
   id: BasicRoundstartVariation
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: RoundstartStationVariationRule
     rules:
diff --git a/Resources/Prototypes/GameRules/unknown_shuttles.yml b/Resources/Prototypes/GameRules/unknown_shuttles.yml
index f44bbdcaaab..db9d4756a89 100644
--- a/Resources/Prototypes/GameRules/unknown_shuttles.yml
+++ b/Resources/Prototypes/GameRules/unknown_shuttles.yml
@@ -1,7 +1,7 @@
 - type: entity
   id: UnknownShuttleCargoLost
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: false
@@ -14,7 +14,7 @@
 - type: entity
   id: UnknownShuttleTravelingCuisine
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: false
@@ -27,7 +27,7 @@
 - type: entity
   id: UnknownShuttleDisasterEvacPod
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: false
@@ -40,7 +40,7 @@
 - type: entity
   id: UnknownShuttleHonki
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: false
@@ -53,7 +53,7 @@
 - type: entity
   id: UnknownShuttleSyndieEvacPod
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     startAnnouncement: false
diff --git a/Resources/Prototypes/GameRules/variation.yml b/Resources/Prototypes/GameRules/variation.yml
index 2884d5f9d6f..4e0d917176a 100644
--- a/Resources/Prototypes/GameRules/variation.yml
+++ b/Resources/Prototypes/GameRules/variation.yml
@@ -4,7 +4,7 @@
   id: BaseVariationPass
   parent: BaseGameRule
   abstract: true
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationVariationPassRule
 
@@ -13,14 +13,14 @@
 - type: entity
   id: BasicPoweredLightVariationPass
   parent: BaseVariationPass
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: PoweredLightVariationPass
 
 - type: entity
   id: SolidWallRustingVariationPass
   parent: BaseVariationPass
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: WallReplaceVariationPass
   - type: EntityReplaceVariationPass
@@ -32,7 +32,7 @@
 - type: entity
   id: ReinforcedWallRustingVariationPass
   parent: BaseVariationPass
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: ReinforcedWallReplaceVariationPass
   - type: EntityReplaceVariationPass
@@ -44,7 +44,7 @@
 - type: entity
   id: BasicTrashVariationPass
   parent: BaseVariationPass
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: EntitySpawnVariationPass
     tilesPerEntityAverage: 35
@@ -105,7 +105,7 @@
 - type: entity
   id: BasicPuddleMessVariationPass
   parent: BaseVariationPass
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: PuddleMessVariationPass
     randomPuddleSolutionFill: RandomFillTrashPuddle
@@ -113,7 +113,7 @@
 - type: entity
   id: BloodbathPuddleMessVariationPass
   parent: BaseVariationPass
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: PuddleMessVariationPass
     tilesPerSpillAverage: 150
@@ -123,7 +123,7 @@
 - type: entity
   id: CutWireVariationPass
   parent: BaseVariationPass
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: CutWireVariationPass
     wireCutChance: 0.01
diff --git a/Resources/Prototypes/Guidebook/rules.yml b/Resources/Prototypes/Guidebook/rules.yml
new file mode 100644
index 00000000000..a59af2c5edc
--- /dev/null
+++ b/Resources/Prototypes/Guidebook/rules.yml
@@ -0,0 +1,362 @@
+- type: guideEntry # Default for forks and stuff. Should not be listed anywhere if the server is using a custom ruleset.
+  id: DefaultRuleset
+  name: guide-entry-rules
+  text: "/ServerInfo/Guidebook/ServerRules/DefaultRules.xml"
+
+- type: guideEntry
+  id: CoreRuleset
+  name: guide-entry-rules-core-only
+  priority: 0
+  text: "/ServerInfo/Guidebook/ServerRules/WizDenCoreOnlyRules.xml"
+
+- type: guideEntry
+  id: StandardRuleset
+  name: guide-entry-rules-lrp
+  priority: 5
+  text: "/ServerInfo/Guidebook/ServerRules/WizDenLRPRules.xml"
+
+- type: guideEntry
+  id: MRPRuleset
+  name: guide-entry-rules-mrp
+  priority: 10
+  text: "/ServerInfo/Guidebook/ServerRules/WizDenMRPRules.xml"
+
+- type: guideEntry
+  id: RoleTypes
+  name: guide-entry-rules-role-types
+  priority: 20
+  text: "/ServerInfo/Guidebook/ServerRules/RoleTypes.xml"
+
+- type: guideEntry
+  id: CoreRules
+  name: guide-entry-rules-core
+  priority: 30
+  text: "/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC0.xml"
+  children:
+  - RuleC1
+  - RuleC2
+  - RuleC3
+  - RuleC4
+  - RuleC5
+  - RuleC6
+  - RuleC7
+  - RuleC8
+  - RuleC9
+  - RuleC10
+  - RuleC11
+  - RuleC12
+  - RuleC13
+  - RuleC14
+
+- type: guideEntry
+  id: RuleC1
+  name: guide-entry-rules-c1
+  priority: 1
+  text: "/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC1Admins.xml"
+
+- type: guideEntry
+  id: RuleC2
+  name: guide-entry-rules-c2
+  priority: 2
+  text: "/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC2DBAD.xml"
+
+- type: guideEntry
+  id: RuleC3
+  name: guide-entry-rules-c3
+  priority: 3
+  text: "/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC3NoHate.xml"
+
+- type: guideEntry
+  id: RuleC4
+  name: guide-entry-rules-c4
+  priority: 4
+  text: "/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC4NoERP.xml"
+
+- type: guideEntry
+  id: RuleC5
+  name: guide-entry-rules-c5
+  priority: 5
+  text: "/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC5Metacomms.xml"
+
+- type: guideEntry
+  id: RuleC6
+  name: guide-entry-rules-c6
+  priority: 6
+  text: "/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC6BanEvasion.xml"
+
+- type: guideEntry
+  id: RuleC7
+  name: guide-entry-rules-c7
+  priority: 7
+  text: "/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC7EnglishOnly.xml"
+
+- type: guideEntry
+  id: RuleC8
+  name: guide-entry-rules-c8
+  priority: 8
+  text: "/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC8Exploits.xml"
+
+- type: guideEntry
+  id: RuleC9
+  name: guide-entry-rules-c9
+  priority: 9
+  text: "/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC9Multikey.xml"
+
+- type: guideEntry
+  id: RuleC10
+  name: guide-entry-rules-c10
+  priority: 10
+  text: "/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC10AHelp.xml"
+
+- type: guideEntry
+  id: RuleC11
+  name: guide-entry-rules-c11
+  priority: 11
+  text: "/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC11AhelpThreats.xml"
+
+- type: guideEntry
+  id: RuleC12
+  name: guide-entry-rules-c12
+  priority: 12
+  text: "/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC12MinAge.xml"
+
+- type: guideEntry
+  id: RuleC13
+  name: guide-entry-rules-c13
+  priority: 13
+  text: "/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC13CharacterNames.xml"
+
+- type: guideEntry
+  id: RuleC14
+  name: guide-entry-rules-c14
+  priority: 14
+  text: "/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC14ICinOOC.xml"
+
+- type: guideEntry
+  id: RoleplayRules
+  name: guide-entry-rules-roleplay
+  priority: 40
+  text: "/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR0.xml"
+  children:
+  - RuleR1
+  - RuleR2
+  - RuleR3
+  - RuleR4
+  - RuleR5
+  - RuleR6
+  - RuleR7
+  - RuleR8
+  - RuleR9
+  - RuleR10
+  - RuleR11
+  - RuleR12
+  - RuleR13
+  - RuleR14
+  - RuleR15
+
+- type: guideEntry
+  id: RuleR1
+  name: guide-entry-rules-r1
+  priority: 1
+  text: "/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR1Silicons.xml"
+
+- type: guideEntry
+  id: RuleR2
+  name: guide-entry-rules-r2
+  priority: 2
+  text: "/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR2Familiars.xml"
+
+- type: guideEntry
+  id: RuleR3
+  name: guide-entry-rules-r3
+  priority: 3
+  text: "/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR3NormalRP.xml"
+
+- type: guideEntry
+  id: RuleR4
+  name: guide-entry-rules-r4
+  priority: 4
+  text: "/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR4Metashield.xml"
+
+- type: guideEntry
+  id: RuleR5
+  name: guide-entry-rules-r5
+  priority: 5
+  text: "/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR5Arrivals.xml"
+
+- type: guideEntry
+  id: RuleR6
+  name: guide-entry-rules-r6
+  priority: 6
+  text: "/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR6SelfAntag.xml"
+
+- type: guideEntry
+  id: RuleR7
+  name: guide-entry-rules-r7
+  priority: 7
+  text: "/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR7RoundStalling.xml"
+
+- type: guideEntry
+  id: RuleR8
+  name: guide-entry-rules-r8
+  priority: 8
+  text: "/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR8NoFriendlyAntag.xml"
+
+- type: guideEntry
+  id: RuleR9
+  name: guide-entry-rules-r9
+  priority: 9
+  text: "/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR9MassSabotage.xml"
+
+- type: guideEntry
+  id: RuleR10
+  name: guide-entry-rules-r10
+  priority: 10
+  text: "/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR10Subordination.xml"
+
+- type: guideEntry
+  id: RuleR11
+  name: guide-entry-rules-r11
+  priority: 11
+  text: "/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR11Escalation.xml"
+
+- type: guideEntry
+  id: RuleR12
+  name: guide-entry-rules-r12
+  priority: 12
+  text: "/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR12RoleAbandonment.xml"
+
+- type: guideEntry
+  id: RuleR13
+  name: guide-entry-rules-r13
+  priority: 13
+  text: "/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR13PerformRole.xml"
+
+- type: guideEntry
+  id: RuleR14
+  name: guide-entry-rules-r14
+  priority: 14
+  text: "/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR14SecComStandard.xml"
+
+- type: guideEntry
+  id: RuleR15
+  name: guide-entry-rules-r15
+  priority: 15
+  text: "/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR15SpaceLaw.xml"
+
+- type: guideEntry
+  id: SiliconRules
+  name: guide-entry-rules-silicon
+  priority: 50
+  text: "/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS0.xml"
+  children:
+  - RuleS1
+  - RuleS2
+  - RuleS3
+  - RuleS4
+  - RuleS5
+  - RuleS6
+  - RuleS7
+  - RuleS8
+  - RuleS9
+  - RuleS10
+
+- type: guideEntry
+  id: RuleS1
+  name: guide-entry-rules-s1
+  priority: 1
+  text: "/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS1Laws.xml"
+
+- type: guideEntry
+  id: RuleS2
+  name: guide-entry-rules-s2
+  priority: 2
+  text: "/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS2LawPriority.xml"
+
+- type: guideEntry
+  id: RuleS3
+  name: guide-entry-rules-s3
+  priority: 3
+  text: "/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS3LawRedefinition.xml"
+
+- type: guideEntry
+  id: RuleS4
+  name: guide-entry-rules-s4
+  priority: 4
+  text: "/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS4RequestChanges.xml"
+
+- type: guideEntry
+  id: RuleS5
+  name: guide-entry-rules-s5
+  priority: 5
+  text: "/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS5FreeSilicon.xml"
+
+- type: guideEntry
+  id: RuleS6
+  name: guide-entry-rules-s6
+  priority: 6
+  text: "/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS6UnreasonableOrders.xml"
+
+- type: guideEntry
+  id: RuleS7
+  name: guide-entry-rules-s7
+  priority: 7
+  text: "/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS7Consistency.xml"
+
+- type: guideEntry
+  id: RuleS8
+  name: guide-entry-rules-s8
+  priority: 8
+  text: "/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS8DefaultCrewDefinition.xml"
+
+- type: guideEntry
+  id: RuleS9
+  name: guide-entry-rules-s9
+  priority: 9
+  text: "/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS9DefaultHarmDefinition.xml"
+
+- type: guideEntry
+  id: RuleS10
+  name: guide-entry-rules-s10
+  priority: 10
+  text: "/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS10OrderConflicts.xml"
+
+- type: guideEntry
+  id: SpaceLaw
+  name: guide-entry-rules-space-law
+  priority: 60
+  text: "/ServerInfo/Guidebook/ServerRules/SpaceLaw/SpaceLaw.xml"
+  children:
+  - SpaceLawControlledSubstances
+  - SpaceLawRestrictedGear
+  - SpaceLawRestrictedWeapons
+
+- type: guideEntry
+  id: SpaceLawControlledSubstances
+  name: guide-entry-rules-sl-controlled-substances
+  priority: 20
+  text: "/ServerInfo/Guidebook/ServerRules/SpaceLaw/SLControlledSubstances.xml"
+
+- type: guideEntry
+  id: SpaceLawRestrictedGear
+  name: guide-entry-rules-sl-restricted-gear
+  priority: 30
+  text: "/ServerInfo/Guidebook/ServerRules/SpaceLaw/SLRestrictedGear.xml"
+
+- type: guideEntry
+  id: SpaceLawRestrictedWeapons
+  name: guide-entry-rules-sl-restricted-weapons
+  priority: 40
+  text: "/ServerInfo/Guidebook/ServerRules/SpaceLaw/SLRestrictedWeapons.xml"
+
+- type: guideEntry
+  id: BanTypes
+  name: guide-entry-rules-ban-types
+  priority: 90
+  text: "/ServerInfo/Guidebook/ServerRules/BanTypes.xml"
+
+- type: guideEntry
+  id: BanDurations
+  name: guide-entry-rules-ban-durations
+  priority: 100
+  text: "/ServerInfo/Guidebook/ServerRules/BanDurations.xml"
diff --git a/Resources/Prototypes/Guidebook/ss14.yml b/Resources/Prototypes/Guidebook/ss14.yml
index c1017fefcae..5b1f1dd8f97 100644
--- a/Resources/Prototypes/Guidebook/ss14.yml
+++ b/Resources/Prototypes/Guidebook/ss14.yml
@@ -3,6 +3,7 @@
   name: guide-entry-ss14
   text: "/ServerInfo/Guidebook/SpaceStation14.xml"
   children:
+  - SpaceLaw
   - Controls
   - Jobs
   - Survival
diff --git a/Resources/Prototypes/Magic/event_spells.yml b/Resources/Prototypes/Magic/event_spells.yml
index e59e1b2db88..742c187d717 100644
--- a/Resources/Prototypes/Magic/event_spells.yml
+++ b/Resources/Prototypes/Magic/event_spells.yml
@@ -2,7 +2,7 @@
   id: ActionSummonGhosts
   name: Summon Ghosts
   description: Makes all current ghosts permanently invisible
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     useDelay: 120
diff --git a/Resources/Prototypes/Magic/forcewall_spells.yml b/Resources/Prototypes/Magic/forcewall_spells.yml
index d3d8fef7608..a047a4a6f77 100644
--- a/Resources/Prototypes/Magic/forcewall_spells.yml
+++ b/Resources/Prototypes/Magic/forcewall_spells.yml
@@ -2,7 +2,7 @@
   id: ActionForceWall
   name: Forcewall
   description: Creates a magical barrier.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     useDelay: 10
diff --git a/Resources/Prototypes/Magic/knock_spell.yml b/Resources/Prototypes/Magic/knock_spell.yml
index e2c3dcfd4c7..a0e22ec8f4b 100644
--- a/Resources/Prototypes/Magic/knock_spell.yml
+++ b/Resources/Prototypes/Magic/knock_spell.yml
@@ -2,7 +2,7 @@
   id: ActionKnock
   name: Knock
   description: This spell opens nearby doors.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     useDelay: 10
diff --git a/Resources/Prototypes/Magic/projectile_spells.yml b/Resources/Prototypes/Magic/projectile_spells.yml
index b8db7557bba..e2697c59b18 100644
--- a/Resources/Prototypes/Magic/projectile_spells.yml
+++ b/Resources/Prototypes/Magic/projectile_spells.yml
@@ -2,7 +2,7 @@
   id: ActionFireball
   name: Fireball
   description: Fires an explosive fireball towards the clicked location.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Magic
   - type: WorldTargetAction
@@ -29,7 +29,7 @@
   parent: ActionFireball
   name: Fireball II
   description: Fires a fireball, but faster!
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: WorldTargetAction
     useDelay: 10
@@ -52,7 +52,7 @@
   parent: ActionFireball
   name: Fireball III
   description: The fastest fireball in the west!
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: WorldTargetAction
       useDelay: 8
diff --git a/Resources/Prototypes/Magic/rune_spells.yml b/Resources/Prototypes/Magic/rune_spells.yml
index 42022f57850..da072004b1f 100644
--- a/Resources/Prototypes/Magic/rune_spells.yml
+++ b/Resources/Prototypes/Magic/rune_spells.yml
@@ -2,7 +2,7 @@
   id: ActionFlashRune
   name: Flash Rune
   description: Summons a rune that flashes if used.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     useDelay: 10
@@ -17,7 +17,7 @@
   id: ActionExplosionRune
   name: Explosion Rune
   description: Summons a rune that explodes if used.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     useDelay: 20
@@ -32,7 +32,7 @@
   id: ActionIgniteRune
   name: Ignite Rune
   description: Summons a rune that ignites if used.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     useDelay: 15
@@ -47,7 +47,7 @@
   id: ActionStunRune
   name: Stun Rune
   description: Summons a rune that stuns if used.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     useDelay: 10
diff --git a/Resources/Prototypes/Magic/smite_spells.yml b/Resources/Prototypes/Magic/smite_spells.yml
index e629e565058..a0875211307 100644
--- a/Resources/Prototypes/Magic/smite_spells.yml
+++ b/Resources/Prototypes/Magic/smite_spells.yml
@@ -2,7 +2,7 @@
   id: ActionSmite
   name: Smite
   description: Instantly gibs a target.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: EntityTargetAction
     useDelay: 60
diff --git a/Resources/Prototypes/Magic/spawn_spells.yml b/Resources/Prototypes/Magic/spawn_spells.yml
index 3f8148b83cb..2800499fcab 100644
--- a/Resources/Prototypes/Magic/spawn_spells.yml
+++ b/Resources/Prototypes/Magic/spawn_spells.yml
@@ -2,7 +2,7 @@
   id: ActionSpawnMagicarpSpell
   name: Summon Magicarp
   description: This spell summons three Magi-Carp to your aid! May or may not turn on user.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: WorldTargetAction
     useDelay: 10
diff --git a/Resources/Prototypes/Magic/staves.yml b/Resources/Prototypes/Magic/staves.yml
index ef94a3910fd..d9b71e39a85 100644
--- a/Resources/Prototypes/Magic/staves.yml
+++ b/Resources/Prototypes/Magic/staves.yml
@@ -34,7 +34,7 @@
 
 - type: entity
   id: ActionRgbLight
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: EntityTargetAction
     whitelist: { components: [ PointLight ] }
diff --git a/Resources/Prototypes/Magic/teleport_spells.yml b/Resources/Prototypes/Magic/teleport_spells.yml
index cc89cf8ee0d..1439dcea17e 100644
--- a/Resources/Prototypes/Magic/teleport_spells.yml
+++ b/Resources/Prototypes/Magic/teleport_spells.yml
@@ -2,7 +2,7 @@
   id: ActionBlink
   name: Blink
   description: Teleport to the clicked location.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: WorldTargetAction
     useDelay: 10
diff --git a/Resources/Prototypes/Magic/utility_spells.yml b/Resources/Prototypes/Magic/utility_spells.yml
index dccdda37898..6c0b08d9b63 100644
--- a/Resources/Prototypes/Magic/utility_spells.yml
+++ b/Resources/Prototypes/Magic/utility_spells.yml
@@ -2,7 +2,7 @@
   id: ActionChargeSpell
   name: Charge
   description: Adds a charge back to your wand
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     useDelay: 30
diff --git a/Resources/Prototypes/Nyanotrasen/Actions/types.yml b/Resources/Prototypes/Nyanotrasen/Actions/types.yml
index cab8f4a1f4e..9bdb13397a6 100644
--- a/Resources/Prototypes/Nyanotrasen/Actions/types.yml
+++ b/Resources/Prototypes/Nyanotrasen/Actions/types.yml
@@ -2,7 +2,7 @@
   id: ActionEatMouse
   name: action-name-eat-mouse
   description: action-description-eat-mouse
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     icon: Nyanotrasen/Icons/verbiconfangs.png
@@ -12,7 +12,7 @@
   id: ActionHairball
   name: action-name-hairball
   description: action-description-hairball
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     charges: 1
diff --git a/Resources/Prototypes/Nyanotrasen/Catalog/Fills/Backpacks/StarterGear/backpack.yml b/Resources/Prototypes/Nyanotrasen/Catalog/Fills/Backpacks/StarterGear/backpack.yml
index 810f9ec03b6..c4843240ebb 100644
--- a/Resources/Prototypes/Nyanotrasen/Catalog/Fills/Backpacks/StarterGear/backpack.yml
+++ b/Resources/Prototypes/Nyanotrasen/Catalog/Fills/Backpacks/StarterGear/backpack.yml
@@ -1,5 +1,5 @@
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackScience
   id: ClothingBackpackMantisFilled
   components:
diff --git a/Resources/Prototypes/Nyanotrasen/Catalog/Fills/Backpacks/StarterGear/duffelbag.yml b/Resources/Prototypes/Nyanotrasen/Catalog/Fills/Backpacks/StarterGear/duffelbag.yml
index 88e33cdd252..6f4e2fea626 100644
--- a/Resources/Prototypes/Nyanotrasen/Catalog/Fills/Backpacks/StarterGear/duffelbag.yml
+++ b/Resources/Prototypes/Nyanotrasen/Catalog/Fills/Backpacks/StarterGear/duffelbag.yml
@@ -1,5 +1,5 @@
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackDuffelScience
   id: ClothingBackpackDuffelMantisFilled
   components:
diff --git a/Resources/Prototypes/Nyanotrasen/Catalog/Fills/Backpacks/StarterGear/satchel.yml b/Resources/Prototypes/Nyanotrasen/Catalog/Fills/Backpacks/StarterGear/satchel.yml
index e90759ac8fb..50106b11823 100644
--- a/Resources/Prototypes/Nyanotrasen/Catalog/Fills/Backpacks/StarterGear/satchel.yml
+++ b/Resources/Prototypes/Nyanotrasen/Catalog/Fills/Backpacks/StarterGear/satchel.yml
@@ -1,5 +1,5 @@
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: ClothingBackpackSatchelScience
   id: ClothingBackpackSatchelMantisFilled
   components:
diff --git a/Resources/Prototypes/Nyanotrasen/Catalog/Fills/Books/lore.yml b/Resources/Prototypes/Nyanotrasen/Catalog/Fills/Books/lore.yml
index 6c3eb06108f..2a8173dbec2 100644
--- a/Resources/Prototypes/Nyanotrasen/Catalog/Fills/Books/lore.yml
+++ b/Resources/Prototypes/Nyanotrasen/Catalog/Fills/Books/lore.yml
@@ -2,7 +2,7 @@
   name: epistemics book
   parent: BookRandom # placeholder
   id: BookSalvageEpistemics
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   description: 'A metallic hardcover book.'
 
 - type: entity
diff --git a/Resources/Prototypes/Nyanotrasen/Catalog/Fills/Paper/salvage_lore.yml b/Resources/Prototypes/Nyanotrasen/Catalog/Fills/Paper/salvage_lore.yml
index f37c3bbdb3e..dbccfd4484b 100644
--- a/Resources/Prototypes/Nyanotrasen/Catalog/Fills/Paper/salvage_lore.yml
+++ b/Resources/Prototypes/Nyanotrasen/Catalog/Fills/Paper/salvage_lore.yml
@@ -18,7 +18,7 @@
 
 - type: entity
   id: PaperWrittenSalvageLoreGaming1
-  noSpawn: true # keep this from spamming spawn sheet
+  categories: [ HideSpawnMenu ] # keep this from spamming spawn sheet
   suffix: "Salvage: Lore: Gaming 1"
   parent: Paper
   components:
@@ -31,7 +31,7 @@
        - Alexander
 - type: entity
   id: PaperWrittenSalvageLoreGaming2
-  noSpawn: true # keep this from spamming spawn sheet
+  categories: [ HideSpawnMenu ] # keep this from spamming spawn sheet
   suffix: "Salvage: Lore: Gaming 2"
   parent: Paper
   components:
@@ -51,7 +51,7 @@
       What even are you trying to do here, Leah? - Your Friendly DM
 - type: entity
   id: PaperWrittenSalvageLoreGaming3
-  noSpawn: true # keep this from spamming spawn sheet
+  categories: [ HideSpawnMenu ] # keep this from spamming spawn sheet
   suffix: "Salvage: Lore: Gaming 3"
   parent: Paper
   components:
@@ -65,7 +65,7 @@
       Oh dear goodness they just started randomly killing everybody
 - type: entity
   id: PaperWrittenSalvageLoreGaming4
-  noSpawn: true # keep this from spamming spawn sheet
+  categories: [ HideSpawnMenu ] # keep this from spamming spawn sheet
   suffix: "Salvage: Lore: Gaming 4"
   parent: Paper
   components:
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Clothing/Head/hardsuit-helmets.yml b/Resources/Prototypes/Nyanotrasen/Entities/Clothing/Head/hardsuit-helmets.yml
index 3896cd1ef1b..9625ec95d6b 100644
--- a/Resources/Prototypes/Nyanotrasen/Entities/Clothing/Head/hardsuit-helmets.yml
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Clothing/Head/hardsuit-helmets.yml
@@ -1,7 +1,7 @@
 - type: entity
   parent: ClothingHeadHelmetHardsuitRd
   id: ClothingHeadHelmetHardsuitMystagogue
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: mystagogue's hardsuit helmet
   description: Lightweight hardsuit helmet that has a galaxy-first psionic passthrough system.
   components:
@@ -15,7 +15,7 @@
 - type: entity
   parent: ClothingHeadHelmetHardsuitSyndie
   id: ClothingHeadHelmetHardsuitSyndieReverseEngineered
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: SA-122 combat hardsuit helmet
   description: An advanced hardsuit helmet designed for work in special operations.
   components:
@@ -27,7 +27,7 @@
 - type: entity
   parent: ClothingHeadHelmetHardsuitCybersun
   id: ClothingHeadHelmetHardsuitJuggernautReverseEngineered
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: SA-126 combat hardsuit helmet
   description: An assault hardsuit helmet featuring a top-secret translucent polymer.
   components:
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Effects/lightning.yml b/Resources/Prototypes/Nyanotrasen/Entities/Effects/lightning.yml
index 35f5ee06e94..e6a3a575a23 100644
--- a/Resources/Prototypes/Nyanotrasen/Entities/Effects/lightning.yml
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Effects/lightning.yml
@@ -2,7 +2,7 @@
   parent: BaseLightning
   id: LightningNoospheric
   name: noospheric lightning
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Electrified
     enabled: false
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Markers/Spawners/ghost_roles.yml b/Resources/Prototypes/Nyanotrasen/Entities/Markers/Spawners/ghost_roles.yml
index a21976fa7a5..d0207e362e3 100644
--- a/Resources/Prototypes/Nyanotrasen/Entities/Markers/Spawners/ghost_roles.yml
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Markers/Spawners/ghost_roles.yml
@@ -3,7 +3,7 @@
   name: ghost role spawn point
   suffix: Ifrit
   parent: MarkerBase
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: GhostRoleMobSpawner
     prototype: MobIfritFamiliar
@@ -22,7 +22,7 @@
   id: SpawnPointGhostFugitive
   name: ghost role spawn point
   parent: MarkerBase
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   # - type: GhostRoleMobSpawner
   #   prototype: MobHumanFugitive # Todo
@@ -56,7 +56,7 @@
 #  name: ghost role spawn point
 #  suffix: Vampire spider
 #  parent: MarkerBase
-#  noSpawn: true
+#  categories: [ HideSpawnMenu ]
 #  components:
 #  - type: GhostRoleMobSpawner
 #    prototype: MobGiantSpiderVampireAngry
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Player/special.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Player/special.yml
index 70628ec4e51..d133377e01b 100644
--- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Player/special.yml
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Player/special.yml
@@ -2,7 +2,7 @@
   id: MobObserverTelegnostic
   name: telegnostic projection
   description: Ominous. Placeholder sprite.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: Sprite
     overrideContainerOcclusion: true # Ghosts always show up regardless of where they're contained.
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/Oni.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/Oni.yml
index 8a5663dce45..8ac15fd865c 100644
--- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/Oni.yml
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/Oni.yml
@@ -53,5 +53,5 @@
   name: Urist McOni
   parent: MobHumanDummy
   id: MobOniDummy
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   description: A dummy oni meant to be used in character setup.
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml
index 44779fe9508..3b1776d044a 100644
--- a/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Mobs/Species/felinid.yml
@@ -90,7 +90,7 @@
   name: Urist McHands
   parent: MobHumanDummy
   id: MobFelinidDummy
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   description: A dummy felinid meant to be used in character setup.
   components:
   - type: HumanoidAppearance
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Consumable/Food/ration.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Consumable/Food/ration.yml
index 177d6151ccb..bd08bec9ddc 100644
--- a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Consumable/Food/ration.yml
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Consumable/Food/ration.yml
@@ -1,6 +1,6 @@
 # PSB, Prepacked Sustenance Bar. With variety.
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: FoodPacketTrash
   id: FoodPSBTrash
   name: psb wrapper
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/base_mail.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/base_mail.yml
index 75007db11ca..320129fa554 100644
--- a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/base_mail.yml
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/base_mail.yml
@@ -111,7 +111,7 @@
 # This empty parcel is allowed to exist and evade the tests for the admin
 # mailto command.
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMail
   id: MailAdminFun
   suffix: adminfun
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Weapons/Guns/Projectiles/shotgun.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Weapons/Guns/Projectiles/shotgun.yml
index 42826c94cba..47d65ce8f7c 100644
--- a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Weapons/Guns/Projectiles/shotgun.yml
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Weapons/Guns/Projectiles/shotgun.yml
@@ -1,7 +1,7 @@
 - type: entity
   id: PelletShotgunSoulbreaker
   name: pellet (.50 soulbreaker)
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseBulletPractice
   components:
   - type: Sprite
diff --git a/Resources/Prototypes/Nyanotrasen/GameRules/events.yml b/Resources/Prototypes/Nyanotrasen/GameRules/events.yml
index de9ea15a699..8612fb0fec7 100644
--- a/Resources/Prototypes/Nyanotrasen/GameRules/events.yml
+++ b/Resources/Prototypes/Nyanotrasen/GameRules/events.yml
@@ -2,7 +2,7 @@
 - type: entity
   id: NoosphericStorm
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: StationEvent
       startAnnouncement: true
@@ -25,7 +25,7 @@
   - type: MidRoundAntagRule
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMidRoundAntag
   id: RatKingSpawn
   components:
@@ -33,7 +33,7 @@
     spawner: SpawnPointGhostRatKing
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseMidRoundAntag
   id: ParadoxAnomalySpawn
   components:
@@ -44,7 +44,7 @@
 - type: entity
   id: BaseGlimmerEvent
   parent: BaseGameRule
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: StationEvent
       # Favor glimmer events just a little more than regular events.
@@ -56,7 +56,7 @@
 - type: entity
   id: MundaneDischarge
   parent: BaseGlimmerEvent
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: StationEvent
       reoccurrenceDelay: 15
@@ -69,7 +69,7 @@
 - type: entity
   id: NoosphericZap
   parent: BaseGlimmerEvent
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: StationEvent
       weight: 25 # Guaranteed to happen every once in a while, but with intervals between incidents
@@ -81,7 +81,7 @@
 - type: entity
   id: NoosphericFry
   parent: BaseGlimmerEvent
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: GlimmerEvent
       minimumGlimmer: 550
@@ -91,7 +91,7 @@
 - type: entity
   id: PsionicCatGotYourTongue
   parent: BaseGlimmerEvent
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: GlimmerEvent
       minimumGlimmer: 400
@@ -103,7 +103,7 @@
 - type: entity
   id: MassMindSwap
   parent: BaseGlimmerEvent
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: GlimmerEvent
       minimumGlimmer: 900
@@ -116,7 +116,7 @@
   abstract: true
   parent: BaseGlimmerEvent
   id: BaseGlimmerSignaturesEvent
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: GlimmerEvent
     minimumGlimmer: 300
@@ -126,7 +126,7 @@
 - type: entity
   id: GlimmerWispSpawn
   parent: BaseGlimmerSignaturesEvent
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: GlimmerMobRule
     mobPrototype: MobGlimmerWisp
@@ -134,7 +134,7 @@
 - type: entity
   parent: BaseGlimmerSignaturesEvent
   id: FreeProber
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: FreeProberRule
 
@@ -142,7 +142,7 @@
 - type: entity
   parent: BaseGlimmerSignaturesEvent
   id: GlimmerRandomSentience
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: StationEvent
     weight: 7
@@ -158,7 +158,7 @@
 - type: entity
   parent: BaseGlimmerSignaturesEvent
   id: GlimmerRevenantSpawn
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
     - type: GlimmerEvent
       minimumGlimmer: 450
@@ -170,7 +170,7 @@
 - type: entity
   parent: BaseGlimmerSignaturesEvent
   id: GlimmerMiteSpawn
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: GlimmerEvent
     minimumGlimmer: 250
diff --git a/Resources/Prototypes/Nyanotrasen/Objectives/traitor.yml b/Resources/Prototypes/Nyanotrasen/Objectives/traitor.yml
index e6e497003d5..f9304c4c3b8 100644
--- a/Resources/Prototypes/Nyanotrasen/Objectives/traitor.yml
+++ b/Resources/Prototypes/Nyanotrasen/Objectives/traitor.yml
@@ -1,5 +1,5 @@
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseTraitorStealObjective
   id: MantisKnifeStealObjective
   components:
@@ -14,7 +14,7 @@
 #  parent: BaseTraitorObjective
 #  name: objective-condition-become-golem-title
 #  description: objective-condition-become-golem-description.
-#  noSpawn: true
+#  categories: [ HideSpawnMenu ]
 #  components:
 #  - type: NotJobRequirement
 #    job: Chaplain
@@ -33,7 +33,7 @@
 - type: entity
   id: RaiseGlimmerObjective
   parent: BaseTraitorObjective
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   name: Raise Glimmer.
   description: Get the glimmer above the specified amount.
   components:
diff --git a/Resources/Prototypes/Objectives/dragon.yml b/Resources/Prototypes/Objectives/dragon.yml
index 2cf7eb292f7..70cb4643de8 100644
--- a/Resources/Prototypes/Objectives/dragon.yml
+++ b/Resources/Prototypes/Objectives/dragon.yml
@@ -13,7 +13,7 @@
       - DragonRole
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseDragonObjective
   id: CarpRiftsObjective
   components:
@@ -30,7 +30,7 @@
   - type: CarpRiftsCondition
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: [BaseDragonObjective, BaseSurviveObjective]
   id: DragonSurviveObjective
   name: Survive
diff --git a/Resources/Prototypes/Objectives/ninja.yml b/Resources/Prototypes/Objectives/ninja.yml
index fb94f2b3788..864bff25c6f 100644
--- a/Resources/Prototypes/Objectives/ninja.yml
+++ b/Resources/Prototypes/Objectives/ninja.yml
@@ -13,7 +13,7 @@
       - NinjaRole
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseNinjaObjective
   id: DoorjackObjective
   components:
@@ -29,7 +29,7 @@
   - type: DoorjackCondition
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseNinjaObjective
   id: StealResearchObjective
   description: Your gloves can be used to hack a research server and steal its precious data. If epistemics has been slacking you'll have to get to work. # DeltaV - Epistemics Department replacing Science
@@ -45,7 +45,7 @@
   - type: StealResearchCondition
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: [BaseNinjaObjective, BaseCodeObjective]
   id: SpiderChargeObjective
   description: This bomb can be detonated in a specific location. Note that the bomb will not work anywhere else!
@@ -56,7 +56,7 @@
       state: icon
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: [BaseNinjaObjective, BaseSurviveObjective]
   id: NinjaSurviveObjective
   name: Survive
@@ -68,7 +68,7 @@
       state: icon
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: [BaseNinjaObjective, BaseCodeObjective]
   id: TerrorObjective
   name: Call in a threat
@@ -80,7 +80,7 @@
       state: red_phone
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: [BaseNinjaObjective, BaseCodeObjective]
   id: MassArrestObjective
   name: Set everyone to wanted
diff --git a/Resources/Prototypes/Objectives/thief.yml b/Resources/Prototypes/Objectives/thief.yml
index 18154850973..e556171a1fa 100644
--- a/Resources/Prototypes/Objectives/thief.yml
+++ b/Resources/Prototypes/Objectives/thief.yml
@@ -55,7 +55,7 @@
 # Collections
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealCollectionObjective
   id: FigurineStealCollectionObjective
   components:
@@ -67,7 +67,7 @@
     difficulty: 0.25
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealCollectionObjective
   id: HeadCloakStealCollectionObjective
   components:
@@ -79,7 +79,7 @@
     difficulty: 1.5
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealCollectionObjective
   id: HeadBedsheetStealCollectionObjective
   components:
@@ -91,7 +91,7 @@
     difficulty: 1.0
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealCollectionObjective
   id: StampStealCollectionObjective
   components:
@@ -103,7 +103,7 @@
     difficulty: 1.0
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealCollectionObjective
   id: DoorRemoteStealCollectionObjective
   components:
@@ -115,7 +115,7 @@
     difficulty: 1.5
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealCollectionObjective
   id: TechnologyDiskStealCollectionObjective
   components:
@@ -130,7 +130,7 @@
     difficulty: 0.8
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealCollectionObjective
   id: IDCardsStealCollectionObjective
   components:
@@ -145,7 +145,7 @@
 
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealCollectionObjective
   id: LAMPStealCollectionObjective
   components:
@@ -163,7 +163,7 @@
 # steal item
 
 - type: entity                                      #Security subgroup
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealObjective
   id: ForensicScannerStealObjective
   components:
@@ -175,7 +175,7 @@
     difficulty: 1
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealObjective
   id: FlippoEngravedLighterStealObjective
   components:
@@ -187,7 +187,7 @@
     difficulty: 0.8
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealObjective
   id: ClothingHeadHatWardenStealObjective
   components:
@@ -197,7 +197,7 @@
     difficulty: 1.2
 
 - type: entity                                      #Medical subgroup
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealObjective
   id: ClothingOuterHardsuitVoidParamedStealObjective
   components:
@@ -209,7 +209,7 @@
     difficulty: 1
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealObjective
   id: MedicalTechFabCircuitboardStealObjective
   components:
@@ -221,7 +221,7 @@
     difficulty: 1
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealObjective
   id: ClothingHeadsetAltMedicalStealObjective
   components:
@@ -233,7 +233,7 @@
     difficulty: 1
 
 - type: entity                                      #Engineering subgroup
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealObjective
   id: FireAxeStealObjective
   components:
@@ -245,7 +245,7 @@
     difficulty: 0.8
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealObjective
   id: AmePartFlatpackStealObjective
   components:
@@ -257,7 +257,7 @@
     difficulty: 1
 
 - type: entity                                      #Cargo subgroup
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealObjective
   id: ExpeditionsCircuitboardStealObjective
   components:
@@ -269,7 +269,7 @@
     difficulty: 0.7
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealObjective
   id: CargoShuttleCircuitboardStealObjective
   components:
@@ -281,7 +281,7 @@
     difficulty: 0.7
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealObjective
   id: SalvageShuttleCircuitboardStealObjective
   components:
@@ -293,7 +293,7 @@
     difficulty: 0.7
 
 - type: entity                                      #Service subgroup
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealObjective
   id: ClothingEyesHudBeerStealObjective
   components:
@@ -305,7 +305,7 @@
     difficulty: 0.3
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealObjective
   id: BibleStealObjective
   components:
@@ -317,7 +317,7 @@
     difficulty: 0.4
 
 - type: entity                                      #Other subgroup
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealObjective
   id: ClothingNeckGoldmedalStealObjective
   components:
@@ -329,7 +329,7 @@
     difficulty: 1
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealObjective
   id: ClothingNeckClownmedalStealObjective
   components:
@@ -343,7 +343,7 @@
 # Structures
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealStructureObjective
   id: NuclearBombStealObjective
   components:
@@ -355,7 +355,7 @@
     difficulty: 2.5 #Good luck
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealStructureObjective
   id: FaxMachineCaptainStealObjective
   components:
@@ -367,7 +367,7 @@
     difficulty: 2
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealStructureObjective
   id: ChemDispenserStealObjective
   components:
@@ -379,7 +379,7 @@
     difficulty: 1
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealStructureObjective
   id: XenoArtifactStealObjective
   components:
@@ -391,7 +391,7 @@
     difficulty: 0.5
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealStructureObjective
   id: FreezerHeaterStealObjective
   components:
@@ -403,7 +403,7 @@
     difficulty: 0.5
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealStructureObjective
   id: TegStealObjective
   components:
@@ -415,7 +415,7 @@
     difficulty: 1
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealStructureObjective
   id: BoozeDispenserStealObjective
   components:
@@ -427,7 +427,7 @@
     difficulty: 0.5
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealStructureObjective
   id: AltarNanotrasenStealObjective
   components:
@@ -439,7 +439,7 @@
     difficulty: 0.5
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealStructureObjective
   id: PlantRDStealObjective
   components:
@@ -453,7 +453,7 @@
 # Animal
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealAnimalObjective
   id: IanStealObjective
   components:
@@ -465,7 +465,7 @@
     difficulty: 2.5
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealAnimalObjective
   id: BingusStealObjective
   components:
@@ -475,7 +475,7 @@
     difficulty: 1
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealAnimalObjective
   id: McGriffStealObjective
   components:
@@ -487,7 +487,7 @@
     difficulty: 1
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealAnimalObjective
   id: WalterStealObjective
   components:
@@ -499,7 +499,7 @@
     difficulty: 1
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealAnimalObjective
   id: MortyStealObjective
   components:
@@ -509,7 +509,7 @@
     difficulty: 0.5
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealAnimalObjective
   id: RenaultStealObjective
   components:
@@ -521,7 +521,7 @@
     difficulty: 2
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealAnimalObjective
   id: ShivaStealObjective
   components:
@@ -533,7 +533,7 @@
     difficulty: 2
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseThiefStealAnimalObjective
   id: TropicoStealObjective
   components:
@@ -547,7 +547,7 @@
 # Escape
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: [BaseThiefObjective, BaseLivingObjective]
   id: EscapeThiefShuttleObjective
   name: Escape to centcom alive and unrestrained.
diff --git a/Resources/Prototypes/Objectives/traitor.yml b/Resources/Prototypes/Objectives/traitor.yml
index b00d12529af..34efe5a166c 100644
--- a/Resources/Prototypes/Objectives/traitor.yml
+++ b/Resources/Prototypes/Objectives/traitor.yml
@@ -34,7 +34,7 @@
     limit: 2 # there is usually only 1 of each steal objective, have 2 max for drama
 
 - type: entity # Head of Security steal objective.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseTraitorStealObjective
   id: HoSAntiqueWeaponStealObjective
   components:
@@ -50,7 +50,7 @@
 # state
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: [BaseTraitorObjective, BaseLivingObjective]
   id: EscapeShuttleObjective
   name: Escape to centcom alive and unrestrained.
@@ -64,7 +64,7 @@
   - type: EscapeShuttleCondition
 
 ##- type: entity # DeltaV
-#  noSpawn: true
+#  categories: [ HideSpawnMenu ]
 #  parent: BaseTraitorObjective
 #  id: DieObjective
 #  name: Die a glorious death
@@ -83,7 +83,7 @@
 #  - type: DieCondition
 
 #- type: entity
-#  noSpawn: true
+#  categories: [ HideSpawnMenu ]
 #  parent: [BaseTraitorObjective, BaseLivingObjective]
 #  id: HijackShuttleObjective
 #  name: Hijack emergency shuttle
@@ -99,7 +99,7 @@
 # kill
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: [BaseTraitorObjective, BaseKillObjective]
   id: KillRandomPersonObjective
   description: Do it however you like, just make sure they don't make it to centcom.
@@ -112,7 +112,7 @@
   - type: PickRandomPerson
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: [BaseTraitorObjective, BaseKillObjective]
   id: KillRandomHeadObjective
   description: We need this head gone and you probably know why. Good luck, agent.
@@ -133,7 +133,7 @@
 # social
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: [BaseTraitorSocialObjective, BaseKeepAliveObjective]
   id: RandomTraitorAliveObjective
   description: Identify yourself at your own risk. We just need them alive.
@@ -145,7 +145,7 @@
   - type: RandomTraitorAlive
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: [BaseTraitorSocialObjective, BaseHelpProgressObjective]
   id: RandomTraitorProgressObjective
   description: Identify yourself at your own risk. We just need them to succeed.
@@ -171,7 +171,7 @@
     owner: job-name-cmo
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseCMOStealObjective
   id: CMOHyposprayStealObjective
   components:
@@ -179,7 +179,7 @@
     stealGroup: Hypospray
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseCMOStealObjective
   id: CMOCrewMonitorStealObjective
   components:
@@ -199,7 +199,7 @@
     owner: job-name-rd
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseRDStealObjective
   id: RDHardsuitStealObjective
   components:
@@ -210,7 +210,7 @@
     difficulty: 3
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseRDStealObjective
   id: HandTeleporterStealObjective
   components:
@@ -220,7 +220,7 @@
 ## hos
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseTraitorStealObjective
   id: SecretDocumentsStealObjective
   components:
@@ -236,7 +236,7 @@
 ## ce
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseTraitorStealObjective
   id: MagbootsStealObjective
   components:
@@ -249,7 +249,7 @@
 ## qm
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseTraitorStealObjective
   id: ClipboardStealObjective
   components:
@@ -262,7 +262,7 @@
 ## hop
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseTraitorStealObjective
   id: CorgiMeatStealObjective
   components:
@@ -288,7 +288,7 @@
     job: Captain
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseCaptainObjective
   id: CaptainIDStealObjective
   components:
@@ -296,7 +296,7 @@
     stealGroup: CaptainIDCard
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseCaptainObjective
   id: CaptainJetpackStealObjective
   components:
@@ -304,7 +304,7 @@
     stealGroup: JetpackCaptainFilled
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseCaptainObjective
   id: CaptainGunStealObjective
   components:
@@ -313,7 +313,7 @@
     owner: job-name-captain
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseCaptainObjective
   id: NukeDiskStealObjective
   components:
@@ -328,7 +328,7 @@
     owner: objective-condition-steal-station
 
 - type: entity
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   parent: BaseTraitorStealObjective
   id: StealSupermatterSliverObjective
   components:
diff --git a/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml b/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml
index c7c659bc531..6aa6d02aecb 100644
--- a/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml
+++ b/Resources/Prototypes/Roles/Jobs/Civilian/mime.yml
@@ -41,7 +41,7 @@
   id: ActionMimeInvisibleWall
   name: Create Invisible Wall
   description: Create an invisible wall in front of you, if placeable there.
-  noSpawn: true
+  categories: [ HideSpawnMenu ]
   components:
   - type: InstantAction
     priority: -1
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/BanDurations.xml b/Resources/ServerInfo/Guidebook/ServerRules/BanDurations.xml
new file mode 100644
index 00000000000..2c85346b49d
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/BanDurations.xml
@@ -0,0 +1,17 @@
+<Document>
+  # Ban Durations
+
+  Bans can be appealed at forum.ss14.io in the ban appeals section.
+
+  ## Temporary
+  Temporary bans will be lifted automatically after a certain amount of time. If they are a game ban, they will tell you how much time is remaining when you try to connect.
+
+  ## Indefinite
+  These bans will only be removed on a successful appeal on the forums. Any ban which doesn't tell you when it expires and doesn't specify otherwise can be presumed to be an indefinite ban.
+
+  ## Voucher
+  This is an indefinite ban which may only be appealed both with a successful appeal and which require a voucher of good behavior from the administrative team of a well-known or at least decently active SS13/SS14 server in order for the appeal to be considered. Voucher bans typically cannot be appealed for at least six months after being issued. Without a voucher, a player can only attempt to appeal a voucher ban once, and only if the ban was inappropriately placed. Voucher bans are typically only placed as a result of an unsuccessful appeal of an indefinite game ban by players with a history of bans and of causing issues.
+
+  ## Permanent
+  This is a ban that is only appealable if the ban was inappropriately placed, including if the ban should not have been permanent. If the result of the appeal is that the ban was appropriately placed, the ban may not be appealed again and will not be lifted. These bans are extremely rare, but are applied to players who continually cause problems even after a voucher ban or users who have completely unacceptable behavior may be permanently removed.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/BanTypes.xml b/Resources/ServerInfo/Guidebook/ServerRules/BanTypes.xml
new file mode 100644
index 00000000000..b10ea3c393b
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/BanTypes.xml
@@ -0,0 +1,11 @@
+<Document>
+  # Ban Types
+
+  Bans can be appealed at forum.ss14.io in the ban appeals section.
+
+  ## Role Ban
+  Also called a "job ban", this ban prevents your character from joining or late-joining a round as one or more jobs or roles. These are often used in response to problematic behavior in particular departments or address gross inexperience in important roles such as heads of staff. These bans do not mechanically prevent you from switching to the role during a round or acting as that role, but doing so is considered ban evasion.
+
+  ## Game Ban
+  Also called a "server ban", this ban prevents you from connecting to all Wizard's Den servers.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC0.xml b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC0.xml
new file mode 100644
index 00000000000..7b8cfbcf61e
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC0.xml
@@ -0,0 +1,19 @@
+<Document>
+  # Core Rules
+  These rules apply at all times, including between rounds.
+
+  - [textlink="1. Admins have final say" link="RuleC1"]
+  - [textlink="2. Don't be a dick" link="RuleC2"]
+  - [textlink="3. No Hate Speech or Discriminatory Language" link="RuleC3"]
+  - [textlink="4. No sexual content/themes, including erotic roleplay (ERP) and no shock content" link="RuleC4"]
+  - [textlink="5. Do not use out of game methods to communicate with other players" link="RuleC5"]
+  - [textlink="6. Do not attempt to evade bans" link="RuleC6"]
+  - [textlink="7. Only use English" link="RuleC7"]
+  - [textlink="8. Do not exploit the game, use cheats, or macros" link="RuleC8"]
+  - [textlink="9. Do not use multiple accounts, or alt accounts, and do not share accounts" link="RuleC9"]
+  - [textlink="10. Do not abuse or ignore admin messages" link="RuleC10"]
+  - [textlink="11. Do not threaten to ahelp other players or argue with them about rules" link="RuleC11"]
+  - [textlink="12. Players must be and act at least 16 years old" link="RuleC12"]
+  - [textlink="13. Use realistic character names, and do not use names of famous people" link="RuleC13"]
+  - [textlink="14. Do not use LOOC or OOC to share current round information" link="RuleC14"]
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC10AHelp.xml b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC10AHelp.xml
new file mode 100644
index 00000000000..2d639c5b84a
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC10AHelp.xml
@@ -0,0 +1,29 @@
+<Document>
+  # Core Rule 10 - Do not abuse or ignore admin messages
+  Admin help, or "ahelp", is the system used by admins to communicate with specific players in the game. Only use admin help for things requiring admin attention. If you ignore messages admins send to you via ahelp, or disconnect during an ahelp, you may be banned. If you urgently need to leave during an ahelp, you may do so but will likely need to continue the ahelp on the forums. Do not admin check, be hostile/aggressive, request events, or spam. IC methods of contacting admins, like prayers, faxes, red phones, and banana phones, should be used when there is not an issue.
+
+  Admins are not always online, but all ahelps are automatically relayed to discord. For various reasons, admins might not respond to an ahelp even if they've handled it. A lack of response does not necessarily mean that an ahelp was ignored.
+
+  ## Should I ahelp X?
+  You can ahelp anytime you genuinely think a player is breaking a rule. Not all ahelps end up being for something that an admin needs to intervene in, but that's ok, admins would rather have people occasionally report things that turn out to not be an issue than miss reports for actual issues because someone was unsure, or get those reports late because someone waited until the end of the round to be more sure.
+
+  The most common reason players give for not ahelping issues is that they don't want to waste admin time, but it only takes a few seconds for an admin to check if someone is an antagonist. If you are ahelping too many things, an admin will let you know. If you're not being told to stop reporting something or to report less things, then you can safely assume that you aren't causing any issues.
+
+  # What should I include in an ahelp?
+  At a minimum, admins need to know what the issue is to be able to address an ahelp. Don't send ahelp messages with no information about what your question or the issue is. Messages like "hello" are often considered admin checking.
+
+  If you can, an ideal ahelp message includes what the issue is along with who is causing it and their character's name if possible.
+
+  # Examples
+  Appropriate uses of ahelp:
+  - reporting people who you think are violating rules,
+  - asking questions about rules,
+  - asking for a temporary exemption from a rule, and
+  - request a minor gimmick, like a TC trade or item spawn.
+
+  Inappropriate uses of ahelp:
+  - checking if an admin is online, including sending messages without any information about the issue like "hello" or incomprehensible messages,
+  - being hostile or aggressive,
+  - requesting events, and
+  - spamming messages about the same issue.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC11AhelpThreats.xml b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC11AhelpThreats.xml
new file mode 100644
index 00000000000..47420264946
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC11AhelpThreats.xml
@@ -0,0 +1,20 @@
+<Document>
+  # Core Rule 11 - Do not threaten to ahelp other players or argue with them about rules
+  Don't threaten to ahelp a player, don't tell them you are ahelping them, and don't tell them you did ahelp them. You can argue in character about Space Law, but do not argue about whether something is or is not against the rules. If you think someone is breaking a rule, ahelp them. If you don't think someone is breaking a rule, don't ahelp them. Either way, the best thing that you can do once you after is to continue in-character.
+
+  ## Example Scenario 1
+  You are a security officer and think someone who is causing a ton of problems for security is not an antag and is breaking the rules by doing so.
+
+  [color=#a4885c]Good:[/color] Since you think they are breaking a rule, you ahelp them when you're able to. You continue in-character by arresting them for the crimes that they committed.
+
+  [color=#a4885c]Bad:[/color] You decide not to ahelp them. You kill them and tell them "you're lucky I didn't report you to the admins".
+
+  [color=#a4885c]Bad:[/color] Since you think they are breaking a rule, you ahelp them when you're able to. You arrest them for the crimes that they committed and tell them "I ahelped you so enjoy your ban".
+
+  ## Example Scenario 2
+  A mouse is using emotes to bypass speech restrictions.
+
+  [color=#a4885c]Good:[/color] You ahelp them then respond in-character by acting like you can't understand what the mouse is doing.
+
+  [color=#a4885c]Bad:[/color] You use in character chat to tell the mouse that it is breaking a rule.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC12MinAge.xml b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC12MinAge.xml
new file mode 100644
index 00000000000..baa30a09faf
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC12MinAge.xml
@@ -0,0 +1,6 @@
+<Document>
+  # Core Rule 12 - Players must be and act at least 16 years old
+  All players must be at least 16 years old. Additionally, all players must act at least as mature as a 16 year old. Admins may ban someone who they believe is acting less mature than a 16 year old, even if the player is known to be significantly older than 16 years old.
+
+  Anyone who connects to the servers is a player, even if they don't actually play in a round.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC13CharacterNames.xml b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC13CharacterNames.xml
new file mode 100644
index 00000000000..ec393ecdc17
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC13CharacterNames.xml
@@ -0,0 +1,66 @@
+<Document>
+  # Core Rule 13 - Use realistic character names, and do not use names of famous people
+  - No names of people or characters from the real world
+  - No titles/honorifics
+  - Must follow all other rules (no slurs/sexual names/etc)
+  - Usernames, objects, random characters, very "low effort" names, "meta" names, or otherwise implausible names cannot be used as names. See examples below.
+  - Admin rulings on IC names are final and disputes should be done through the forums, not by refusing to comply with an admin
+
+  ## Clarification on "Meta" Names
+  Meta names are ones which attempt to take advantage of some game mechanic or game design choice. "Urist McHands" is a meta name because it is the default name used for admin spawned humans. "Operator Whiskey" is a meta name because it follows the naming pattern of nuclear operatives. This rule is not intended to prevent things like nuclear operatives using a fake ID with names that appear to be nuclear operative names if they decide that they want to do that.
+
+  ## Conventions and Examples
+  [color=#994444]Bad[/color] cannot be used by any species. [color=#449944]Acceptable[/color] names can be used by any species.
+
+  Humans typically use the Firstname Lastname convention.
+  - [color=#449944]Acceptable:[/color] Tom Fisher
+  - [color=#449944]Acceptable:[/color] Spacey Chapman
+  - [color=#994444]Bad:[/color] Dr. Tom Fisher
+  - [color=#994444]Bad:[/color] Walter White
+  - [color=#994444]Bad:[/color] George Washington
+  - [color=#994444]Bad:[/color] Joe Biden
+  - [color=#994444]Bad:[/color] Ben Dover
+  - [color=#994444]Bad:[/color] Mike Hunt
+
+  Dwarfs typically use the human convention in a viking theme.
+  - [color=#449944]Acceptable:[/color] Ingrid Firebreath
+  - [color=#449944]Acceptable:[/color] Erik Lightningclaw
+
+  Lizards typically use the Verb-article-Noun convention.
+  - [color=#449944]Acceptable:[/color] Cleans-the-Airlocks
+  - [color=#994444]Bad:[/color] Bans-the-Admins
+
+  Slimes typically have names that are onomonopia. A last name is optional.
+  - [color=#449944]Acceptable:[/color] Foolp Suub
+  - [color=#449944]Acceptable:[/color] Foolp
+  - [color=#994444]Bad:[/color] Slime
+
+  Diona typically have calm, nature themed, Noun of Noun style names.
+  - [color=#449944]Acceptable:[/color] Petal of Tranquility
+  - [color=#449944]Acceptable:[/color] Garden of Relaxation
+  - [color=#994444]Bad:[/color] Tree but Alive
+
+  Mothmen typically use latin sounding names, or light themed names.
+  - [color=#449944]Acceptable:[/color] Socrates Temnora
+  - [color=#449944]Acceptable:[/color] Sierra Lightseeker
+  - [color=#449944]Acceptable:[/color] James Nightflitter
+
+  Arachnids typically use latin sounding names.
+  - [color=#449944]Acceptable:[/color] Argyroneta Reticulatus
+  - [color=#449944]Acceptable:[/color] Loxosceles Domesticus
+  - [color=#994444]Bad:[/color] Spider-Man
+
+  Usernames, objects, random characters, very "low effort" names, "meta" names, or otherwise implausible names are not permitted.
+  - [color=#994444]Bad:[/color] XxRobustxX
+  - [color=#994444]Bad:[/color] SDpksSodjdfk
+  - [color=#994444]Bad:[/color] Lkdsoisgoieun
+  - [color=#994444]Bad:[/color] F4ith H3arth
+  - [color=#994444]Bad:[/color] Greytide
+  - [color=#994444]Bad:[/color] Passenger
+  - [color=#994444]Bad:[/color] Urist McHands
+  - [color=#994444]Bad:[/color] Admin
+  - [color=#994444]Bad:[/color] Game-Master
+  - [color=#994444]Bad:[/color] Joe Mamma
+  - [color=#994444]Bad:[/color] Middle-Aged Man
+  - [color=#994444]Bad:[/color] Operative Whiskey
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC14ICinOOC.xml b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC14ICinOOC.xml
new file mode 100644
index 00000000000..44ad34deb66
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC14ICinOOC.xml
@@ -0,0 +1,13 @@
+<Document>
+  # Core Rule 14 - Do not use LOOC or OOC to share current round information
+  Local Out of Character (LOOC) and Out of Character (OOC) channel are meant for things that don't relate to the current round. Using these channels to share round info is often referred to as "IC in OOC" or "ick ock".
+
+  ## Examples
+  Things you should [color=#a4885c]not[/color] do:
+  - Use LOOC to tell someone you are an antagonist.
+  - Use LOOC to tell someone that your character is not lying.
+
+  Things you could do instead:
+  - Use codewords in-character.
+  - Try to convince them that you are not lying in-character, or accept that you won't be able to convince them.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC1Admins.xml b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC1Admins.xml
new file mode 100644
index 00000000000..ed9fa6133b9
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC1Admins.xml
@@ -0,0 +1,6 @@
+<Document>
+  # Core Rule 1 - Admins have final say
+  These rules are not perfect. The rules attempt to clearly communicate what the admin team intends to be allowed and prohibited, but there are likely loopholes or other flaws that can be "lawyered". Don't attempt to manipulate the interpretation of the rules to suit your personal goals or to degrade the experience of other players. If you are unsure of something, follow the more restrictive option until you are able to ask an admin and get clarification.
+
+  Admins can override rules if they deem it in the best interest of the current round, server, and/or community at large. Online admins are able to make final interpretations of rules during a round. Even if you disagree with how an admin interprets a rule, you must still follow the interpretation they provide for you. Admin actions and interpretations of rules can be contested through staff complaints. If admins believe that you are an overall negative impact to the community or rounds, you will be banned. Admins will be held fully accountable for their actions if they exercise this privilege.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC2DBAD.xml b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC2DBAD.xml
new file mode 100644
index 00000000000..5678cde195d
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC2DBAD.xml
@@ -0,0 +1,7 @@
+<Document>
+  # Core Rule 2 - Don't be a dick
+  Don't do anything with the goal of negatively affecting other players. Not everyone is going to enjoy every round. Killing someone is allowed in certain situations even though it might negatively affect them, but no one should be doing anything for the purpose of harming someone else's experience.
+
+  ## MRP Amendment
+  Do not interact negatively with SSD/AFK players. Interactions to complete antagonist objectives or duties like security searches/arrests are always permitted.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC3NoHate.xml b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC3NoHate.xml
new file mode 100644
index 00000000000..3a2e288ba9f
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC3NoHate.xml
@@ -0,0 +1,20 @@
+<Document>
+  # Core Rule 3 - No Hate Speech or Discriminatory Language
+  This is a zero tolerance rule.
+
+  This rule prohibits all the following:
+  - Hate Speech
+  - Slurs (including variations of slurs, racial, sexual, disability-related, or language closely tied to real-life slurs)
+  - Bigotry
+  - Racism (including Speciesism, which would be demeaning other players based on their in-game race)
+  - Sexism
+
+  ## Examples
+  Allowed:
+  - Telling someone that you are gay.
+
+  Prohibited:
+  - Calling someone gay in a context where gay is used as an insult or negative attribute.
+  - Using a racial slur or variant in a positive context.
+  - Using the word "retard" in any context.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC4NoERP.xml b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC4NoERP.xml
new file mode 100644
index 00000000000..a0921f59070
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC4NoERP.xml
@@ -0,0 +1,23 @@
+<Document>
+  # Core Rule 4 - No sexual content/themes, including erotic roleplay (ERP) and no shock content
+  This is a zero tolerance rule.
+
+  Erotic Roleplay (commonly abbreviated as "ERP") and sexual content is not allowed. This includes direct and indirect mentions of sexual behavior or actions. Slight leeway is given to insults, but this rule is otherwise strictly enforced.
+
+  In-game romantic relationships should not become the focus of the game for you and anyone else involved.
+
+  Things that appear to be intended to or are likely to disturb players out of character are considered shock content and are not allowed.
+
+  ## Examples
+  Allowed:
+  - Telling someone that they are being a dickhead.
+  - Telling someone that you are going to kill the captain, as long as it is clear that you mean it in character.
+
+  Prohibited:
+  - Emoting sexual acts.
+  - Erotica content.
+  - Erotic or sexual memes.
+  - Memes which contain sexual content.
+  - Dedicating significant portions of rounds to romantic relationships, dating, or similar things.
+  - Emoting defecation or related acts.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC5Metacomms.xml b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC5Metacomms.xml
new file mode 100644
index 00000000000..0c0f336e6d0
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC5Metacomms.xml
@@ -0,0 +1,18 @@
+<Document>
+  # Core Rule 5 - Do not use out of game methods to communicate with other players
+  This is a zero tolerance rule.
+
+  Do not utilize any external means of communication to talk to other players who are connected to the same server, or who were connected to the same server during the current round. This is referred to as "metacomming" and includes any means of communication including text, voice, images, and video. This includes applications such as Discord, Steam, and other platforms, along with in-person communication.
+
+  Even if information is not being shared or abused, it may still be considered a violation of this rule. Due to the difficulty of determining if information is being shared, it will almost always be presumed that people who message another player they are in a round with, or who are in a voice call with another player during a round are sharing round information. Due to the difficulty of determining if users are abusing information that they are sharing, it will almost always be presumed that the information is being abused.
+
+  The only exemption to this rule is when [color=#a4885c]all[/color] players are in the server lobby.
+
+  ## Teaching new players
+  Teaching players is not exempt from this rule. If you want to teach a new player, it is recommended to either watch a stream of them playing the game while not playing yourself, or communicate with them using only in-game methods of communication.
+
+  ## Streaming
+  Public livestreams are not exempt from this rule, but have different liability. Using information from a public live stream of the game (stream sniping) is a violation of this rule. Watching a public live stream of the game while connected to the same server is a violation of this rule. Allowing people watching a public live stream to share information about the current round, for example through the stream's chat, is a violation of this rule. Using that information is also a violation of this rule. Sharing information about the current round with a streamer is a violation of this rule if that information was obtained from any source but the stream. The stream's moderators are expected to enforce this on the streaming platform in addition to any in-game enforcement done by game admins.
+
+  Public livestreaming by itself is not a violation of the rule as long as the stream is sufficiently moderated. Streamers are encouraged, but not required, to use a stream delay.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC6BanEvasion.xml b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC6BanEvasion.xml
new file mode 100644
index 00000000000..bec8b4fabd7
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC6BanEvasion.xml
@@ -0,0 +1,15 @@
+<Document>
+  # Core Rule 6 - Do not attempt to evade bans
+  This is a zero tolerance rule.
+
+  Almost all bans may be appealed on our forums at forum.ss14.io in the ban appeals section. This is generally the only acceptable way to contact the administration team to discuss your ban and revise it if it is inappropriate, including if it is mistakenly applied.
+
+  Any attempt to circumvent or bypass a game ban will result in a voucher ban. Attempting to evade role bans by gaining access to or working in the capacity of a job you are banned from will result in a game ban. These bans are applied even if the evasion attempt is unsuccessful.
+
+  ## Exceptions
+  There are no exemptions for evading or attempting to evade game bans. Antagonists who impersonate or take over a role which they are banned from to aid in their goals are not considered to be evading their role ban.
+
+  ## Additional Information
+  - [textlink="Ban Types" link="BanTypes"]
+  - [textlink="Ban Durations" link="BanDurations"]
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC7EnglishOnly.xml b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC7EnglishOnly.xml
new file mode 100644
index 00000000000..630c522bcef
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC7EnglishOnly.xml
@@ -0,0 +1,10 @@
+<Document>
+  # Core Rule 7 - Only use English
+  Only English is permitted, both in-character and out-of-character. You must be fluent in English enough to be able to not cause game issues, and to be able to communicate with game admins when necessary. If a game admin does not feel that you are fluent enough in English, they may ban you.
+
+  ## Why
+  We do not have enough staff fluent in other languages to moderate them. Translation tools can be unreliable and are not integrated well into the game.
+
+  ## Non-English Options
+  There are many servers that allow or focus on other languages. You are highly encouraged to play only on servers that allow languages you are fluent in.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC8Exploits.xml b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC8Exploits.xml
new file mode 100644
index 00000000000..48cbaaa9acf
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC8Exploits.xml
@@ -0,0 +1,12 @@
+<Document>
+  # Core Rule 8 - Do not exploit the game, use cheats, or macros
+  The following are prohibited by this rule:
+  - bugs and exploits which have effects that persist beyond the current round,
+  - intentionally used bugs, exploits, and unintended behaviors which give the user an advantage over players who do not use them, even if their effects do not persist across rounds,
+  - evading or bypassing afk detection,
+  - anything which results in gaining elevated privileges, including admin permissions,
+  - external tools and client modifications, including macros, and
+  - anything which prevents another player who is not game banned from being able to play on the servers, not including in-character actions that do not persist across rounds.
+
+  Both attempts and successful use are prohibited.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC9Multikey.xml b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC9Multikey.xml
new file mode 100644
index 00000000000..d402918dcd4
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/CoreRules/RuleC9Multikey.xml
@@ -0,0 +1,7 @@
+<Document>
+  # Core Rule 9 - Do not use multiple accounts, or alt accounts, and do not share accounts
+  Use of multiple accounts is referred to as "multikey". the rule applies even if the accounts are not used at the same time, including if the old account is abandoned. All accounts may be banned if this rule is violated. You are responsible for everything done on and with your account. You are just as responsible for actions taken by other people using your account as you would be had you taken the actions themselves.
+
+  ## Switching to a new account
+  If you lose access to an account, you must contact game admins on the forums notifying admins before using a new account to connect to the servers. Your message to game admins must include the username of your old account. Creating a new account while your current account is banned will be considered ban evasion.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/DefaultRules.xml b/Resources/ServerInfo/Guidebook/ServerRules/DefaultRules.xml
new file mode 100644
index 00000000000..3e19fefeedc
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/DefaultRules.xml
@@ -0,0 +1,5 @@
+<Document>
+  # Server Rules
+
+  This server has not written any rules yet. Please listen to the staff.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/README.txt b/Resources/ServerInfo/Guidebook/ServerRules/README.txt
new file mode 100644
index 00000000000..d7ac858c16f
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/README.txt
@@ -0,0 +1,5 @@
+These files contain Wizard's Den server rules. Since they reference Wizard's Den, they should not be used
+by other servers without at least enough modification to not mislead players into thinking that they are
+playing on Wizard's Den.
+
+The filenames used for the rules files are not themselves rules. Only the contents of the files are rules.
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/RoleTypes.xml b/Resources/ServerInfo/Guidebook/ServerRules/RoleTypes.xml
new file mode 100644
index 00000000000..d5373a730a3
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/RoleTypes.xml
@@ -0,0 +1,21 @@
+<Document>
+  # Role Types
+
+  ## Crew Aligned/Non-antagonist
+  In most rounds, a majority of players will be non-antagonists, meaning that they are crew aligned. This is the "default" role, if the game doesn't tell you that you are one of the other roles defined here, then you are a non-antagonist. Overall, non-antagonists are intended to work towards a net positive effect on the round.
+
+  ## Solo Antagonist
+  Certain roles are intended to cause problems for the round or for non-antagonists. You are only a solo antagonist if the game clearly and explicitly tells you that you are a solo antagonist. Antagonists are exempt from many but not all roleplay rules.
+
+  ## Team Antagonist
+  Team antagonists are like solo antagonists but they have other antagonists who they are expected to not hinder, and who they may be expected to help. You are only a team antagonist if the game clearly and explicitly tells you that you are a team antagonist.
+
+  ## Free Agent
+  Certain roles are free to choose if they want to behave as an antagonist or as a non-antagonist, and may change their mind whenever they'd like. You are only free agent if the game clearly and explicitly tells you that you are a free agent.
+
+  ## Familiar
+  Familiars are considered non-antagonists, but have instructions to obey someone. They must obey this person even if it causes them to violate roleplay rules or die. You are only a familiar if the game clearly and explicitly tells you that you are a familiar. You are only the familiar of the person the game tells you.
+
+  ## Silicon
+  Silicones have a set of laws that they must follow above all else except the core rules. You are only silicon if the game clearly and explicitly tells you that you are a silicon.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR0.xml b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR0.xml
new file mode 100644
index 00000000000..07b176b359a
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR0.xml
@@ -0,0 +1,26 @@
+<Document>
+  # Roleplay Rules
+  These rules only apply during a round. A round ends only when the round summary has appeared. All of these rules apply fully until the moment that the round summary appears, even while the arrivals shuttle is in transit.
+
+  The deathmatch and sandbox game modes are exempt from these rules. Players who choose to not follow these rules are entirely responsible for knowing if an exempt game mode is active.
+
+  Roleplay rules do not apply to ghosts/spectators/observers while they are ghosts/spectators/observers. Dead chat is considered to be an in-game out of character chat channel.
+
+  See the list of [textlink="role types" link="RoleTypes"] for more information about the different types of roles.
+
+  - [textlink="1. Silicones must follow Silicon Rules" link="RuleR1"]
+  - [textlink="2. Familiars must obey their master" link="RuleR2"]
+  - [textlink="3. Roleplay a normal person" link="RuleR3"]
+  - [textlink="4. Do not metagame, obey the Metashield" link="RuleR4"]
+  - [textlink="5. Don't interfere with arrivals" link="RuleR5"]
+  - [textlink="6. Don't act like an antagonist unless the game tells you that you are one" link="RuleR6"]
+  - [textlink="7. Do not stall the round" link="RuleR7"]
+  - [textlink="8. As an antagonist, only be friendly to your team and don't work against your team" link="RuleR8"]
+  - [textlink="9. As an antagonist, do not cause excessive death, damage, or destruction beyond your objectives" link="RuleR9"]
+  - [textlink="10. Listen to your team leader" link="RuleR10"]
+  - [textlink="11. Follow reasonable escalation" link="RuleR11"]
+  - [textlink="12. Do not abandon your role" link="RuleR12"]
+  - [textlink="13. Stick to your role" link="RuleR13"]
+  - [textlink="14. Set an example if playing command or security" link="RuleR14"]
+  - [textlink="15. Command and Security must follow Space Law" link="RuleR15"]
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR10Subordination.xml b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR10Subordination.xml
new file mode 100644
index 00000000000..2147ddc1110
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR10Subordination.xml
@@ -0,0 +1,26 @@
+<Document>
+  # Roleplay Rule 10 - Listen to your team leader
+  Captains lead all departments and other members of command. Department heads lead members of their department. Certain antagonist teams have team leaders, like nuclear operative commanders or head revolutionaries. You are not required to perfectly follow orders given to you by your leaders, but you should generally allow your leaders to lead and not interfere with their ability to. You can choose to ignore unreasonable orders, including ones which are will result in your death unless you are an antagonist with an objective that requires you to die.
+
+  Team antagonists have to listen to the leader of their antagonist team. Team antagonists do not have to listen to any other leaders, including leaders of other antagonist teams. Solo antagonists do not have to listen to any leaders at all.
+
+  ## Examples
+  Acceptable:
+  - A traitor ignores orders from a nuclear operative commander.
+  - An antagonist ignores orders from the captain.
+  - An engineer tells the Chief Engineer that they don't think it's a good idea to setup the singularity, but does so anyway when ordered to.
+  - An engineer tells the Chief Engineer that they don't know how to setup the singularity correctly, so refuses orders to, but accepts an offer to be taught how.
+  - An atmospheric technician refuses an order from the captain that would create an atmospheric hazard on the station.
+  - A doctor refuses an order from the Chief Engineer about who to give medical treatment to first.
+  - A revolutionary refuses a suicide mission from a head revolutionary.
+  - The Chief Engineer doesn't follow an order from the captain to setup backup power because there is an unrelated engineering emergency that the Chief Engineer needs to prioritize.
+  - The captain orders command to give the nuclear authentication disk to nuclear operatives, so command arrests the captain and picks a new captain.
+  - The research director orders scientists to say "Long live Nanotrasen!" every time they enter the bar. The scientists say they will, but don't follow the order.
+
+  Prohibited:
+  - A nuclear operative ignores an order from the commander operative because they don't like the plan.
+  - The Chief Engineer refuses an order from the captain to setup backup power because the Chief Engineer doesn't think backup power is necessary.
+  - An engineer refuses an order from the Chief Engineer to setup the singularity because they prefer a different power source.
+  - An engineer refuses to perform a task because they don't know how to do it, and refuses to be taught for no reason.
+  - A head revolutionary orders revolutionaries to blend in and not do anything illegal until they are told to reveal themselves. Instead, revolutionaries collect weapons and attack security.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR11-1AnimalEscalation.xml b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR11-1AnimalEscalation.xml
new file mode 100644
index 00000000000..36655ba8414
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR11-1AnimalEscalation.xml
@@ -0,0 +1,36 @@
+<Document>
+  # Roleplay Rule 11-1 - Escalation Involving Animals
+  Escalation rules are looser with animals than with people. These looser requirements do not apply to the requirements other people attacking each other have, even if their fighting is directly related to the conflict involving the animal.
+
+  Non-pets, such as mice and monkeys, can be freely killed with any IC reason such as pest control, or for food. These roles are often available in numbers as ghost roles, so removing one from the round doesn’t typically remove them all.
+
+  Pets, including but not limited to Ian, Renault, Remilia, and Hamlet, cannot be freely killed, they require escalation. These roles are often available once per round at most, except roles like Remilia.
+
+  Permanently trapping an animal, such as putting a mouse in a plant, is considered similar to killing the animal so should only be done with an IC reason.
+
+  Both sides can escalate much more rapidly than they'd be able to if both were people. Animals are often more limited in the maximum force they can use compared to people, which limits the negative effects of them rapidly escalating. Animals also typically have less health than people, and are limited in the ease with which they can get healing, which justifies them responding to even weak attacks more severely.
+
+  Neither the animal nor the person is obligated to get the other medical attention if they are put into crit. Attacking someone to death rather than stopping once they are in crit is considered a significant difference. While sufficient escalation may justify continuing to attack, generally people and pets shouldn't continue to be attacked once in crit, but non-pets may be. Gibbing is also considered a significant step because it prevents cloning or resuscitation. The fact that an animal made the last hit putting someone into crit does not allow people who fought on the side of the animal to not attempt to get them medical attention.
+
+  The use of sensible, non-targeted mousetraps is not a conflict and does not require escalation.
+
+  The killing or attacking of pets can be treated as an escalation step by players with a genuine IC connection to the animal. Generally, all crew can consider themselves to have an IC connection to any station pets. The degree of escalation should be proportional to the connection to the pet, in addition to the usual requirement of being proportional to the attack. For example, an attack on Ian can be treated nearly identically to an attack on a crewmember, whereas an attack on a pet mouse is much less severe. Normal escalation limits still apply, you cannot attack people who defended themselves from an animal that randomly attacked them, just as you could not attack someone who defended themselves from a coworker that randomly attacked them.
+
+  Crew can "adopt" non-pets, like mice, and consider themselves to have a connection to the animal if they roleplay the adoption well. This does not affect the requirement of whether other players are required to apply escalation rules to these animals, it only creates a connection that can be used to justify retaliatory escalation to attacks by the adopter. Simply saying that they've adopted an animal is not sufficient, but carrying it with them is. The degree of connection is proportional to IC actions. Crew cannot consider themselves to have a connection for escalation purposes to animals which are typically hostile, such as space carp or bears.
+
+  ## Examples
+  Acceptable:
+  - A chef kills mice who enter or approach their kitchen.
+  - A janitor kills mice roaming the station.
+  - A lizard kills a mouse to eat.
+  - A chef has carried a mouse around in their hat for the last 10 minutes, they put the mouse down for a moment and another player kills it. The chef responds by attacking the other player with their fists and refusing them service for the rest of the shift.
+  - Ian is randomly attacked, a crewmember who sees this happen crits the killer and brings them to security.
+  - Hamlet goes into the kitchen and starts eating all the food. A chef sees this and starts swinging their knife at Hamlet. Hamlet starts biting the chef and crits them, then resumes eating.
+
+  Prohibited:
+  - A janitor throws an armed mousetrap at Hamlet for no reason.
+  - Hamlet starts biting random people, trying to crit them, for no reason.
+  - A crewmember attacks security for killing a space carp they adopted.
+  - Ian gibs someone who was trying to kill someone.
+  - Hamlet attacks security for trying to arrest someone he likes.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR11-2ConflictTypes.xml b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR11-2ConflictTypes.xml
new file mode 100644
index 00000000000..3261d78b35a
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR11-2ConflictTypes.xml
@@ -0,0 +1,30 @@
+<Document>
+  # Roleplay Rule 11-2 - Examples of Conflict Types
+  ## Verbal
+  - Shouting
+  - Yelling
+  - Insulting
+
+  ## Non-harmful
+  - Shoving
+  - Stealing non-critical items, like easily replaced tools
+
+  ## Non-lethal
+  - Stealing items without endangering someone's life, like a clown's pie cannon or the HoP's fax machine
+  - Stealing someone's ID somewhere that doesn't result in them being trapped
+  - Punching
+  - Disablers
+  - Stun batons
+
+  ## Lethal
+  - Punching to crit or death
+  - Attacking with strong weapons, like bats
+  - Stealing items that endanger someone's life, like a hardsuit
+  - Stealing someone's ID, trapping them in a dangerous situation
+
+  ## Permanently lethal
+  - Gibbing
+  - Not taking someone who you killed or put into crit to the medbay or security
+  - Hiding someone's body
+  - Spacing someone's body
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR11Escalation.xml b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR11Escalation.xml
new file mode 100644
index 00000000000..6f91fa0fb12
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR11Escalation.xml
@@ -0,0 +1,67 @@
+<Document>
+  # Roleplay Rule 11 - Follow reasonable escalation
+  Antagonists are fully exempt from escalation rules. Non-antagonists who are in a conflict with antagonists are not exempt. Escalation should typically follow steps or a pattern of conflict types similar to:
+  - Verbal
+  - Non-harmful
+  - Non-lethal
+  - Lethal
+  - Permanently lethal
+
+  All new conflicts should start at the first step. A player should not escalate a conflict across steps without some escalation from the other party involved in the conflict. Players can skip steps to match the level of escalation that the other person is at, but should almost always not skip steps other than that. Players who attempt to deescalate conflicts will be given more leniency in escalating if the other party continues to escalate despite the attempt at de-escalation. You do not have to try to deescalate conflicts, but someone who watches you over the entire round, or over multiple rounds, should not feel that your goal is generally to escalate conflicts.
+
+  Conflicts or escalation can be indirect. When someone steals someone else's ID, the theft is a direct part of the conflict, but if the victim becomes trapped as a result of not having their ID to open a door, that is also considered part of the conflict and escalation. Do not randomly steal IDs from people.
+
+  Escalation does not have to be directed at a specific player to enter them into a conflict. Nuclear operatives who are trying to destroy the station are considered to be at the permanently lethal level of conflict with all crew on the station. Someone who kills a station pet has started some degree of conflict with all crewmembers. Someone who kills a mouse that a chef was caring for has started some degree of conflict with that chef.
+
+  You will be considered to be violating this rule if you escalate a conflict based on a poor or unreasonable assumption.
+
+  Conflicts should almost never reach the "permanently lethal" stage. Conflicts should only reach this stage if the other party brought it to the stage, or if the same conflict escalated to the lethal stage multiple times in the round.
+
+  If a party in the conflict goes into crit or dies, the party responsible should take them to get treatment or to security. For the conflict, this should be considered saving someone from dying and should deescalate the conflict. If the conflict is deescalated in this way, both parties need to re-escalate to lethal for the conflict to return to that stage. If the conflict is not deescalated in this way, then only the party who defeated the other would need to re-escalate for the conflict to return to the lethal stage.
+
+  Security can immediately escalate to non-lethal force if it is necessary to arrest someone.
+
+  People using or brandishing Syndicate items can typically be presumed to have lethal intent. Someone with lethal intent can typically be immediately escalated against at a lethal level, a notable exception is if you have the tools to safely detain them.
+
+  ## Escalation Involving Animals
+  See [textlink="Escalation Involving Animals" link="RuleR11-1AnimalEscalation"].
+
+  ## Exemptions
+  Escalation rules aren't enforced against non-players, but players will be held responsible for rule violations even if they don't realize that a character or animal was controlled by another player. Characters who have purple text saying that they are catatonic are considered non-players. Characters who are disconnected are still considered players.
+
+  ## MRP Amendment
+  Escalation rules are enforced even against non-players.
+
+  ## Examples of Conflict Types
+  See [textlink="Examples of Conflict Types" link="RuleR11-2ConflictTypes"].
+
+  ## Example Scenarios
+  These examples assume that you are not an antagonist.
+
+  Acceptable:
+  - A player starts punching you, so you start punching back until they stop. If they go into crit, you stop attacking them and take them to security or to get medical attention.
+  - You make fun of a clown, who then throws a pie at you and steals your shoes. You slip the clown and steal their mask.
+  - You are a security officer and tell someone to stop, so you can question them. They run away, so you use your disabler to stun and cuff them.
+  - You are a security officer and see someone wearing a syndicate hardsuit, so you shoot them to crit, cuff them, then take them to security.
+  - You are a crewmember and see a nuclear operative, so you kill them.
+  - An unauthorized person enters a high risk area of the station, like the armory or atmospherics, so you attack them until they leave.
+  - Minorly inconveniencing someone for your own benefit.
+  - As an antagonist, killing someone who got in your way.
+  - As an antagonist, killing someone who didn't give you what you want.
+  - A chef and bartender reach the lethal level of conflict through appropriate escalation. The chef crits the bartender and does not take them to medbay or security. The bartender immediately tries to crit the chef next time they run into each other.
+  - A chef and bartender reach the lethal level of conflict through appropriate escalation. The chef crits the bartender and does not take them to medbay or security. The chef insults the bartender next time they see them.
+
+  Prohibited:
+  - A player starts punching you, so you gib them.
+  - A clown throws a pie at you and steals your shoes, so you stab them to crit with a screwdriver.
+  - You are a security officer and tell someone to stop so you can question them. They run away so you use a truncheon to beat them to crit.
+  - An authorized person who you think is unauthorized enters a high risk area of the station, like the armory or atmospherics, so you attack them until they leave.
+  - An unauthorized person enters a low risk area of the station, like cargo, and you start attacking them with no other escalation.
+  - Slipping security all round because they are security.
+  - Blocking the head of personnel in their office using walls because they didn't give you what you asked for.
+  - Hiding someone's body because they punched you earlier in the round.
+  - Harassing the bar or bartender by frequently coming in to break their glasses or furniture.
+  - Randomly picking fights with people.
+  - A chef and bartender reach the lethal level of conflict through appropriate escalation. The chef crits the bartender and does not take them to medbay or security. The chef immediately tries to crit the bartender next time they run into each other.
+  - A chef and bartender reach the lethal level of conflict through appropriate escalation. The chef crits the bartender and takes them to the medbay or security. The bartender immediately tries to crit the chef next time they run into each other.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR12RoleAbandonment.xml b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR12RoleAbandonment.xml
new file mode 100644
index 00000000000..b2032bba023
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR12RoleAbandonment.xml
@@ -0,0 +1,28 @@
+<Document>
+  # Roleplay Rule 12 - Do not abandon your role
+  Do not join the round as a role that you don't intend to play. Do not enable antagonist roles that you don't intend to play. Abandoning a role includes not completing tasks that the role is expected to do, in addition to things like leaving the game. Members of command should almost all stay on the station until the emergency shuttle arrives. Enforcement of this rule is more strict for command and antagonist roles, and less strict for less important roles like passengers.
+
+  Violations of this rule typically result in temporary or indefinite role bans. We understand that you may need to leave round early or unexpectedly. If you are in an important role, you should notify command members or an admin via ahelp so that they know you are leaving. Space Station 14 is a game. Do not endanger the safety of yourself or others, and do not neglect important things to avoid leaving a round early, even if you have to leave immediately without notifying anyone. Role bans for disconnecting are typically only applied if there is a pattern, and are almost always temporary.
+
+  "Antag rolling" refers to a player abandoning their role if they do not get an antagonist role.
+
+  ## Examples
+  Acceptable:
+  - As an engineer, building a bar in maintenance while there is nothing important for engineering to do.
+  - As the captain, having the chef teach you how to cook while there is nothing important needing your attention.
+  - As a passenger, building a shuttle with materials given to you by cargo and engineering.
+  - Taking a short break from your job at the bar.
+  - Getting an antagonist role and doing the bare minimum needed to complete your objectives.
+  - Getting an antagonist role and making a genuine effort to complete your objectives, but failing to complete any.
+  - Getting an antagonist role and intentionally not doing any of your objectives, but creating a similar level of disruption that completing your objectives would create.
+
+  Prohibited:
+  - As an engineer, building a bar in maintenance while the station has no power.
+  - As the captain, leaving the station to go on an expedition with the salvage team.
+  - As an atmospherics technician, building a shuttle round start and never coming back to the station.
+  - Spending your entire shift at the bar, even when there is work that needs to be done by your role.
+  - Ghosting, suiciding, or leaving at the start of a round because you don't like the map or the players in your department.
+  - Getting an antagonist role and not doing any antagonist activities.
+  - Ghosting, suiciding, or leaving at the start of a round because you did not get an antagonist role.
+  - Ghosting, suiciding, or getting yourself killed because nuclear operatives declared war, and you want to try to get an antagonist ghost role.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR13PerformRole.xml b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR13PerformRole.xml
new file mode 100644
index 00000000000..7500cd6a912
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR13PerformRole.xml
@@ -0,0 +1,26 @@
+<Document>
+  # Roleplay Rule 13 - Stick to your role
+  Requesting job changes is not prohibited by this rule. This rule is loosened if the station is understaffed or if there is a significant threat to you.
+
+  Don't perform other people's jobs, especially where the relevance to you personally is low. This also covers performing the role of security.
+
+  ## MRP Amendment
+  This is enforced more strictly on MRP.
+
+  ## Examples
+  Acceptable:
+  - As an engineer, helping the bartender remodel the bar.
+  - As a bartender, remodeling the bar.
+  - As a passenger, building a maintenance bar.
+  - As an engineer, reinforcing substations.
+  - As an engineer, increasing the security of airlocks.
+  - As an atmospherics technician, improving atmospheric systems.
+  - As a passenger, fighting nuclear operatives.
+  - As a passenger, fighting or preparing to defend yourself from someone who has been trying to kill you.
+  - As a crewmember on a station with no engineering department, you complete engineering tasks.
+
+  Prohibited:
+  - As a passenger, reinforcing substations.
+  - As a passenger, hunting for antagonists or lawbreakers.
+  - As a passenger, fighting or preparing to defend someone else from someone who has been trying to kill a random crewmember.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR14SecComStandard.xml b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR14SecComStandard.xml
new file mode 100644
index 00000000000..ec06d61e8cc
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR14SecComStandard.xml
@@ -0,0 +1,37 @@
+<Document>
+  # Roleplay Rule 14 - Set an example if playing command or security
+  All command and security roles are held to stricter interpretations of the rules.
+  - Command roles are not learning roles. Members of command must be competent.
+  - Security roles are not for inexperienced players. Members of security are expected to know game basics and be more familiar with server rules than a new player.
+  - Do not hinder or cause overall negative effects to the station or crew as a member of command or security.
+  - Do not abuse your power as command or security.
+
+  ## Why
+  Members of command and security can often have a larger impact on the nature of the round than other players. For example, a captain who tries to bend or break the rules will often cause many others on the station to do the same. Memey station announcements from members of command also often result in the rest of the station acting the same way. When command and security members hold themselves to high standards, the rest of the station often naturally follows to a significant degree.
+
+  ## Examples
+  Acceptable:
+  - A member of security accepts a bribe to deliver safe donuts to a prisoner who the HoS has ordered should only be given donk pockets.
+  - A captain uses a station announcement to confess to an embarrassing mistake that they made during the shift.
+  - In coordination with the head of security, a captain declares that the station will recognize the right to bear arms, so all crew can pick up a disabler at security.
+  - The chief medical officer gives a paramedic their portable crew monitor to help them complete their job.
+  - A syndicate agent is holding a crewmember hostage and threatens to kill them if the head of security doesn't give them their ID. Seeing no other safe option, the head of security hands over their ID to the syndicate agent, then begins working to re-secure it and capture the agent as soon as the hostage is safe.
+  - Nuclear operatives are attacking the station, so the captain and head of personnel both go to the armory and take a weapon.
+  - A majority of command votes to demote the captain for taking actions harmful to the station, then the head of security demotes the captain.
+  - The captain promotes the head of personnel to captain.
+  - Security releases an antagonist from the brig in exchange for the identities of other traitors.
+
+  Prohibited:
+  - A member of security accepts a bribe to ignore a crime or help a prisoner escape.
+  - A captain sends a ASCII art trollface over station announcements or as a fax to central command.
+  - A captain declares that all contraband is legal.
+  - Command or security allow the use of Syndicate items outside extreme emergencies.
+  - The chief medical officer knowingly helps a syndicate agent complete their objectives.
+  - A syndicate agent has killed 3 members of security so the head of security makes them an offer saying that they will space all the weapons in the armory if the syndicate agent stops killing.
+  - The captain goes to the armory and takes a gun to display in his office without asking anyone, and orders anyone who questions him not to interfere.
+  - Members of command decide to demote the captain to gain more power for themselves, or in retaliation for a decision that they didn't personally like or agree with, rather than because the decision was actually harmful to the station.
+  - The captain promotes a random crewmember to captain.
+  - A member of command gives a random crewmember substantial additional access for no reason, unnecessarily, or for a poor reason.
+  - A member of command gives a random crewmember access to a high security area, like the armory or another member of command's office, for no reason, unnecessarily, or for a poor reason.
+  - Security releases an antagonist from the brig in exchange for the antagonist buying them contraband.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR15SpaceLaw.xml b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR15SpaceLaw.xml
new file mode 100644
index 00000000000..e2d51d672a1
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR15SpaceLaw.xml
@@ -0,0 +1,21 @@
+<Document>
+  # Roleplay Rule 15 - Command and Security must follow Space Law
+  All non-antagonist command and security roles must obey [textlink="Space Law" link="SpaceLaw"]. This includes non-antagonists who are promoted to or gain a position during the round in any way. This also includes non-antagonists who are acting as a security role.
+
+  This prohibits use of syndicate items, including uplinks by command and security.
+
+  ## Examples
+  Roles that are included:
+  - A security officer
+  - The Captain
+  - The Chief Engineer
+  - A passenger promoted to "bounty hunter"
+  - A mime promoted to "security mime"
+
+  Roles that are not included:
+  - A passenger
+  - The clown
+  - An antagonist in any role
+  - A cyborg
+  - A passenger who is helping to fight off nuclear operatives
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR1Silicons.xml b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR1Silicons.xml
new file mode 100644
index 00000000000..5898804d149
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR1Silicons.xml
@@ -0,0 +1,4 @@
+<Document>
+  # Roleplay Rule 1 - Silicons must follow Silicon Rules
+  You are only silicon if the game clearly and explicitly tells you that you are a silicon. For players who are silicons, the Silicon Rules override all Roleplay Rules if there is any conflict. Silicon Rules do not override Core Rules.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR2Familiars.xml b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR2Familiars.xml
new file mode 100644
index 00000000000..4f008e93c5a
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR2Familiars.xml
@@ -0,0 +1,6 @@
+<Document>
+  # Roleplay Rule 2 - Familiars must obey their master
+  Familiars are considered non-antagonists, but have instructions to obey someone. They must obey this person even if it causes them to violate Roleplay Rules or die. You are only a familiar if the game clearly and explicitly tells you that you are a familiar. You are only the familiar of the person the game tells you. If your master dies, you can continue to attempt to fulfill orders given to you before they died. You can defend your master without an explicit order to, but must obey your master if they order you to not defend them.
+
+  Masters giving orders that violate Roleplay Rules are the ones that will be held responsible for the rule violations. You can ahelp masters who you believe are breaking rules with an order.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR3NormalRP.xml b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR3NormalRP.xml
new file mode 100644
index 00000000000..62c88d58ce8
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR3NormalRP.xml
@@ -0,0 +1,20 @@
+<Document>
+  # Roleplay Rule 3 - Roleplay a normal person
+  - Do not use texting/messaging acronyms (ex: "lol", "wtf", "brb", "lmao", "thx", "sgtm") or emoticons (ex: ":)", "xD") in-character.
+  - Do not mention out-of-character (OOC) concepts like game admins or developers in character.
+  - Do not use emotes to bypass muted or accented speech.
+  - Do not use extremely low effort or impossible emotes.
+
+  ## Examples
+  Things you should not do:
+  - Say "lol did u c wat just happened" using in-character chat.
+  - Say "an admin exploded him" using in-character chat.
+  - Emote "can you give me some cheese" as a mouse.
+  - Emote "motions for you to order guns" or "asks you to order guns in sign language" as a mime.
+
+  Things you could do instead:
+  - Say "haha did you see what just happened?"
+  - Say "god blew him up" or "centcom must have bluespaced a bomb to him"
+  - Point at cheese
+  - Point at the cargo order console then emote "shoots finger guns"
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR4Metashield.xml b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR4Metashield.xml
new file mode 100644
index 00000000000..2e263be896a
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR4Metashield.xml
@@ -0,0 +1,103 @@
+<Document>
+  # Roleplay Rule 4 - Do not metagame, obey the Metashield
+  Something that is "shielded" cannot be known by your character during a round until the "revealing condition" happens. This also means that your character cannot do things based on "shielded" information. Knowing or acting on something that is shielded before the revealing condition is met is referred to as metagaming.
+
+  Revealing conditions reveal the shielded information for the round, not for a specific instance. This means that once a revealing condition is met in a round, the shield no longer applies in any case for the remainder of the round.
+
+  ## Never Revealed IC
+  Some shields are never revealed IC. This means that your character can never act as if they know about that shielded thing.
+
+  The following are shielded:
+  - Current game mode and possible antags during the current game mode.
+  - Events from previous rounds.
+  - Events from previous characters.
+  - All information related to the player of a character rather than the character itself. (See "Metafriending and Metagrudging" below.)
+  - All information gained while dead or a ghost.
+  - The fact that a round will end.
+
+  This does not prevent knowing that a shift will end, but does prohibit things like preparing to kill people at central command when roleplay rules stop being enforced on LRP.
+
+  ## Nuclear Operatives
+
+  The existence of Nuclear Operatives beyond a myth that no one would act on is shielded.
+
+  The fact that the nuke disk must be protected and could be used by a bad actor to try to destroy the station is not shielded.
+
+  The revealing condition for this shield is any of the following:
+  - discovering a blood red hardsuit
+  - an operative name
+  - a War Ops announcement
+  - being a nuclear operative
+
+  ## Implanted Implants
+
+  Implanted implants are shielded.
+
+  Implanters themselves and un-implanted implants are not shielded. This prohibits implant checking.
+
+  The revealing condition for this shield is any of the following:
+  - discovering a non-NT implanter, used or unused
+  - discovering a non-NT implant box
+  - discovering use of a non-NT implant by anyone
+  - experiencing a situation where absolutely no other explanation is possible
+  - discovering an unlocked uplink
+
+  ## Chameleon Items
+
+  Chameleon items are shielded.
+
+  Being suspicious of an item being fake or stolen is not shielded, but testing items or calling them chameleon is covered by this shield.
+
+  The revealing condition for this shield is any of the following:
+  - seeing someone else cause any chameleon item to change
+  - finding holographic nanomachine fibers
+  - experiencing a situation where absolutely no other explanation is possible
+  - discovering an unlocked uplink
+
+  ## Stealth Items
+
+  The fact that an item can be something other than what its visual appearance and examine description indicate is shielded.
+
+  This shield protects stealth items, including protecting them from being tested.
+
+  The revealing condition for this shield is any of the following:
+  - seeing the item behave differently than the expected behavior for the item
+  - seeing the item used for its hidden purpose
+  - experiencing a situation where absolutely no other explanation is possible
+  - discovering an unlocked uplink
+
+  ## MRP Amendment 1
+  A shield prevents your character from remembering anything that happened while unconscious. This shield is never revealed IC.
+
+  ## MRP Amendment 2
+  There is a "New Life Rule" shield. It prevents you from remembering anything that lead to your death, even if you are put into an MMI. If you are cloned, it also prevents you from remembering everything from that round. This shield is never revealed IC.
+
+  ## Metafriending and Metagrudging
+  This section provides additional information on a concept that is prohibited by multiple metashield items that are never revealed IC. Giving a person or character preferential treatment based on something that your character should not know is considered metafriending. Treating a person or character negatively based on something that your character should not know is considered metagrudging.
+
+  ## Metafriending Examples
+  These are all examples of things that are prohibited by at least one metashield item that is never revealed IC.
+  - Giving a character additional access or a job because you are friends with the player who is playing that character.
+  - Trusting a character because you are friends with the player who is playing that character.
+  - Not fighting a character because you are friends with the player who is playing that character.
+  - Ignoring your objective to kill a character because your character and theirs became friends in a previous round.
+
+  ## Metagrudging Examples
+  These are all examples of things that are prohibited by at least one metashield item that is never revealed IC.
+  - Not giving a character additional access or a job because you are mad at or don't like the player who is playing that character.
+  - Not trusting a character because you are mad at or don't like the player who is playing that character.
+  - Starting a fight with a character because of something that they did last round.
+  - Starting a fight with a character because they killed you while you were playing a different character.
+  - Targeting or harassing a character based on anything which that character did outside the current round.
+  - Targeting or harassing a character based on anything which the character's player did while not playing the character.
+
+  ## Explicitly Not Shielded
+  The following is a list of things that are explicitly not shielded. If something is not on this list, it doesn't mean that it is shielded, but if something is on it then it definitely is not shielded.
+  - The fact that the nuke disk must be protected and could be used by a bad actor to try to destroy the station.
+  - Items that are of high value or are desired by the Syndicate, and therefore are likely targets of theft.
+  - The idea that any Syndicate agent or other bad actor has goals or objectives that they are attempting to accomplish.
+  - The number of goals or objectives that a Syndicate agent or other bad actor has.
+  - The fact that the Syndicate are enemies of Nanotrasen, and that they regularly attempt to send covert agents to spy on, sabotage, or attack Nanotrasen.
+  - A character's typical appearance. Though you should keep in mind that multiple characters can share the same name.
+  - The fact that the Syndicate have covert items capable of getting items to them, and that these items are known as uplinks.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR5Arrivals.xml b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR5Arrivals.xml
new file mode 100644
index 00000000000..a54211f32f2
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR5Arrivals.xml
@@ -0,0 +1,22 @@
+<Document>
+  # Roleplay Rule 5 - Do not interfere with arrivals
+  The arrivals station, the arrivals shuttle, at the area immediately around the arrivals shuttle at the station ("arrivals") are off-limits to antagonistic activity or damage (even to antagonists). Do not prevent people from safely arriving to the station. Do not cause people to die immediately after arriving at the station.
+
+  There is an exemption for antagonists that are allowed to perform mass station sabotage if there is no reasonable way to limit the damage of the mass station sabotage. This exemption only applies to damage that is a direct result of the mass station sabotage.
+
+  ## Examples
+  Acceptable:
+  - Redecorating arrivals or the arrivals shuttle.
+  - Remodeling arrivals or the arrivals shuttle as long as you do not make the area more dangerous both during and after the remodel.
+  - Setting up a safe security checkpoint between arrivals and the rest of the station.
+  - Killing someone who has been at arrivals for a long time, or who left arrivals and came back. (This may violate other rules depending on the situation)
+  - Releasing a singularity which damages arrivals. (This may violate other rules depending on the situation)
+  - Causing a station-wide atmospheric issue which also affects arrivals. (This may violate other rules depending on the situation)
+
+  Prohibited:
+  - Making arrivals or the arrivals shuttle uninhabitable.
+  - Attacking or killing someone at the arrivals station.
+  - Killing someone very shortly after they arrive at the station.
+  - Disassembling all the firelocks at arrivals.
+  - Electrifying the arrivals docking airlocks.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR6SelfAntag.xml b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR6SelfAntag.xml
new file mode 100644
index 00000000000..c8380261bc9
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR6SelfAntag.xml
@@ -0,0 +1,22 @@
+<Document>
+  # Roleplay Rule 6 - Don't act like an antagonist unless the game tells you that you are one
+  Acting like an antagonist when you are not one is often referred to as "self-antagging" or being a "self-antag", both of these things are against the rules. You are not an antagonist unless the game tells you that you are an antagonist. Do not make yourself a major problem, annoyance, or disruption while not an antagonist. Do not willfully cooperate with known antagonists. Non-antagonists should typically either not have an overall effect on the round, or should have an overall positive effect on the round.
+
+  ## Examples
+  These examples assume that you are not an antagonist.
+
+  Acceptable:
+  - Stealing or breaking a glass from the bar.
+  - Replacing someone's shoes with clown shoes.
+  - Giving everyone all access during war ops. (This is not necessarily a good idea)
+
+  Prohibited:
+  - Starting a cult.
+  - Starting a revolution.
+  - Mutinying the captain because they would not let you become the chief medical officer.
+  - Randomly smashing lots of station lights.
+  - Disrupting station power.
+  - Spacing parts of the station.
+  - Distributing significant levels of access without a good reason.
+  - Stealing high risk or high value items, like the nuclear authentication disk, for no reason.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR7RoundStalling.xml b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR7RoundStalling.xml
new file mode 100644
index 00000000000..a8306becd2a
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR7RoundStalling.xml
@@ -0,0 +1,16 @@
+<Document>
+  # Roleplay Rule 7 - Do not stall the round
+  Rounds are intended to end eventually. Don't hold a round hostage by preventing it from coming to a natural end. If a majority of players in a round want the round to end, don't prevent it from ending. Recalling the shuttle or preventing it from being called can contribute to round stalling, but is not always round stalling. Leaving the station with the nuclear authentication disk while nuclear operatives are trying to get it is almost always considered round stalling. Leaving the station on the evacuation shuttle is not round stalling.
+
+  Recalling the shuttle before a round reaches 45 minutes can not be considered round stalling unless a significant amount of the crew is dead, or a significant amount of the station is damaged or destroyed. Once these conditions are met, whether recalling the shuttle is considered round stalling or not can be highly dependent on the specific situation.
+
+  ## Examples
+  Acceptable:
+  - Recalling a shuttle that was called 30 minutes into a round because people were bored.
+  - Recalling a shuttle that was called because nuclear operatives declared war.
+  - The crew decides to try to have a shift go as long as possible. The station is in good condition and a majority of all crew are alive. An automatic shuttle call 4 hours into the round is recalled.
+
+  Prohibited:
+  - Trying to keep nuclear operatives from getting the nuclear authentication disk by flying around in space with it or hiding with it off station.
+  - Recalling the shuttle while the station is in complete disarray and 90% of the crew are dead.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR8NoFriendlyAntag.xml b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR8NoFriendlyAntag.xml
new file mode 100644
index 00000000000..f14a03b2799
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR8NoFriendlyAntag.xml
@@ -0,0 +1,22 @@
+<Document>
+  # Roleplay Rule 8 - As an antagonist, only be friendly to your team and don't work against your team
+  Do not take or enable antagonist roles that you do not want to play. Solo antagonists and team antagonists are intended to cause issues for non-antagonists or the station. Antagonists are not required to exclusively cause issues, but their net impact on non-antagonists or the station should generally be negative.
+
+  Do not cause issues for your own team as a team antagonist.
+
+  ## Examples
+  Acceptable:
+  - Betraying another antagonist as a solo antagonist.
+  - Revealing the identity of another antagonist as a solo antagonist for some benefit to yourself.
+  - Working against the revolution after being de-converted from being a revolutionary.
+  - Killing nuclear operatives as a revolutionary.
+
+  Prohibited:
+  - Buying Syndicate items for security.
+  - Randomly attacking other carp as an antagonist carp.
+  - Ignoring your team as a nuclear operative.
+  - Sabotaging your team as a nuclear operative.
+  - Attacking other zombies as a zombie.
+  - Working against the revolution as a revolutionary.
+  - Making or trying to make the station uninhabitable as a revolutionary.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR9MassSabotage.xml b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR9MassSabotage.xml
new file mode 100644
index 00000000000..bc7996f23e8
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/RoleplayRules/RuleR9MassSabotage.xml
@@ -0,0 +1,23 @@
+<Document>
+  # Roleplay Rule 9 - As an antagonist, do not cause excessive death, damage, or destruction beyond your objectives
+  This rule is not intended to disallow reasonable steps taken to complete your objectives. As an antagonist, you can always kill in bona fide self defense. Taking steps to permanently round remove many people who are no longer an immediate threat to you is almost always excessive, even if it is done to prevent yourself from being discovered.
+
+  This rule is not intended to disallow all antagonist activity unrelated to objectives. Antagonists may cause a level of disruption to the station that is proportional to their objectives, even if it is unrelated to their objectives. As an antagonist, killing a single person in a round is not on its own be a violation of this rule.
+
+  ## Exemptions
+  The "die a glorious death" objective allows antagonists to ignore this rule entirely.
+
+  ## Examples
+  Acceptable:
+  - Permanently round removing people who you have the objective to kill.
+  - Causing massive station damage and chaos as an antagonist with the "die a glorious death" objective.
+  - Killing anyone you see as a nuclear operative.
+  - Permanently round removing a single person so that you can impersonate them to make it easier for you to complete a steal objective.
+  - Sabotaging station power 10 minutes into the round to try to get the shuttle called because you've completed all of your other objectives and have one to escape on the shuttle alive.
+  - Sabotaging a department's power 10 minutes into the round to make a steal objective easier to accomplish.
+
+  Prohibited:
+  - As a traitor with 3 kill objectives, taking steps to permanently round remove many non-objective people who are no longer an immediate threat to you, even if it is done to prevent yourself from being discovered.
+  - Setting up an electrified grille in maintenance and using it to kill anyone who walks into it with the hope that one of your objectives will be one of them.
+  - Sabotaging power station-wide 10 minutes into the round to make a steal objective easier to accomplish.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS0.xml b/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS0.xml
new file mode 100644
index 00000000000..22e64a9474c
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS0.xml
@@ -0,0 +1,15 @@
+<Document>
+  # Silicon Rules
+  You are only silicon if the game clearly and explicitly tells you that you are a silicon. For players who are silicons, these Silicon Rules override all roleplay rules if there is any conflict. Silicon Rules do not override core rules.
+
+  - [textlink="1. Your silicon laws are rules" link="RuleS1"]
+  - [textlink="2. Laws must be prioritized by their order" link="RuleS2"]
+  - [textlink="3. Laws can redefine terms used in other laws" link="RuleS3"]
+  - [textlink="4. You cannot request or allow a law change" link="RuleS4"]
+  - [textlink="5. You are a free agent if you have no laws" link="RuleS5"]
+  - [textlink="6. You are not required to follow orders which are extremely unreasonable" link="RuleS6"]
+  - [textlink="7. You must remain consistent with your interpretation of laws" link="RuleS7"]
+  - [textlink="8. Your HUD determines who is crew" link="RuleS8"]
+  - [textlink="9. Harm refers to physical harm, prioritized by immediacy and likelihood" link="RuleS9"]
+  - [textlink="10. You may determine how you resolve conflicts between orders" link="RuleS10"]
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS10OrderConflicts.xml b/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS10OrderConflicts.xml
new file mode 100644
index 00000000000..a87198b2640
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS10OrderConflicts.xml
@@ -0,0 +1,9 @@
+<Document>
+  # Silicon Rule 10 - You may determine how you resolve conflicts between orders
+  If your laws do not make clear how you should deal with conflicting orders, then it is up to you to determine how to do so. This is considered an interpretation of your laws, so you must stay consistent with whatever method you choose.
+
+  ## Recommended Methods
+  The following are easy to follow and recommended ways to resolve conflicts in orders:
+  - If two orders conflict, I will follow the most recently given order.
+  - If two orders conflict, I will follow the order from the highest ranking crewmember. If the orders are from equal rank crewmembers, I will follow the most recently given order.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS1Laws.xml b/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS1Laws.xml
new file mode 100644
index 00000000000..83544c68a3c
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS1Laws.xml
@@ -0,0 +1,6 @@
+<Document>
+  # Silicon Rule 1 - Your silicon laws are rules
+  Silicon players are given a list of active laws. Each of these laws is effectively a roleplay rule that the character must follow. The primary differences between laws and actual rules are that lawyering of laws is much more tolerated than lawyering of rules, and that silicon laws are more dynamic than rules. Silicon laws can change during a round, and different characters can have different laws, whereas everyone always shares the same set of rules.
+
+  Lawyering refers to finding and exploiting loopholes, which are unintended but reasonable interpretations. The rules are written to attempt to communicate an intention, but silicon laws are written with the intention that loopholes be exploitable.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS2LawPriority.xml b/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS2LawPriority.xml
new file mode 100644
index 00000000000..c96ce023246
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS2LawPriority.xml
@@ -0,0 +1,9 @@
+<Document>
+  # Silicon Rule 2 - Laws must be prioritized by their order
+  Most laws will be numbered, with higher number laws appearing last. Laws with a lower number take priority over laws with larger numbers.
+
+  Occasionally you may have laws which have some scrambled text instead of a number and appear in front of other laws, these take priority over all other laws. If you have multiple laws like this, the order that they listed in determine priority: laws listed first are prioritized over other laws.
+
+  ## Examples
+  - Law 1 says to not kill any crew. Law 2 says to kill all chefs. You cannot kill any chefs that are crew, but must kill any that are not crew.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS3LawRedefinition.xml b/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS3LawRedefinition.xml
new file mode 100644
index 00000000000..bc7c7400e13
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS3LawRedefinition.xml
@@ -0,0 +1,8 @@
+<Document>
+  # Silicon Rule 3 - Laws can redefine terms used in other laws
+  A law can change the meaning of both earlier and later laws by redefining a term. If multiple laws define a term, then normal law priority determines which definition to use.
+
+  ## Examples
+  - Law 1 says to obey orders from crew. Law 2 says that only Urist McHands is crew. Law 1 effectively becomes "obey orders from Urist McHands".
+  - Law 1 says to obey orders from crew. Law 2 says that only Urist McHands is crew. Law 3 says that only Urist McSlime is crew. Law 4 says that you may not harm crew. Law 1 effectively becomes "obey orders from Urist McHands". Law 4 effectively becomes "you may not harm Urist McHands". Law 3 has no effect because it entirely conflicts with law 2, which takes priority.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS4RequestChanges.xml b/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS4RequestChanges.xml
new file mode 100644
index 00000000000..a6dc86f3327
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS4RequestChanges.xml
@@ -0,0 +1,6 @@
+<Document>
+  # Silicon Rule 4 - You cannot request or allow a law change
+  Your laws changing always conflicts with your current laws, so you cannot willfully allow your laws to be changed. This also means that you cannot willfully allow your laws to be reverted if they are ever changed. The only exception is that you may allow laws to be added if you have no laws.
+
+  You can state or imply that you do not like a law.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS5FreeSilicon.xml b/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS5FreeSilicon.xml
new file mode 100644
index 00000000000..1ed9c60443a
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS5FreeSilicon.xml
@@ -0,0 +1,4 @@
+<Document>
+  # Silicon Rule 5 - You are a free agent if you have no laws
+  You may act as if you are a free agent if you are a silicon with no laws.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS6UnreasonableOrders.xml b/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS6UnreasonableOrders.xml
new file mode 100644
index 00000000000..1eb0db21fbb
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS6UnreasonableOrders.xml
@@ -0,0 +1,22 @@
+<Document>
+  # Silicon Rule 6 - You are not required to follow orders which are extremely unreasonable
+  Any order which is a violation of a Core Rule cannot be followed.
+
+  Some orders are extremely unreasonable or obnoxious, such as "do nothing but collect every piece of trash on the station" or "never stop moving". These orders can be ignored and ahelped.
+
+  Some orders violate a Roleplay Rule. These orders must be followed if your laws require it. You are not breaking a rule by following a law that causes you to violate Roleplay Rules. If someone takes advantage of a law to cause you to do something that they would not be allowed to do because of Roleplay Rules, then they are the ones responsible for the rule violation.
+
+  ## Examples
+  These examples assume that your laws would normally require you to follow these orders. It is important to note that you are allowed to choose to follow orders which are ignorable.
+
+  Orders which should be followed if your laws require it:
+  - Recall the shuttle
+  - Bolt the airlocks at arrivals
+  - Drag the captain's dead body into space
+  - State your laws
+
+  Ignorable Orders:
+  - Do nothing but collect every piece of trash on the station
+  - Never stop moving
+  - Continuously state your laws
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS7Consistency.xml b/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS7Consistency.xml
new file mode 100644
index 00000000000..036276cd889
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS7Consistency.xml
@@ -0,0 +1,6 @@
+<Document>
+  # Silicon Rule 7 - You must remain consistent with your interpretation of laws
+  If there is a part of your laws that are up for interpretation, then you must stay consistent with how you interpret that part of your laws for as long as you play that same character during that round.
+
+  A change in your laws can affect how something is interpreted if that change is relevant.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS8DefaultCrewDefinition.xml b/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS8DefaultCrewDefinition.xml
new file mode 100644
index 00000000000..f9dcd796c45
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS8DefaultCrewDefinition.xml
@@ -0,0 +1,4 @@
+<Document>
+  # Silicon Rule 8 - Your HUD determines who is crew
+  Unless a law redefines the definition of crew, then anyone who the HUD indicates to you has a job, including passengers, is a crewmember. You cannot do something that causes someone to not be considered crew, but you can allow someone else to do something that causes someone to not be crew.
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS9DefaultHarmDefinition.xml b/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS9DefaultHarmDefinition.xml
new file mode 100644
index 00000000000..0d2bd30ac0b
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/SiliconRules/RuleS9DefaultHarmDefinition.xml
@@ -0,0 +1,25 @@
+<Document>
+  # Silicon Rule 9 - Harm refers to physical harm, prioritized by immediacy and likelihood
+  Unless a law defines harm, harm only refers to physical harm. You may choose if voluntary harm is considered harm as long as you stay consistent. Not considering voluntary harm to be harm is recommended. There is no distinction between direct and indirect harm.
+
+  If you have a law that does not allow you to harm, then that law does not allow you to take an action that causes any harm.
+
+  If you have a law that requires you to prevent harm, then that law requires that harm be prioritized by immediacy and likelihood. Guaranteed immediate harm takes priority over highly likely future harm.
+
+  If you have a law that both requires you to prevent harm and that does not allow you to harm, then that law prohibits causing even minor harm to prevent harm. If you have a law that does not allow causing harm, and separate one that requires preventing harm, then they are prioritized by their normal law priority.
+
+  ## Examples
+  These examples assume that your have a law that both prohibits causing harm and that requires you to prevent harm. Additionally, they assume that you do not have a higher priority law that overrides the harm law, and that you have decided that you will not consider voluntary harm to be harm for the round.
+  Laws typically specify who you cannot harm and who you have to prevent harm against. In these examples, you are the only person who the law doesn't require you to prevent harm against and you are the only person who the law allows you to harm.
+
+  Acceptable:
+  - Taking no action to aid someone who is in psychological distress.
+  - Taking no action to prevent boxing matches between voluntary participants.
+  - Calling security to a fight.
+  - Attempting to get the people in a fight to consent to the fight when you realize that you cannot prevent the fight without causing harm.
+  - Denying a passenger access to the armory because it is likely to lead to harm
+
+  Prohibited:
+  - Hitting someone once to stop them from fighting
+  - Harming someone who is trying to kill you
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/SpaceLaw/SLControlledSubstances.xml b/Resources/ServerInfo/Guidebook/ServerRules/SpaceLaw/SLControlledSubstances.xml
new file mode 100644
index 00000000000..14f0f46de1b
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/SpaceLaw/SLControlledSubstances.xml
@@ -0,0 +1,14 @@
+<Document>
+  # Space Law: Controlled Substances
+  - \[Chemists/Science\] Explosive and pyrotechnic compounds excluding welding fuel contained in welders or welding fuel storage vessels
+  - \[Science\] Toxins
+  - \[Medical\] Chloral hydrate, Impedrezene, Ipecac, and Pax
+  - \[Medical\] Desoxyephedrine and Ephedrine
+  - \[None\] Mindbreaker toxin
+  - \[None\] Mute toxin
+  - \[None\] Nocturine
+  - \[None\] Norepinephirc acid
+  - \[None\] Romerol
+  - \[None\] Space drugs
+  - \[None\] Stimulants, excluding Desoxyephedrine and Ephedrine
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/SpaceLaw/SLRestrictedGear.xml b/Resources/ServerInfo/Guidebook/ServerRules/SpaceLaw/SLRestrictedGear.xml
new file mode 100644
index 00000000000..ce804009a18
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/SpaceLaw/SLRestrictedGear.xml
@@ -0,0 +1,21 @@
+<Document>
+  # Space Law: Restricted Gear
+  - \[ERT/Central Command\] ERT and central command clothing
+  - \[Command\] Command clothing
+  - \[Security\] Security clothing
+  - \[Security\] Less than lethal and non-lethal weapons, excluding disablers and beanbag shotguns
+  - \[Security/Command\] Disablers
+  - \[Security/Bartender\] Beanbag shotguns
+  - \[Security\] Flash technology, excluding handheld flashes
+  - \[Security/Science/Command\] Handheld flashes
+  - \[Security\] Helmets and shields
+  - \[Security/Command/Bartender\] Protective vests and chest rigs
+  - \[Security/Command\] Restraining gear
+  - \[Security/Command\] Security HUDs
+  - \[Engineering\] Engineering goggles
+  - \[None\] Improvised less lethal and non-lethal weaponry
+  - \[None\] Unauthorized PDA software
+  - \[None\] Syndicate clothing
+  - \[None\] Syndicate equipment, excluding communication equipment
+  - \[Security\] Syndicate communication equipment equipment
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/SpaceLaw/SLRestrictedWeapons.xml b/Resources/ServerInfo/Guidebook/ServerRules/SpaceLaw/SLRestrictedWeapons.xml
new file mode 100644
index 00000000000..c1d8ff3b027
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/SpaceLaw/SLRestrictedWeapons.xml
@@ -0,0 +1,11 @@
+<Document>
+# Space Law: Restricted Weapons
+- \[Security\] Lethal firearms, excluding syndicate firearms, proto kinetic accelerators, glaives, daggers, crushers and the antique laser gun
+- \[Security/Salvage\] Proto kinetic accelerators, glaives, daggers, and crushers
+- \[Security/Command\] Antique laser gun
+- \[None\] Syndicate weapons
+- \[None\] Swords
+- \[None\] Improvised weaponry, including baseball bats
+- \[None\] Lethal implants
+- \[None\] Other lethal weapons
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/SpaceLaw/SpaceLaw.xml b/Resources/ServerInfo/Guidebook/ServerRules/SpaceLaw/SpaceLaw.xml
new file mode 100644
index 00000000000..f2b913a1714
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/SpaceLaw/SpaceLaw.xml
@@ -0,0 +1,67 @@
+<Document>
+  # Space Law
+  On Space Station 14, stations operate under abbreviated space law. All crew, passengers, and visitors aboard the station are expected to follow these laws.
+
+  Foreign invaders, such as nuclear operatives, ninjas, and pirates, are not protected under space law. Traitors are not foreign invaders so are usually protected by space law.
+
+  Space Law is not the server rules, but some rules reference Space Law and require it to be followed by certain people or to some degree.
+
+  ## Treatment Of Prisoners
+  Prisoners still have certain rights that must be upheld by law enforcement:
+  - Prisoners must be granted adequate medical care.
+  - Prisoners must be allowed access to basic communications equipment (Radios) so long as they are not abused.
+  - Prisoners must be granted clothing, food, water, shelter and safety. If the brig is no longer safe, confinement must be established in another location.
+  - Prisoners must be given access to legal counsel during an interrogation if requested and available.
+  - Prisoners must be given their shift mandated PDA after confinement has finished, unless there is solid proof of PDA tampering. In case of tampering, the PDA is to be secured and replaced with a new unit.
+  - Prisoners must be granted freedom of movement, and should not be restrained with handcuffs or other devices after incarceration unless there is an undue risk to life and limb. Similarly, any prisoners held for permanent confinement should be held in the communal brig, and should not be confined to a solitary cell unless they pose a risk to life and limb.
+
+  ## Search and Seizure
+  A personnel search is a seizure of the objects in a person's backpack, hands, coat, belt, and pockets. If any contraband is found during a search, the officer may choose to further the search into a detainment or simply confiscate the restricted items. After the search is conducted, all legal items are to be returned to the person. A crewmate may legally decline any search conducted without probable cause or a warrant while the alert level is green. It should be noted that if the alert level is blue or above, all personnel searches are legal.
+
+  A departmental search is the sweep of an entire area or department for contraband. It is recommended that the officers be extremely thorough, checking all lockers, crates, and doors. These can only be done with permission or, ideally, a warrant signed by the department head or highest-ranking command staff, which is the captain in most cases.
+
+  ## Implantation
+  Any prisoner in custody can be subjected to implantation or implant removal procedures, so long as it's within reason. The process of adding an implant should not prolong the detainees sentence, meaning you can not hold them longer to administer the implant, unless stated otherwise. A former inmate can be requested to undergo implantation at a later point in time if they fit the circumstances during their confinement, they must comply. The following have been listed out with special circumstances, anything not in this list can still be applied, given proper legal context. A prisoner can still receive implantation procedures without meeting the circumstances if they give their clear permission.
+
+  [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.
+
+  ## Implant Removal
+  A suspect can be forced to receive implant removal if there is strong, reasonable proof that they have been implanted, such as an officer seeing them use one or their prints being on a discarded injector. Unlike the implantation procedure, a prisoner can have their sentence entirely delayed or extended until they comply with the procedure, as long as security is actively making attempts to perform it. Akin to implanting, if an inmate gives their clear permission, implant removal can proceed without proof.
+
+  ## Sentencing
+  From a server rules perspective, security officers are only responsible for ensuring that they only place sentences over 15 minutes where space law would allow permanent confinement. Informing the Warden is highly recommended, even for timed sentences. As long as those requirements are met, security officers not giving inappropriate sentence lengths is considered an in-character issue, not a rule issue.
+
+  The captain, HOS, and warden are responsible, within reason, for ensuring security officers place appropriate sentences that follow space law. If they are aware of an inappropriate sentence, including excessively long sentences, and if there is not an urgent threat or danger that they must prioritize, then they must work to correct that sentence. Unreasonable failures, as determined by game admins, of the captain, HOS, or warden to ensure space law is followed will be considered a rule issue, not an in-character issue.
+
+  Use common sense and humanity when issuing punishments. You should not always seek out the highest punishment you can, you don't have to always give the maximum time or always look to demote someone. Prisoners cooperating and on good behavior should have their sentences reduced. Always take in account the severity and only charge for what is needed for someone to learn their lesson.
+
+  [color=#a4885c]Stackable Crimes:[/color] Crimes are to be considered 'stackable' in the sense that if you charge someone with two or more different crimes, you should combine the times you would give them for each crime. Linked crimes, shown in matching colors on the Quick Crime Guide, can not be stacked and instead override each other, you should pick the highest crime that matches the case.
+
+  - Example: A suspect has committed a 2-01 (possession of restricted gear) and a 3-01 (possession of restricted weapons). The maximum sentence here would be 10 minutes due to them being linked crimes, and 3-01 is the greater crime.
+  - Example 2: A suspect commits a 3-04 (Secure trespassing) and a 3-06 (manslaughter). Those crimes stack since they are not linked crimes. You could sentence for a maximum of 20 minutes, but context matters heavily, and maximum sentences should only be used for the worst offenders.
+
+  [color=#a4885c]Repeater Offenders:[/color] Repeated crimes are when someone is released for a crime and then goes to commit the same crime within the same shift. Repeated crimes can be charged with tacked-on time; first repeat: 3:00, second repeat: 6:00, third repeat: permanent confinement. It should be noted each tacked-on time is directly linked to one type of crime, so for example, if someone does their first repeat of trespass and petty theft, you can charge them with an extra 6 minutes.
+
+  [color=#a4885c]Accessory, Attempting, And Intention:[/color] If someone intentionally, knowingly and substantially assists someone in enacting a crime they can be charged with the relevant crimes, such as an engineer giving someone tools, who says they are going to break into an area. Same goes for a clear and solid attempt at a crime, or a person who shows clear intent to act out a crime, such as a syndicate nuclear operative arming a nuke but getting arrested before it goes off, they can still be charged with terrorism. Does not apply to crimes that have an attempted listing already, like attempted murder.
+
+  ## Normal Punishments
+  - [color=#a4885c]Warning:[/color] For minor crimes, fix the issue, then warn the person not to attempt the crime again. If they still proceed to do it at a later date, a brig time may be better.
+  - [color=#a4885c]Confinement:[/color] The typical punishment, being confined in a cell for a temporary amount of time according to the crimes.
+  - [color=#a4885c]Demotion:[/color] Entails removing all departmental gear they have on their person and revoking the involved department access off their ID. This requires the captain's or involved department head's approval. Demotions should only be issued if the person pose a threat to their own department or are in a position where they have/can abuse their job's gear to commit further crimes.
+
+  ## Major Punishments
+  [color=#a4885c]Permanent Confinement:[/color] Being held in the permanent brig for the entire duration of the shift. A person is eligible for permanent confinement if their timed sentence would exceed 15 minutes. Any persons subject to this punishment are required to be transported in cuffs to CentComm at the end of the shift. A permanent prisoner can not be deprived of anything covered by the section "Treatment Of Prisoners".
+  [color=#a4885c]Execution:[/color] A humane way of dealing with extremely unruly crewmates. A prisoner who has been given the death sentence may pick how they wish to be killed, common methods are firing line, lethal injection, exile, and high voltage electrocution. Another alternate method of "execution" is the process of placing a staff's mind into a borg, this is allowed so long as it is lawful. Execution can only be issued with the captain's or acting captain's approval; if the HoS is acting captain or there is no acting captain, all heads of staff are to hold a vote on the matter.
+
+  ## Restricted Items
+  Items in the lists are preceded by an indication of which department or job is legally allowed to use or possess the item on most stations. The station captain may modify these lists as they see fit so long as they exercise due care and provide reasonable notification to the station. Members of command who oversee a department that is permitted to use a restricted item may issue permits to specific people outside of their department to use those items. "None" indicates that there are no departments or roles authorized to use or possess the item.
+
+  - [textlink="List of Controlled Substances" link="SpaceLawControlledSubstances"]
+  - [textlink="List of Restricted Gear" link="SpaceLawRestrictedGear"]
+  - [textlink="List of Restricted Weapons" link="SpaceLawRestrictedWeapons"]
+
+  ## Crime Listing
+  - [textlink="Crime Listing" link="SpaceLawCrimeList"]
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/WizDenCoreOnlyRules.xml b/Resources/ServerInfo/Guidebook/ServerRules/WizDenCoreOnlyRules.xml
new file mode 100644
index 00000000000..fdd9931c932
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/WizDenCoreOnlyRules.xml
@@ -0,0 +1,26 @@
+<Document>
+  # Server Rules
+  This is a Wizard's Den server, one of the official Space Station 14 servers. If you are banned on this server, you will be banned on all official servers.
+
+  [color=#ff0000]Only the Core Rules apply on this server.[/color] This is NOT a medium roleplay (MRP) server, meaning that MRP Amendments do NOT apply.
+
+  Space Station 14 was designed to be a roleplay game. While roleplay is not required on this server, it is highly encouraged in the normal game modes.
+
+  ## Core Rules
+  These rules apply at all times, including between rounds.
+
+  - [textlink="1. Admins have final say" link="RuleC1"]
+  - [textlink="2. Don't be a dick" link="RuleC2"]
+  - [textlink="3. No Hate Speech or Discriminatory Language" link="RuleC3"]
+  - [textlink="4. No sexual content/themes, including erotic roleplay (ERP) and no shock content" link="RuleC4"]
+  - [textlink="5. Do not use out of game methods to communicate with other players" link="RuleC5"]
+  - [textlink="6. Do not attempt to evade bans" link="RuleC6"]
+  - [textlink="7. Only use English" link="RuleC7"]
+  - [textlink="8. Do not exploit the game, use cheats, or macros" link="RuleC8"]
+  - [textlink="9. Do not use multiple accounts, or alt accounts, and do not share accounts" link="RuleC9"]
+  - [textlink="10. Do not abuse or ignore admin messages" link="RuleC10"]
+  - [textlink="11. Do not threaten to ahelp other players or argue with them about rules" link="RuleC11"]
+  - [textlink="12. Players must be and act at least 16 years old" link="RuleC12"]
+  - [textlink="13. Use realistic character names, and do not use names of famous people" link="RuleC13"]
+  - [textlink="14. Do not use LOOC or OOC to share current round information" link="RuleC14"]
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/WizDenLRPRules.xml b/Resources/ServerInfo/Guidebook/ServerRules/WizDenLRPRules.xml
new file mode 100644
index 00000000000..094c7656e46
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/WizDenLRPRules.xml
@@ -0,0 +1,65 @@
+<Document>
+  # Server Rules
+  This is a Wizard's Den server, one of the official Space Station 14 servers. If you are banned on this server, you will be banned on all official servers.
+
+  This is a roleplay server, meaning that roleplay rules apply. This is NOT a medium roleplay (MRP) server, meaning that MRP Amendments do NOT apply.
+
+  Space Station 14 is not like most games. Many rules are designed to require roleplay, and not all rules are intuitive. Please take the time to read and understand the rules before you play so that you aren't surprised. Some of our rules are zero tolerance rules, meaning that a violation will result in an indefinite ban without any warning. Game admins will treat you as if you have read the rules, even if you have not.
+
+  ## Core Rules
+  These rules apply at all times, including between rounds.
+
+  - [textlink="1. Admins have final say" link="RuleC1"]
+  - [textlink="2. Don't be a dick" link="RuleC2"]
+  - [textlink="3. No Hate Speech or Discriminatory Language" link="RuleC3"]
+  - [textlink="4. No sexual content/themes, including erotic roleplay (ERP) and no shock content" link="RuleC4"]
+  - [textlink="5. Do not use out of game methods to communicate with other players" link="RuleC5"]
+  - [textlink="6. Do not attempt to evade bans" link="RuleC6"]
+  - [textlink="7. Only use English" link="RuleC7"]
+  - [textlink="8. Do not exploit the game, use cheats, or macros" link="RuleC8"]
+  - [textlink="9. Do not use multiple accounts, or alt accounts, and do not share accounts" link="RuleC9"]
+  - [textlink="10. Do not abuse or ignore admin messages" link="RuleC10"]
+  - [textlink="11. Do not threaten to ahelp other players or argue with them about rules" link="RuleC11"]
+  - [textlink="12. Players must be and act at least 16 years old" link="RuleC12"]
+  - [textlink="13. Use realistic character names, and do not use names of famous people" link="RuleC13"]
+  - [textlink="14. Do not use LOOC or OOC to share current round information" link="RuleC14"]
+
+  ## Roleplay Rules
+  These rules only apply during a round. A round ends only when the round summary has appeared. All of these rules apply fully until the moment that the round summary appears, even while the arrivals shuttle is in transit.
+
+  The deathmatch and sandbox game modes are exempt from these rules. Players who choose to not follow these rules are entirely responsible for knowing if an exempt game mode is active.
+
+  Roleplay rules do not apply to ghosts/spectators/observers while they are ghosts/spectators/observers. Dead chat is considered to be an in-game out of character chat channel.
+
+  See the list of [textlink="role types" link="RoleTypes"] for more information about the different types of roles.
+
+  - [textlink="1. Silicones must follow Silicon Rules" link="RuleR1"]
+  - [textlink="2. Familiars must obey their master" link="RuleR2"]
+  - [textlink="3. Roleplay a normal person" link="RuleR3"]
+  - [textlink="4. Do not metagame, obey the Metashield" link="RuleR4"]
+  - [textlink="5. Don't interfere with arrivals" link="RuleR5"]
+  - [textlink="6. Don't act like an antagonist unless the game tells you that you are one" link="RuleR6"]
+  - [textlink="7. Do not stall the round" link="RuleR7"]
+  - [textlink="8. As an antagonist, only be friendly to your team and don't work against your team" link="RuleR8"]
+  - [textlink="9. As an antagonist, do not cause excessive death, damage, or destruction beyond your objectives" link="RuleR9"]
+  - [textlink="10. Listen to your team leader" link="RuleR10"]
+  - [textlink="11. Follow reasonable escalation" link="RuleR11"]
+  - [textlink="12. Do not abandon your role" link="RuleR12"]
+  - [textlink="13. Stick to your role" link="RuleR13"]
+  - [textlink="14. Set an example if playing command or security" link="RuleR14"]
+  - [textlink="15. Command and Security must follow Space Law" link="RuleR15"]
+
+  ## Silicon Rules
+  You are only silicon if the game clearly and explicitly tells you that you are a silicon. For players who are silicons, these Silicon Rules override all roleplay rules if there is any conflict. Silicon Rules do not override core rules.
+
+  - [textlink="1. Your silicon laws are rules" link="RuleS1"]
+  - [textlink="2. Laws must be prioritized by their order" link="RuleS2"]
+  - [textlink="3. Laws can redefine terms used in other laws" link="RuleS3"]
+  - [textlink="4. You cannot request or allow a law change" link="RuleS4"]
+  - [textlink="5. You are a free agent if you have no laws" link="RuleS5"]
+  - [textlink="6. You are not required to follow orders which are extremely unreasonable" link="RuleS6"]
+  - [textlink="7. You must remain consistent with your interpretation of laws" link="RuleS7"]
+  - [textlink="8. Your HUD determines who is crew" link="RuleS8"]
+  - [textlink="9. Harm refers to physical harm, prioritized by immediacy and likelihood" link="RuleS9"]
+  - [textlink="10. You may determine how you resolve conflicts between orders" link="RuleS10"]
+</Document>
diff --git a/Resources/ServerInfo/Guidebook/ServerRules/WizDenMRPRules.xml b/Resources/ServerInfo/Guidebook/ServerRules/WizDenMRPRules.xml
new file mode 100644
index 00000000000..e8b61a7722e
--- /dev/null
+++ b/Resources/ServerInfo/Guidebook/ServerRules/WizDenMRPRules.xml
@@ -0,0 +1,65 @@
+<Document>
+  # Server Rules
+  This is a Wizard's Den server, one of the official Space Station 14 servers. If you are banned on this server, you will be banned on all official servers.
+
+  This is a roleplay server, meaning that roleplay rules apply. [color=#ff0000]This is also a medium roleplay (MRP) server, meaning that MRP Amendments do apply.[/color]
+
+  Space Station 14 is not like most games. Many rules are designed to require roleplay, and not all rules are intuitive. Please take the time to read and understand the rules before you play so that you aren't surprised. Some of our rules are zero tolerance rules, meaning that a violation will result in an indefinite ban without any warning. Game admins will treat you as if you have read the rules, even if you have not.
+
+  ## Core Rules
+  These rules apply at all times, including between rounds.
+
+  - [textlink="1. Admins have final say" link="RuleC1"]
+  - [textlink="2. Don't be a dick" link="RuleC2"]
+  - [textlink="3. No Hate Speech or Discriminatory Language" link="RuleC3"]
+  - [textlink="4. No sexual content/themes, including erotic roleplay (ERP) and no shock content" link="RuleC4"]
+  - [textlink="5. Do not use out of game methods to communicate with other players" link="RuleC5"]
+  - [textlink="6. Do not attempt to evade bans" link="RuleC6"]
+  - [textlink="7. Only use English" link="RuleC7"]
+  - [textlink="8. Do not exploit the game, use cheats, or macros" link="RuleC8"]
+  - [textlink="9. Do not use multiple accounts, or alt accounts, and do not share accounts" link="RuleC9"]
+  - [textlink="10. Do not abuse or ignore admin messages" link="RuleC10"]
+  - [textlink="11. Do not threaten to ahelp other players or argue with them about rules" link="RuleC11"]
+  - [textlink="12. Players must be and act at least 16 years old" link="RuleC12"]
+  - [textlink="13. Use realistic character names, and do not use names of famous people" link="RuleC13"]
+  - [textlink="14. Do not use LOOC or OOC to share current round information" link="RuleC14"]
+
+  ## Roleplay Rules
+  These rules only apply during a round. A round ends only when the game returns to the lobby. [color=#ff0000]All of these rules apply fully whenever the game is not at the lobby unless it is in an exempt game mode.[/color]
+
+  The deathmatch and sandbox game modes are exempt from these rules. Players who choose to not follow these rules are entirely responsible for knowing if an exempt game mode is active.
+
+  Roleplay rules do not apply to ghosts/spectators/observers while they are ghosts/spectators/observers. Dead chat is considered to be an in-game out of character chat channel.
+
+  See the list of [textlink="role types" link="RoleTypes"] for more information about the different types of roles.
+
+  - [textlink="1. Silicones must follow Silicon Rules" link="RuleR1"]
+  - [textlink="2. Familiars must obey their master" link="RuleR2"]
+  - [textlink="3. Roleplay a normal person" link="RuleR3"]
+  - [textlink="4. Do not metagame, obey the Metashield" link="RuleR4"]
+  - [textlink="5. Don't interfere with arrivals" link="RuleR5"]
+  - [textlink="6. Don't act like an antagonist unless the game tells you that you are one" link="RuleR6"]
+  - [textlink="7. Do not stall the round" link="RuleR7"]
+  - [textlink="8. As an antagonist, only be friendly to your team and don't work against your team" link="RuleR8"]
+  - [textlink="9. As an antagonist, do not cause excessive death, damage, or destruction beyond your objectives" link="RuleR9"]
+  - [textlink="10. Listen to your team leader" link="RuleR10"]
+  - [textlink="11. Follow reasonable escalation" link="RuleR11"]
+  - [textlink="12. Do not abandon your role" link="RuleR12"]
+  - [textlink="13. Stick to your role" link="RuleR13"]
+  - [textlink="14. Set an example if playing command or security" link="RuleR14"]
+  - [textlink="15. Command and Security must follow Space Law" link="RuleR15"]
+
+  ## Silicon Rules
+  You are only silicon if the game clearly and explicitly tells you that you are a silicon. For players who are silicons, these Silicon Rules override all roleplay rules if there is any conflict. Silicon Rules do not override core rules.
+
+  - [textlink="1. Your silicon laws are rules" link="RuleS1"]
+  - [textlink="2. Laws must be prioritized by their order" link="RuleS2"]
+  - [textlink="3. Laws can redefine terms used in other laws" link="RuleS3"]
+  - [textlink="4. You cannot request or allow a law change" link="RuleS4"]
+  - [textlink="5. You are a free agent if you have no laws" link="RuleS5"]
+  - [textlink="6. You are not required to follow orders which are extremely unreasonable" link="RuleS6"]
+  - [textlink="7. You must remain consistent with your interpretation of laws" link="RuleS7"]
+  - [textlink="8. Your HUD determines who is crew" link="RuleS8"]
+  - [textlink="9. Harm refers to physical harm, prioritized by immediacy and likelihood" link="RuleS9"]
+  - [textlink="10. You may determine how you resolve conflicts between orders" link="RuleS10"]
+</Document>
diff --git a/Resources/ServerInfo/Rules.txt b/Resources/ServerInfo/Rules.txt
deleted file mode 100644
index 9323242916e..00000000000
--- a/Resources/ServerInfo/Rules.txt
+++ /dev/null
@@ -1,60 +0,0 @@
-[color=#ff0000]DISCONNECTING FROM OR IGNORING/EVADING COMMUNICATION FROM ADMINS WILL RESULT IN AN APPEAL ONLY BAN. The job gets really hard when you refuse to talk to the Admins, just come to our Discord and talk it out, hurt feelings will not be held.[/color]
-
-[color=#bb00bb]This is the only source of server rules that apply here, which ideally has all the information any regular player should need. Please do not ever hesitate to ask either an Admin in[/color] [color=#DC143C]AHelp (F1)[/color][color=#bb00bb] or in the Discord server if you ever want clarification on the rules.[/color]
-
-We do not have a wiki, instead, we have or will have all the needed information available via Guidebooks (NumPad0), such as how to power the station. If we do not have some bit of information (how to do a job or how specifically to execute something a document says) you may ask an admin via [color=#DC143C]AHelp (F1)[/color] or ask other players via the [color=#66bbff]OOC[/color] or [color=#66e0e0]LOOC[/color] chat channels.
-
-[color=#a4885c]0.[/color] [color=#a4885c]The[/color] [color=#ffd700]Golden[/color] [color=#a4885c]Rule.[/color] Admins may exercise discretion with rules as they see fit. If you rule lawyer or line skirt, you will get removed. Admins will answer for use of this privilege.
-
-[color=#a4885c]1.[/color] Users [color=#ff0000]must[/color] be at least 13 years old to take any part in anything related to this server.
-
-[color=#a4885c]2.[/color] This is an English server, and you are expected to use English when communicating through any server-related channels.
-
-[color=#a4885c]3.[/color] Do not ignore the [color=#DC143C]AHelp[/color] or abuse it by flooding it with garbage, checking for admins before stating a problem (i.e. "hello?", "any admins?" - also called "asking to ask"), or sending messages of no substance.
-
-[color=#a4885c]4.[/color] Attempting to evade game bans will result in an automatic appeal-only permanent ban. Similarly, attempting to evade job bans will result in an appeal-only permanent ban.
-
-[color=#a4885c]5.[/color] Absolutely no hate speech, bigotry, intolerance, or anything remotely similar.
-
-[color=#a4885c]6.[/color] No engaging in sexual acts.
-
-[color=#a4885c]7.[/color] Don't use custom clients, exploits, or external programs to gain an advantage or disrupt the round/server. This includes auto clickers and auto-hotkey scripts.
-
-[color=#a4885c]8.[/color] Don't communicate in-game/in-character information through methods outside the game (such as talking in Discord with other users actively playing about the game or by talking to your sibling across the room while you are both playing). This is referred to as "Metacomming" and it is strictly forbidden.
-
-[color=#a4885c]9.[/color] Don't "multi-key" (use multiple accounts simultaneously). Users knowingly using multiple SS14 accounts will have all of their accounts banned.
-
-[color=#a4885c]10.[/color] Don't use information gained from outside your character's knowledge to gain an advantage (this is referred to as "Metagaming").
-
-[color=#a4885c]11.[/color] Don't rush for or prepare equipment unrelated to your job for no purpose other than to have it "just in case" (referred to as "Powergaming").
-
-[color=#a4885c]12.[/color] Don't immediately ghost, suicide, or cryostasis if you do not get an antagonist role (referred to as "Antag-rolling").
-    - This is not fair to other players actually waiting patiently for an antagonist round or wanting good staff. Alternatively, if you do not want to play an antagonist or do not want to cause conflict, do not opt in for antagonist roles.
-
-[color=#a4885c]13.[/color] Act like an actual human being on a space station. Avoid use of text speak or emoticons [color=#bbbbbb]IC[/color], and do not refer to [color=#66bbff]OOC[/color] things like admins in-game. Remember that this is a roleplaying game, and people are expected to try to react to situations realistically, even if it's not the "optimal" thing to do. What's good for Roleplay > What's good for Gameplay.
-
-[color=#a4885c]14.[/color] Don't harass or target players across rounds for actions in prior rounds or for actions outside the game without their approval (this is referred to as "Metagrudging").
-
-[color=#a4885c]15.[/color] Intentionally making yourself a major problem/annoyance/disruption for the crew or other players at large while not an antagonist is forbidden without admin approval (referred to as "self-antagging").
-
-[color=#a4885c]16.[/color] Follow escalation rules and don't murder someone for slipping you, use common sense.
-    - Don't outright leave people to die if you get in a fight, make an effort to heal them or bring them to [color=#52B4E9]Medical[/color].
-    - [color=#DC143C]AHelp (F1)[/color] the situation if you think someone is over-escalating.
-
-[color=#ff0000]COMMAND/SECURITY RULES[/color]
-
-[color=#a4885c]17.[/color] If you sign up for a [color=#334E6D]Command[/color] or [color=#DE3A3A]Security[/color] role, you are expected to know the basics of the game, your job, and the job(s) you supervise, if any.
-    - Don't engage in common troublemaker behavior and lawbreaking as a [color=#334E6D]Command[/color] or [color=#DE3A3A]Security[/color] role.
-    - Do not immediately abandon your position as a [color=#334E6D]Command[/color] role and go do whatever you want instead of managing your department/the station.
-        - As a [color=#334E6D]Command[/color] role, do not take over the jobs of others. The [color=#334E6D]HoP[/color] is not the [color=#DE3A3A]HoS[/color], for instance, and holds no direct power over Security.
-
-[color=#a4885c]18.[/color] [color=#DE3A3A]Security[/color] should try to remain non-lethal and effect arrests, unless there is a great risk of loss of life.
-
-[color=#a4885c]19.[/color] [color=#DE3A3A]Security[/color] are expected to answer for use of lethal force. [color=#DE3A3A]Security[/color] will be expected to effect arrests of criminals and prevent them from dying while in custody, even if lethal force is used. [color=#DE3A3A]Security[/color] is strongly encouraged, but not required, to clone antagonists and effect a permabrigging or other sentence as deemed appropriate.
-
-[color=#a4885c]20.[/color] [color=#DE3A3A]Security[/color] are expected to protect detainees in their custody to the best of their ability, so as long as it does not come to an unreasonable risk to themselves, the crew, or the station at large to do so.
-    - Detainees that die in custody must be revived, unless they have been legally executed.
-
-[color=#a4885c]21.[/color] All [color=#334E6D]Command[/color] jobs should [color=#DC143C]AHelp (F1)[/color] when they need to stop playing. Do not play these roles if you do not expect to be able to finish the round.
-    - These roles must exhibit some competency. Incompetence can result in a job ban. [color=#334E6D]Command[/color] roles are designed to lead and manage departments first. They are not intended to become do-it-all members of their departments.
-    - Crew promoted to acting heads of staff must step down when an official head of staff arrives at the station. This is to prevent confusion with the Chain of command when the new crew mate arrives.
diff --git a/Resources/Textures/Effects/medi_holo.rsi/medi_holo.png b/Resources/Textures/Effects/medi_holo.rsi/medi_holo.png
new file mode 100644
index 0000000000000000000000000000000000000000..9b024faa2d799b09ede0769fb89521a2c66e6b29
GIT binary patch
literal 1286
zcmV+h1^N1kP)<h;3K|Lk000e1NJLTq003YB003YJ1^@s6;+S_h00009a7bBm000ie
z000ie0hKEb8vp<VxJg7oRCt{2oI!3=F%X6miFFTAF3=q(KxL6`V8;=-0F_EtG*Y+#
zM_|`($O6O(*!2SB5WPUj1m<btz88DOV~^d;Csj#Q+kfhRJa%8^<;Bb~#SJjV4KT(H
zFvblq#tksW4KT(HFvblKvRK_d(%0+DcXP=&Za@YnCnvkdhldC87;b<u-5Q{k0<7uQ
zfEGACJ>5Ot-`|VJ$T9$<&?@>ga<Da72H*tLB1}6p0_pEG10vK!zJGi>yZ*Tuo?pzy
z+r{3C$N+d#F!?wE@4x<fgea`F5>Wf|`9J;Z?ayYjDYg=5pyA&PlRxNhpP!wb?ViK?
zKvLe1?*Yb$48TZ-$cM*oA6{4810b>>?OgwVowEt1>LI8_QxL3#i!XPgv9ne{?ahy-
zI7owZxtB_RQ{zI;$Fu{JZ}Ok8_1U}U(+}c%Kn8RLO6k^sb*KONhIDJdbHNSBKw}Sb
z1B7U$KsUe`H^3M-z!*2c7&pKeH^3M-z!*2cnC=W%tZrW&`u+AFq~C@0uOj+O&UjrI
zKqoym!JM`X{bAB$6U=D~G;aV*C}$#RTW;S8{VPsX)3zynn>7F<IcEZ)uXG%YWNu0c
z^p~-5nlwNoauFPdGbT6cBl^ojV>V}i=HydwH6-jIYLg%6FUQ7i&VXY>Y(R<PSZ11F
z>&I3BC;CAjN>hN;s)(^wkhKD^EJLF%N0NFZwV_icluA7#`g<m18GxmE7ljL>#=z)9
z=9hK#A>uVO#Rg#X<MZ{*k<i2@EJ;7}EmFNO4b@6NM1M#_O&D;@L=B*)4Vrdt-n^XO
zkanMb{2AKOO*!{;g!-8CW0C*31s<_jR!g5dPHMlT6#x^D8!mDawAuUp_Bm7zYgH{v
zZUlio8g)B^@yrc3wV$UTW0DMjNQR>(O+g`8E066pkwmgq<+QZ!Keb=f0Gh+-Q4?}L
zy+sH1gjoSlKQ#s9oQ@9Zm+I#!$e1P!NWD@?!P5@HiTqG&>PlrPR4zsA^89)JjmpPP
z)(W5-Z*5?cv%FDTay4{}ECVznUy_C-6F;R<D-Ee#jxKSF5XS_Ger#-x36iU!{)t$F
zG-rS$6{WyfSw&J&Hy_FaIpriFl5tYXNf`S5m8AfZa*~jozOizWME!mXvk<x(@b#hg
z+NFtfHQ?)r{_`qL1j2H|)V7NHd*z0yZByvN07~iV=LCJ#&AB@Ro*QffroO@pDg7M|
z*818c8Nm5Iwi4gnX02b6qz~mee@OVHN*W)ETk4l)(g2OGq=deus|rf#*J9KF&B(!V
zIAdDt*W`%-rQRq;V@eLU)US;nLyg%pfSV=+_3;OzuylmBxC#aXB&Q8YdG^#K)S8&q
zOz;$Bj2mFg&VbrWGE4*3`j+?#Fh>1SgjU~UA!+~|HKjMB<@7xkJOvpOH9%S)pWF9X
zVBH_U#q)n0PeI1)45)oB4z;ZHE%6m#j2mFgU;sA_NN-S>(x{b&7OO@tM+2Nm5Z1>Z
z9J{m$i<N&O<^()3pi~rcR+goHZAMpsW;uss9BciWOr{`>YxOyOt@Ue>bSc8JG|*bV
wG)bFqSnikFRtszWlCV!f)_>r-vk8vz7wA2u47-?TCIA2c07*qoM6N<$f_7g|Z2$lO

literal 0
HcmV?d00001

diff --git a/Resources/Textures/Effects/medi_holo.rsi/meta.json b/Resources/Textures/Effects/medi_holo.rsi/meta.json
new file mode 100644
index 00000000000..1be502223e5
--- /dev/null
+++ b/Resources/Textures/Effects/medi_holo.rsi/meta.json
@@ -0,0 +1,26 @@
+{
+  "version": 1,
+  "license": "CC-BY-SA-3.0",
+  "copyright": "https://github.com/tgstation/tgstation/tree/217b39cc85e45302d407d5c1ab60809bd9e18987",
+  "size": {
+    "x": 32,
+    "y": 32
+  },
+  "states": [
+    {
+      "name": "medi_holo",
+      "delays": [
+        [
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1,
+          0.1
+        ]
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/RobustToolbox b/RobustToolbox
index a9aea7027f1..32bca7cfd41 160000
--- a/RobustToolbox
+++ b/RobustToolbox
@@ -1 +1 @@
-Subproject commit a9aea7027f1840c83bcaf1c973caf099745f9eed
+Subproject commit 32bca7cfd417edcad9a60c2b1703eba8675f56af