diff --git a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs index 59e0e6040b..de9ccbbf50 100644 --- a/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs +++ b/Content.Client/Administration/UI/Tabs/ObjectsTab/ObjectsTab.xaml.cs @@ -76,7 +76,7 @@ public sealed partial class ObjectsTab : Control switch (selection) { case ObjectsTabSelection.Stations: - entities.AddRange(_entityManager.EntitySysManager.GetEntitySystem().Stations); + entities.AddRange(_entityManager.EntitySysManager.GetEntitySystem().GetStationNames()); break; case ObjectsTabSelection.Grids: { diff --git a/Content.Client/Anomaly/Effects/ClientInnerBodySystem.cs b/Content.Client/Anomaly/Effects/ClientInnerBodySystem.cs index 15ebc8a993..d96980fb1d 100644 --- a/Content.Client/Anomaly/Effects/ClientInnerBodySystem.cs +++ b/Content.Client/Anomaly/Effects/ClientInnerBodySystem.cs @@ -1,6 +1,6 @@ using Content.Shared.Anomaly.Components; using Content.Shared.Anomaly.Effects; -using Content.Shared.Body.Components; +using Content.Shared.Humanoid; using Robust.Client.GameObjects; namespace Content.Client.Anomaly.Effects; @@ -25,9 +25,8 @@ public sealed class ClientInnerBodyAnomalySystem : SharedInnerBodyAnomalySystem var index = _sprite.LayerMapReserve((ent.Owner, sprite), ent.Comp.LayerMap); - if (TryComp(ent, out var body) && - body.Prototype is not null && - ent.Comp.SpeciesSprites.TryGetValue(body.Prototype.Value, out var speciesSprite)) + if (TryComp(ent, out var humanoidAppearance) && + ent.Comp.SpeciesSprites.TryGetValue(humanoidAppearance.Species, out var speciesSprite)) { _sprite.LayerSetSprite((ent.Owner, sprite), index, speciesSprite); } diff --git a/Content.Client/CardboardBox/CardboardBoxSystem.cs b/Content.Client/CardboardBox/CardboardBoxSystem.cs index e52ce03a76..ecebe16727 100644 --- a/Content.Client/CardboardBox/CardboardBoxSystem.cs +++ b/Content.Client/CardboardBox/CardboardBoxSystem.cs @@ -1,8 +1,8 @@ using System.Numerics; -using Content.Shared.Body.Components; using Content.Shared.CardboardBox; using Content.Shared.CardboardBox.Components; using Content.Shared.Examine; +using Content.Shared.Mobs.Components; using Content.Shared.Movement.Components; using Robust.Client.GameObjects; @@ -15,13 +15,13 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem [Dependency] private readonly ExamineSystemShared _examine = default!; [Dependency] private readonly SpriteSystem _sprite = default!; - private EntityQuery _bodyQuery; + private EntityQuery _mobStateQuery; public override void Initialize() { base.Initialize(); - _bodyQuery = GetEntityQuery(); + _mobStateQuery = GetEntityQuery(); SubscribeNetworkEvent(OnBoxEffect); } @@ -66,8 +66,8 @@ public sealed class CardboardBoxSystem : SharedCardboardBoxSystem if (!_examine.InRangeUnOccluded(sourcePos, mapPos, box.Distance, null)) continue; - // no effect for anything too exotic - if (!_bodyQuery.HasComp(mob)) + // no effect for non-mobs that have MobMover, such as mechs and vehicles. + if (!_mobStateQuery.HasComp(mob)) continue; var ent = Spawn(box.Effect, mapPos); diff --git a/Content.Client/Changeling/Transform/ChangelingTransformBoundUserInterface.cs b/Content.Client/Changeling/Transform/ChangelingTransformBoundUserInterface.cs new file mode 100644 index 0000000000..8e383bc967 --- /dev/null +++ b/Content.Client/Changeling/Transform/ChangelingTransformBoundUserInterface.cs @@ -0,0 +1,35 @@ +using Content.Shared.Changeling.Transform; +using JetBrains.Annotations; +using Robust.Client.UserInterface; + +namespace Content.Client.Changeling.Transform; + +[UsedImplicitly] +public sealed partial class ChangelingTransformBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey) +{ + private ChangelingTransformMenu? _window; + + protected override void Open() + { + base.Open(); + + _window = this.CreateWindow(); + + _window.OnIdentitySelect += SendIdentitySelect; + } + + protected override void UpdateState(BoundUserInterfaceState state) + { + base.UpdateState(state); + + if (state is not ChangelingTransformBoundUserInterfaceState current) + return; + + _window?.UpdateState(current); + } + + public void SendIdentitySelect(NetEntity identityId) + { + SendPredictedMessage(new ChangelingTransformIdentitySelectMessage(identityId)); + } +} diff --git a/Content.Client/Changeling/Transform/ChangelingTransformMenu.xaml b/Content.Client/Changeling/Transform/ChangelingTransformMenu.xaml new file mode 100644 index 0000000000..38ae0ec715 --- /dev/null +++ b/Content.Client/Changeling/Transform/ChangelingTransformMenu.xaml @@ -0,0 +1,8 @@ + + + + diff --git a/Content.Client/Changeling/Transform/ChangelingTransformMenu.xaml.cs b/Content.Client/Changeling/Transform/ChangelingTransformMenu.xaml.cs new file mode 100644 index 0000000000..beef9ae427 --- /dev/null +++ b/Content.Client/Changeling/Transform/ChangelingTransformMenu.xaml.cs @@ -0,0 +1,60 @@ +using System.Numerics; +using Content.Client.UserInterface.Controls; +using Content.Shared.Changeling.Transform; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.Changeling.Transform; + +[GenerateTypedNameReferences] +public sealed partial class ChangelingTransformMenu : RadialMenu +{ + [Dependency] private readonly IEntityManager _entity = default!; + public event Action? OnIdentitySelect; + + public ChangelingTransformMenu() + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + } + + public void UpdateState(ChangelingTransformBoundUserInterfaceState state) + { + Main.DisposeAllChildren(); + foreach (var identity in state.Identites) + { + var identityUid = _entity.GetEntity(identity); + + if (!_entity.TryGetComponent(identityUid, out var metadata)) + continue; + + var identityName = metadata.EntityName; + + var button = new ChangelingTransformMenuButton() + { + StyleClasses = { "RadialMenuButton" }, + SetSize = new Vector2(64, 64), + ToolTip = identityName, + }; + + var entView = new SpriteView() + { + SetSize = new Vector2(48, 48), + VerticalAlignment = VAlignment.Center, + HorizontalAlignment = HAlignment.Center, + Stretch = SpriteView.StretchMode.Fill, + }; + entView.SetEntity(identityUid); + button.OnButtonUp += _ => + { + OnIdentitySelect?.Invoke(identity); + Close(); + }; + button.AddChild(entView); + Main.AddChild(button); + } + } +} + +public sealed class ChangelingTransformMenuButton : RadialMenuTextureButtonWithSector; diff --git a/Content.Client/Cloning/CloningSystem.cs b/Content.Client/Cloning/CloningSystem.cs new file mode 100644 index 0000000000..9bfa230295 --- /dev/null +++ b/Content.Client/Cloning/CloningSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Cloning; + +namespace Content.Client.Cloning; + +public sealed partial class CloningSystem : SharedCloningSystem; diff --git a/Content.Client/Forensics/Systems/ForensicsSystem.cs b/Content.Client/Forensics/Systems/ForensicsSystem.cs new file mode 100644 index 0000000000..048fff600e --- /dev/null +++ b/Content.Client/Forensics/Systems/ForensicsSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared.Forensics.Systems; + +namespace Content.Client.Forensics.Systems; + +public sealed class ForensicsSystem : SharedForensicsSystem; diff --git a/Content.Client/Inventory/ClientInventorySystem.cs b/Content.Client/Inventory/ClientInventorySystem.cs index 1f926b42a1..97da176ed2 100644 --- a/Content.Client/Inventory/ClientInventorySystem.cs +++ b/Content.Client/Inventory/ClientInventorySystem.cs @@ -115,6 +115,13 @@ namespace Content.Client.Inventory OnLinkInventorySlots?.Invoke(uid, component); } + protected override void OnInit(Entity ent, ref ComponentInit args) + { + base.OnInit(ent, ref args); + + _clothingVisualsSystem.InitClothing(ent.Owner, ent.Comp); + } + public override void Shutdown() { CommandBinds.Unregister(); @@ -261,7 +268,6 @@ namespace Content.Client.Inventory TryAddSlotData((ent.Owner, inventorySlots), (SlotData)slot); } - _clothingVisualsSystem.InitClothing(ent, ent.Comp); if (ent.Owner == _playerManager.LocalEntity) ReloadInventory(inventorySlots); } diff --git a/Content.Client/Playtime/ClientsidePlaytimeTrackingManager.cs b/Content.Client/Playtime/ClientsidePlaytimeTrackingManager.cs index 330ff9122f..9a4f75d596 100644 --- a/Content.Client/Playtime/ClientsidePlaytimeTrackingManager.cs +++ b/Content.Client/Playtime/ClientsidePlaytimeTrackingManager.cs @@ -94,7 +94,7 @@ public sealed class ClientsidePlaytimeTrackingManager return; } - // At less than 1 minute of time diff, there's not much point, and saving regardless will brick tests + // At less than 1 minute of time diff, there's not much point // The reason this isn't checking for 0 is because TotalMinutes is fractional, rather than solely whole minutes if (timeDiffMinutes < 1) return; diff --git a/Content.Client/Station/StationSystem.cs b/Content.Client/Station/StationSystem.cs index 5a4a1853d2..e1ccd14d7b 100644 --- a/Content.Client/Station/StationSystem.cs +++ b/Content.Client/Station/StationSystem.cs @@ -2,34 +2,5 @@ namespace Content.Client.Station; -/// -/// This handles letting the client know stations are a thing. Only really used by an admin menu. -/// -public sealed partial class StationSystem : SharedStationSystem -{ - private readonly List<(string Name, NetEntity Entity)> _stations = new(); - - /// - /// All stations that currently exist. - /// - /// - /// I'd have this just invoke an entity query, but we're on the client and the client barely knows about stations. - /// - // TODO: Stations have a global PVS override now, this can probably be changed into a query. - public IReadOnlyList<(string Name, NetEntity Entity)> Stations => _stations; - - /// - public override void Initialize() - { - base.Initialize(); - - SubscribeNetworkEvent(StationsUpdated); - } - - private void StationsUpdated(StationsUpdatedEvent ev) - { - _stations.Clear(); - // TODO this needs to be done in component states and with the Ensure() methods - _stations.AddRange(ev.Stations); - } -} +/// +public sealed partial class StationSystem : SharedStationSystem; diff --git a/Content.Client/Storage/Systems/StorageSystem.cs b/Content.Client/Storage/Systems/StorageSystem.cs index bd6659de01..1a21de4b99 100644 --- a/Content.Client/Storage/Systems/StorageSystem.cs +++ b/Content.Client/Storage/Systems/StorageSystem.cs @@ -40,6 +40,11 @@ public sealed class StorageSystem : SharedStorageSystem component.MaxItemSize = state.MaxItemSize; component.Whitelist = state.Whitelist; component.Blacklist = state.Blacklist; + component.StorageInsertSound = state.StorageInsertSound; + component.StorageRemoveSound = state.StorageRemoveSound; + component.StorageOpenSound = state.StorageOpenSound; + component.StorageCloseSound = state.StorageCloseSound; + component.DefaultStorageOrientation = state.DefaultStorageOrientation; _oldStoredItems.Clear(); diff --git a/Content.Client/UserInterface/StatsWindow.xaml b/Content.Client/UserInterface/StatsWindow.xaml index 9cda4e41c6..8a287b4aa7 100644 --- a/Content.Client/UserInterface/StatsWindow.xaml +++ b/Content.Client/UserInterface/StatsWindow.xaml @@ -5,7 +5,7 @@ - + diff --git a/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs b/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs index 7652e39bfd..8d74e2efe7 100644 --- a/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs +++ b/Content.Client/UserInterface/Systems/Emotes/EmotesUIController.cs @@ -21,17 +21,20 @@ public sealed class EmotesUIController : UIController, IOnStateChanged UIManager.GetActiveUIWidgetOrNull()?.EmotesButton; private SimpleRadialMenu? _menu; - private static readonly Dictionary EmoteGroupingInfo - = new Dictionary - { - [EmoteCategory.General] = ("emote-menu-category-general", new SpriteSpecifier.Texture(new ResPath("/Textures/Clothing/Head/Soft/mimesoft.rsi/icon.png"))), - [EmoteCategory.Hands] = ("emote-menu-category-hands", new SpriteSpecifier.Texture(new ResPath("/Textures/Clothing/Hands/Gloves/latex.rsi/icon.png"))), - [EmoteCategory.Vocal] = ("emote-menu-category-vocal", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Emotes/vocal.png"))), - }; + private static readonly Dictionary EmoteGroupingInfo = + new() + { + [EmoteCategory.General] = ("emote-menu-category-general", + new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Head/Soft/mimesoft.rsi"), "icon")), + [EmoteCategory.Hands] = ("emote-menu-category-hands", + new SpriteSpecifier.Rsi(new ResPath("/Textures/Clothing/Hands/Gloves/latex.rsi"), "icon")), + [EmoteCategory.Vocal] = ("emote-menu-category-vocal", + new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Emotes/vocal.png"))), + }; public void OnStateEntered(GameplayState state) { @@ -135,7 +138,7 @@ public sealed class EmotesUIController : UIController, IOnStateChanged(); var player = _playerManager.LocalSession?.AttachedEntity; - Dictionary> emotesByCategory = new(); + Dictionary> emotesByCategory = new(); foreach (var emote in emotePrototypes) { if(emote.Category == EmoteCategory.Invalid) diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs index 2570e2246a..dae3203f9f 100644 --- a/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs +++ b/Content.IntegrationTests/Tests/GameObjects/Components/ActionBlocking/HandCuffTest.cs @@ -1,11 +1,9 @@ #nullable enable using Content.Server.Cuffs; -using Content.Shared.Body.Components; using Content.Shared.Cuffs.Components; using Content.Shared.Hands.Components; using Robust.Server.Console; using Robust.Shared.GameObjects; -using Robust.Shared.Map; namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking { @@ -22,9 +20,15 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking components: - type: Cuffable - type: Hands + hands: + hand_right: + location: Right + hand_left: + location: Left + sortedHands: + - hand_right + - hand_left - type: ComplexInteraction - - type: Body - prototype: Human - type: entity name: HandcuffsDummy @@ -47,7 +51,6 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking HandsComponent hands = default!; var entityManager = server.ResolveDependency(); - var mapManager = server.ResolveDependency(); var host = server.ResolveDependency(); var map = await pair.CreateTestMap(); @@ -73,7 +76,6 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.ActionBlocking { Assert.That(entityManager.TryGetComponent(human, out cuffed!), $"Human has no {nameof(CuffableComponent)}"); Assert.That(entityManager.TryGetComponent(human, out hands!), $"Human has no {nameof(HandsComponent)}"); - Assert.That(entityManager.TryGetComponent(human, out BodyComponent? _), $"Human has no {nameof(BodyComponent)}"); Assert.That(entityManager.TryGetComponent(cuffs, out HandcuffComponent? _), $"Handcuff has no {nameof(HandcuffComponent)}"); Assert.That(entityManager.TryGetComponent(secondCuffs, out HandcuffComponent? _), $"Second handcuffs has no {nameof(HandcuffComponent)}"); }); diff --git a/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs b/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs index 9e925a451a..02552669f7 100644 --- a/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs +++ b/Content.IntegrationTests/Tests/Station/EvacShuttleTest.cs @@ -2,9 +2,9 @@ using System.Linq; using Content.Server.GameTicking; using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Systems; -using Content.Server.Station.Components; using Content.Shared.CCVar; using Content.Shared.Shuttles.Components; +using Content.Shared.Station.Components; using Robust.Shared.GameObjects; using Robust.Shared.Map.Components; diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs index 672ae695bf..2b5ea90b12 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs @@ -27,9 +27,9 @@ public sealed partial class AdminVerbSystem private static readonly EntProtoId DefaultNukeOpRule = "LoneOpsSpawn"; private static readonly EntProtoId DefaultRevsRule = "Revolutionary"; private static readonly EntProtoId DefaultThiefRule = "Thief"; - private static readonly ProtoId PirateGearId = "PirateGear"; - + private static readonly EntProtoId DefaultChangelingRule = "Changeling"; private static readonly EntProtoId ParadoxCloneRuleId = "ParadoxCloneSpawn"; + private static readonly ProtoId PirateGearId = "PirateGear"; // All antag verbs have names so invokeverb works. private void AddAntagVerbs(GetVerbsEvent args) @@ -58,7 +58,7 @@ public sealed partial class AdminVerbSystem _antag.ForceMakeAntag(targetPlayer, DefaultTraitorRule); }, Impact = LogImpact.High, - Message = string.Join(": ", traitorName, Loc.GetString("admin-verb-make-traitor")), + Message = string.Join(": ", traitorName, Loc.GetString("admin-verb-make-traitor")), }; args.Verbs.Add(traitor); @@ -153,6 +153,21 @@ public sealed partial class AdminVerbSystem }; args.Verbs.Add(thief); + var changelingName = Loc.GetString("admin-verb-text-make-changeling"); + Verb changeling = new() + { + Text = changelingName, + Category = VerbCategory.Antag, + Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Objects/Weapons/Melee/armblade.rsi"), "icon"), + Act = () => + { + _antag.ForceMakeAntag(targetPlayer, DefaultChangelingRule); + }, + Impact = LogImpact.High, + Message = string.Join(": ", changelingName, Loc.GetString("admin-verb-make-changeling")), + }; + args.Verbs.Add(changeling); + var paradoxCloneName = Loc.GetString("admin-verb-text-make-paradox-clone"); Verb paradox = new() { diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs index 6e88d59be6..44795d1fb2 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Tools.cs @@ -2,15 +2,12 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Numerics; using Content.Server.Administration.Components; -using Content.Server.Atmos; -using Content.Server.Atmos.Components; using Content.Server.Cargo.Components; using Content.Server.Doors.Systems; using Content.Server.Hands.Systems; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Server.Stack; -using Content.Server.Station.Components; using Content.Server.Station.Systems; using Content.Server.Weapons.Ranged.Systems; using Content.Shared.Access; @@ -28,6 +25,7 @@ using Content.Shared.Hands.Components; using Content.Shared.Inventory; using Content.Shared.PDA; using Content.Shared.Stacks; +using Content.Shared.Station.Components; using Content.Shared.Verbs; using Content.Shared.Weapons.Ranged.Components; using Robust.Server.Physics; diff --git a/Content.Server/Anomaly/AnomalySystem.Generator.cs b/Content.Server/Anomaly/AnomalySystem.Generator.cs index 1b88429204..46ad9278f8 100644 --- a/Content.Server/Anomaly/AnomalySystem.Generator.cs +++ b/Content.Server/Anomaly/AnomalySystem.Generator.cs @@ -1,6 +1,5 @@ using Content.Server.Anomaly.Components; using Content.Server.Power.EntitySystems; -using Content.Server.Station.Components; using Content.Shared.Anomaly; using Content.Shared.CCVar; using Content.Shared.Materials; @@ -164,8 +163,7 @@ public sealed partial class AnomalySystem var xform = Transform(uid); if (_station.GetStationInMap(xform.MapID) is not { } station || - !TryComp(station, out var data) || - _station.GetLargestGrid(data) is not { } grid) + _station.GetLargestGrid(station) is not { } grid) { if (xform.GridUid == null) return; diff --git a/Content.Server/Body/Systems/BrainSystem.cs b/Content.Server/Body/Systems/BrainSystem.cs index 86d2cb61ff..e916849a81 100644 --- a/Content.Server/Body/Systems/BrainSystem.cs +++ b/Content.Server/Body/Systems/BrainSystem.cs @@ -1,47 +1,46 @@ using Content.Server.Body.Components; using Content.Server.Ghost.Components; -using Content.Shared.Body.Components; using Content.Shared.Body.Events; using Content.Shared.Mind; using Content.Shared.Mind.Components; +using Content.Shared.Mobs.Components; using Content.Shared.Pointing; -namespace Content.Server.Body.Systems +namespace Content.Server.Body.Systems; + +public sealed class BrainSystem : EntitySystem { - public sealed class BrainSystem : EntitySystem + [Dependency] private readonly SharedMindSystem _mindSystem = default!; + + public override void Initialize() { - [Dependency] private readonly SharedMindSystem _mindSystem = default!; + base.Initialize(); - public override void Initialize() - { - base.Initialize(); + SubscribeLocalEvent((uid, _, args) => HandleMind(args.Body, uid)); + SubscribeLocalEvent((uid, _, args) => HandleMind(uid, args.OldBody)); + SubscribeLocalEvent(OnPointAttempt); + } - SubscribeLocalEvent((uid, _, args) => HandleMind(args.Body, uid)); - SubscribeLocalEvent((uid, _, args) => HandleMind(uid, args.OldBody)); - SubscribeLocalEvent(OnPointAttempt); - } + private void HandleMind(EntityUid newEntity, EntityUid oldEntity) + { + if (TerminatingOrDeleted(newEntity) || TerminatingOrDeleted(oldEntity)) + return; - private void HandleMind(EntityUid newEntity, EntityUid oldEntity) - { - if (TerminatingOrDeleted(newEntity) || TerminatingOrDeleted(oldEntity)) - return; + EnsureComp(newEntity); + EnsureComp(oldEntity); - EnsureComp(newEntity); - EnsureComp(oldEntity); + var ghostOnMove = EnsureComp(newEntity); + ghostOnMove.MustBeDead = HasComp(newEntity); // Don't ghost living players out of their bodies. - var ghostOnMove = EnsureComp(newEntity); - if (HasComp(newEntity)) - ghostOnMove.MustBeDead = true; + if (!_mindSystem.TryGetMind(oldEntity, out var mindId, out var mind)) + return; - if (!_mindSystem.TryGetMind(oldEntity, out var mindId, out var mind)) - return; + _mindSystem.TransferTo(mindId, newEntity, mind: mind); + } - _mindSystem.TransferTo(mindId, newEntity, mind: mind); - } - - private void OnPointAttempt(Entity ent, ref PointAttemptEvent args) - { - args.Cancel(); - } + private void OnPointAttempt(Entity ent, ref PointAttemptEvent args) + { + args.Cancel(); } } + diff --git a/Content.Server/Cargo/Components/StationCargoOrderDatabaseComponent.cs b/Content.Server/Cargo/Components/StationCargoOrderDatabaseComponent.cs index 37d0f5b7d1..56401602b3 100644 --- a/Content.Server/Cargo/Components/StationCargoOrderDatabaseComponent.cs +++ b/Content.Server/Cargo/Components/StationCargoOrderDatabaseComponent.cs @@ -1,8 +1,8 @@ using System.Linq; -using Content.Server.Station.Components; using Content.Shared.Cargo; using Content.Shared.Cargo.Components; using Content.Shared.Cargo.Prototypes; +using Content.Shared.Station.Components; using Robust.Shared.Prototypes; namespace Content.Server.Cargo.Components; diff --git a/Content.Server/Cargo/Systems/CargoSystem.Orders.cs b/Content.Server/Cargo/Systems/CargoSystem.Orders.cs index 862bf1f096..febe093d98 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.Orders.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.Orders.cs @@ -1,7 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.Cargo.Components; -using Content.Server.Station.Components; using Content.Shared.Cargo; using Content.Shared.Cargo.BUI; using Content.Shared.Cargo.Components; @@ -13,8 +12,8 @@ using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Labels.Components; using Content.Shared.Paper; +using Content.Shared.Station.Components; using JetBrains.Annotations; -using Robust.Shared.Audio; using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Timing; diff --git a/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs b/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs index 25fb514db6..9e5c20e8c9 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.Telepad.cs @@ -3,11 +3,11 @@ using System.Linq; using Content.Server.Cargo.Components; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; -using Content.Server.Station.Components; using Content.Shared.Cargo; using Content.Shared.Cargo.Components; using Content.Shared.DeviceLinking; using Content.Shared.Power; +using Content.Shared.Station.Components; using Robust.Shared.Audio; using Robust.Shared.Random; using Robust.Shared.Utility; diff --git a/Content.Server/Cargo/Systems/PricingSystem.cs b/Content.Server/Cargo/Systems/PricingSystem.cs index 5e449eb8da..24ee4ffaf2 100644 --- a/Content.Server/Cargo/Systems/PricingSystem.cs +++ b/Content.Server/Cargo/Systems/PricingSystem.cs @@ -90,19 +90,22 @@ public sealed class PricingSystem : EntitySystem if (args.Handled) return; - if (!TryComp(uid, out var body) || !TryComp(uid, out var state)) + if (!TryComp(uid, out var state)) { - Log.Error($"Tried to get the mob price of {ToPrettyString(uid)}, which has no {nameof(BodyComponent)} and no {nameof(MobStateComponent)}."); + Log.Error($"Tried to get the mob price of {ToPrettyString(uid)}, which has no {nameof(MobStateComponent)}."); return; } - // TODO: Better handling of missing. - var partList = _bodySystem.GetBodyChildren(uid, body).ToList(); - var totalPartsPresent = partList.Sum(_ => 1); - var totalParts = partList.Count; + var partPenalty = 0.0; + if (TryComp(uid, out var body)) + { + var partList = _bodySystem.GetBodyChildren(uid, body).ToList(); + var totalPartsPresent = partList.Sum(_ => 1); + var totalParts = partList.Count; - var partRatio = totalPartsPresent / (double) totalParts; - var partPenalty = component.Price * (1 - partRatio) * component.MissingBodyPartPenalty; + var partRatio = totalPartsPresent / (double) totalParts; + partPenalty = component.Price * (1 - partRatio) * component.MissingBodyPartPenalty; + } args.Price += (component.Price - partPenalty) * (_mobStateSystem.IsAlive(uid, state) ? 1.0 : component.DeathPenalty); } diff --git a/Content.Server/Chat/Systems/ChatNotificationSystem.cs b/Content.Server/Chat/Systems/ChatNotificationSystem.cs new file mode 100644 index 0000000000..8cd3d54e80 --- /dev/null +++ b/Content.Server/Chat/Systems/ChatNotificationSystem.cs @@ -0,0 +1,105 @@ +using Content.Server.Chat.Managers; +using Content.Shared.Chat; +using Content.Shared.Chat.Prototypes; +using Content.Shared.Mind; +using Content.Shared.Roles; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Server.Chat.Systems; + +/// +/// This system is used to notify specific players of the occurance of predefined events. +/// +public sealed partial class ChatNotificationSystem : EntitySystem +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly IChatManager _chats = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly SharedRoleSystem _roles = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly ILogManager _logManager = default!; + + private ISawmill _sawmill = default!; + + // The following data does not need to be saved + + // Local cache for rate limiting chat notifications by source + // (Recipient, ChatNotification) -> Dictionary + private readonly Dictionary<(EntityUid, ProtoId), Dictionary> _chatNotificationsBySource = new(); + + // Local cache for rate limiting chat notifications by type + // (Recipient, ChatNotification) -> next allowed TOA + private readonly Dictionary<(EntityUid, ProtoId), TimeSpan> _chatNotificationsByType = new(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnChatNotification); + + _sawmill = _logManager.GetSawmill("chatnotification"); + } + + /// + /// Triggered when the specified player recieves a chat notification event. + /// + /// The player receiving the chat notification. + /// The chat notification event + public void OnChatNotification(Entity ent, ref ChatNotificationEvent args) + { + if (!_proto.TryIndex(args.ChatNotification, out var chatNotification)) + { + _sawmill.Warning("Attempted to index ChatNotificationPrototype " + args.ChatNotification + " but the prototype does not exist."); + return; + } + + var source = args.Source; + var playerNotification = (ent, args.ChatNotification); + + // Exit without notifying the player if we received a notification before the appropriate time has elasped + + if (chatNotification.NotifyBySource) + { + if (!_chatNotificationsBySource.TryGetValue(playerNotification, out var trackedSources)) + trackedSources = new(); + + trackedSources.TryGetValue(source, out var timeSpan); + trackedSources[source] = _timing.CurTime + chatNotification.NextDelay; + + _chatNotificationsBySource[playerNotification] = trackedSources; + + if (_timing.CurTime < timeSpan) + return; + } + else + { + _chatNotificationsByType.TryGetValue(playerNotification, out var timeSpan); + _chatNotificationsByType[playerNotification] = _timing.CurTime + chatNotification.NextDelay; + + if (_timing.CurTime < timeSpan) + return; + } + + var sourceName = args.SourceNameOverride ?? Name(source); + var userName = args.UserNameOverride ?? (args.User.HasValue ? Name(args.User.Value) : string.Empty); + var targetName = Name(ent); + + var message = Loc.GetString(chatNotification.Message, ("source", sourceName), ("user", userName), ("target", targetName)); + var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", message)); + + _chats.ChatMessageToOne( + ChatChannel.Notifications, + message, + wrappedMessage, + default, + false, + ent.Comp.PlayerSession.Channel, + colorOverride: chatNotification.Color + ); + + if (chatNotification.Sound != null && _mind.TryGetMind(ent, out var mindId, out _)) + _roles.MindPlaySound(mindId, chatNotification.Sound); + } +} diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index d49da57801..7ff12595d1 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -7,7 +7,6 @@ using Content.Server.Chat.Managers; using Content.Server.GameTicking; using Content.Server.Speech.EntitySystems; using Content.Server.Speech.Prototypes; -using Content.Server.Station.Components; using Content.Server.Station.Systems; using Content.Shared.ActionBlocker; using Content.Shared.Administration; @@ -21,6 +20,7 @@ using Content.Shared.Mobs.Systems; using Content.Shared.Players; using Content.Shared.Players.RateLimiting; using Content.Shared.Radio; +using Content.Shared.Station.Components; using Content.Shared.Whitelist; using Robust.Server.Player; using Robust.Shared.Audio; diff --git a/Content.Server/Cloning/CloningSystem.Subscriptions.cs b/Content.Server/Cloning/CloningSystem.Subscriptions.cs index eba806ceb8..84ef050305 100644 --- a/Content.Server/Cloning/CloningSystem.Subscriptions.cs +++ b/Content.Server/Cloning/CloningSystem.Subscriptions.cs @@ -1,11 +1,16 @@ using Content.Server.Forensics; +using Content.Server.Speech.EntitySystems; using Content.Shared.Cloning.Events; -using Content.Shared.Clothing.Components; using Content.Shared.FixedPoint; +using Content.Shared.Inventory; using Content.Shared.Labels.Components; using Content.Shared.Labels.EntitySystems; +using Content.Shared.Movement.Components; +using Content.Shared.Movement.Systems; using Content.Shared.Paper; using Content.Shared.Stacks; +using Content.Shared.Speech.Components; +using Content.Shared.Storage; using Content.Shared.Store; using Content.Shared.Store.Components; using Robust.Shared.Prototypes; @@ -13,47 +18,58 @@ using Robust.Shared.Prototypes; namespace Content.Server.Cloning; /// -/// The part of item cloning responsible for copying over important components. -/// This is used for . -/// Anything not copied over here gets reverted to the values the item had in its prototype. +/// The part of item cloning responsible for copying over important components. /// /// -/// This method of copying items is of course not perfect as we cannot clone every single component, which would be pretty much impossible with our ECS. -/// We only consider the most important components so the paradox clone gets similar equipment. -/// This method of using subscriptions was chosen to make it easy for forks to add their own custom components that need to be copied. +/// These are all not part of their corresponding systems because we don't want systems every system to depend on a CloningSystem namespace import, which is still heavily coupled to med code. +/// TODO: Create a more generic "CopyEntity" method/event (probably in RT) that doesn't have this problem and then move all these subscriptions. /// -public sealed partial class CloningSystem : EntitySystem +public sealed partial class CloningSystem { [Dependency] private readonly SharedStackSystem _stack = default!; [Dependency] private readonly LabelSystem _label = default!; [Dependency] private readonly ForensicsSystem _forensics = default!; [Dependency] private readonly PaperSystem _paper = default!; + [Dependency] private readonly VocalSystem _vocal = default!; + [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!; public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnCloneStack); - SubscribeLocalEvent(OnCloneLabel); - SubscribeLocalEvent(OnClonePaper); - SubscribeLocalEvent(OnCloneForensics); - SubscribeLocalEvent(OnCloneStore); + // These are used for . + // Anything not copied over here gets reverted to the values the item had in its prototype. + // This method of copying items is of course not perfect as we cannot clone every single component, which would be pretty much impossible with our ECS. + // We only consider the most important components so the paradox clone gets similar equipment. + // This method of using subscriptions was chosen to make it easy for forks to add their own custom components that need to be copied. + SubscribeLocalEvent(OnCloneItemStack); + SubscribeLocalEvent(OnCloneItemLabel); + SubscribeLocalEvent(OnCloneItemPaper); + SubscribeLocalEvent(OnCloneItemForensics); + SubscribeLocalEvent(OnCloneItemStore); + + // These are for cloning components that cannot be cloned using CopyComp. + // Put them into CloningSettingsPrototype.EventComponents to have them be applied to the clone. + SubscribeLocalEvent(OnCloneVocal); + SubscribeLocalEvent(OnCloneStorage); + SubscribeLocalEvent(OnCloneInventory); + SubscribeLocalEvent(OnCloneInventory); } - private void OnCloneStack(Entity ent, ref CloningItemEvent args) + private void OnCloneItemStack(Entity ent, ref CloningItemEvent args) { // if the clone is a stack as well, adjust the count of the copy if (TryComp(args.CloneUid, out var cloneStackComp)) _stack.SetCount(args.CloneUid, ent.Comp.Count, cloneStackComp); } - private void OnCloneLabel(Entity ent, ref CloningItemEvent args) + private void OnCloneItemLabel(Entity ent, ref CloningItemEvent args) { // copy the label _label.Label(args.CloneUid, ent.Comp.CurrentLabel); } - private void OnClonePaper(Entity ent, ref CloningItemEvent args) + private void OnCloneItemPaper(Entity ent, ref CloningItemEvent args) { // copy the text and any stamps if (TryComp(args.CloneUid, out var clonePaperComp)) @@ -63,13 +79,13 @@ public sealed partial class CloningSystem : EntitySystem } } - private void OnCloneForensics(Entity ent, ref CloningItemEvent args) + private void OnCloneItemForensics(Entity ent, ref CloningItemEvent args) { // copy any forensics to the cloned item _forensics.CopyForensicsFrom(ent.Comp, args.CloneUid); } - private void OnCloneStore(Entity ent, ref CloningItemEvent args) + private void OnCloneItemStore(Entity ent, ref CloningItemEvent args) { // copy the current amount of currency in the store // at the moment this takes care of uplink implants and the portable nukie uplinks @@ -80,4 +96,35 @@ public sealed partial class CloningSystem : EntitySystem } } + private void OnCloneVocal(Entity ent, ref CloningEvent args) + { + if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name)) + return; + + _vocal.CopyComponent(ent.AsNullable(), args.CloneUid); + } + + private void OnCloneStorage(Entity ent, ref CloningEvent args) + { + if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name)) + return; + + _storage.CopyComponent(ent.AsNullable(), args.CloneUid); + } + + private void OnCloneInventory(Entity ent, ref CloningEvent args) + { + if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name)) + return; + + _inventory.CopyComponent(ent.AsNullable(), args.CloneUid); + } + + private void OnCloneInventory(Entity ent, ref CloningEvent args) + { + if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name)) + return; + + _movementSpeedModifier.CopyComponent(ent.AsNullable(), args.CloneUid); + } } diff --git a/Content.Server/Cloning/CloningSystem.cs b/Content.Server/Cloning/CloningSystem.cs index 97ab41e7b1..b0d62be523 100644 --- a/Content.Server/Cloning/CloningSystem.cs +++ b/Content.Server/Cloning/CloningSystem.cs @@ -24,7 +24,7 @@ namespace Content.Server.Cloning; /// System responsible for making a copy of a humanoid's body. /// For the cloning machines themselves look at CloningPodSystem, CloningConsoleSystem and MedicalScannerSystem instead. /// -public sealed partial class CloningSystem : EntitySystem +public sealed partial class CloningSystem : SharedCloningSystem { [Dependency] private readonly HumanoidAppearanceSystem _humanoidSystem = default!; [Dependency] private readonly InventorySystem _inventory = default!; @@ -84,13 +84,7 @@ public sealed partial class CloningSystem : EntitySystem return true; } - /// - /// Copy components from one entity to another based on a CloningSettingsPrototype. - /// - /// The orignal Entity to clone components from. - /// The target Entity to clone components to. - /// The clone settings prototype containing the list of components to clone. - public void CloneComponents(EntityUid original, EntityUid clone, CloningSettingsPrototype settings) + public override void CloneComponents(EntityUid original, EntityUid clone, CloningSettingsPrototype settings) { var componentsToCopy = settings.Components; var componentsToEvent = settings.EventComponents; @@ -128,7 +122,8 @@ public sealed partial class CloningSystem : EntitySystem } // If the original does not have the component, then the clone shouldn't have it either. - RemComp(clone, componentRegistration.Type); + if (!HasComp(original, componentRegistration.Type)) + RemComp(clone, componentRegistration.Type); } var cloningEv = new CloningEvent(settings, clone); diff --git a/Content.Server/Commands/ContentCompletionHelper.cs b/Content.Server/Commands/ContentCompletionHelper.cs index f843d84a62..bc80b28762 100644 --- a/Content.Server/Commands/ContentCompletionHelper.cs +++ b/Content.Server/Commands/ContentCompletionHelper.cs @@ -1,4 +1,4 @@ -using Content.Server.Station.Components; +using Content.Shared.Station.Components; using Robust.Shared.Console; namespace Content.Server.Commands; diff --git a/Content.Server/CrewManifest/CrewManifestSystem.cs b/Content.Server/CrewManifest/CrewManifestSystem.cs index d8e0858ce0..448e2fff20 100644 --- a/Content.Server/CrewManifest/CrewManifestSystem.cs +++ b/Content.Server/CrewManifest/CrewManifestSystem.cs @@ -1,7 +1,6 @@ using System.Linq; using Content.Server.Administration; using Content.Server.EUI; -using Content.Server.Station.Components; using Content.Server.Station.Systems; using Content.Server.StationRecords; using Content.Server.StationRecords.Systems; @@ -10,12 +9,12 @@ using Content.Shared.CCVar; using Content.Shared.CrewManifest; using Content.Shared.GameTicking; using Content.Shared.Roles; +using Content.Shared.Station.Components; using Content.Shared.StationRecords; using Robust.Shared.Configuration; using Robust.Shared.Console; using Robust.Shared.Player; using Robust.Shared.Prototypes; -using Robust.Shared.Utility; namespace Content.Server.CrewManifest; diff --git a/Content.Server/Emoting/Components/BodyEmotesComponent.cs b/Content.Server/Emoting/Components/BodyEmotesComponent.cs index 3fd71def0d..d911a89ec9 100644 --- a/Content.Server/Emoting/Components/BodyEmotesComponent.cs +++ b/Content.Server/Emoting/Components/BodyEmotesComponent.cs @@ -1,6 +1,6 @@ using Content.Server.Emoting.Systems; using Content.Shared.Chat.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Prototypes; namespace Content.Server.Emoting.Components; @@ -14,11 +14,6 @@ public sealed partial class BodyEmotesComponent : Component /// /// Emote sounds prototype id for body emotes. /// - [DataField("soundsId", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string? SoundsId; - - /// - /// Loaded emote sounds prototype used for body emotes. - /// - public EmoteSoundsPrototype? Sounds; + [DataField] + public ProtoId? SoundsId; } diff --git a/Content.Server/Emoting/Systems/BodyEmotesSystem.cs b/Content.Server/Emoting/Systems/BodyEmotesSystem.cs index 594eb0ec6d..aef79f1419 100644 --- a/Content.Server/Emoting/Systems/BodyEmotesSystem.cs +++ b/Content.Server/Emoting/Systems/BodyEmotesSystem.cs @@ -14,15 +14,8 @@ public sealed class BodyEmotesSystem : EntitySystem public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnStartup); - SubscribeLocalEvent(OnEmote); - } - private void OnStartup(EntityUid uid, BodyEmotesComponent component, ComponentStartup args) - { - if (component.SoundsId == null) - return; - _proto.TryIndex(component.SoundsId, out component.Sounds); + SubscribeLocalEvent(OnEmote); } private void OnEmote(EntityUid uid, BodyEmotesComponent component, ref EmoteEvent args) @@ -43,6 +36,9 @@ public sealed class BodyEmotesSystem : EntitySystem if (!TryComp(uid, out HandsComponent? hands) || hands.Count <= 0) return false; - return _chat.TryPlayEmoteSound(uid, component.Sounds, emote); + if (!_proto.Resolve(component.SoundsId, out var sounds)) + return false; + + return _chat.TryPlayEmoteSound(uid, sounds, emote); } } diff --git a/Content.Server/Forensics/Systems/ForensicsSystem.cs b/Content.Server/Forensics/Systems/ForensicsSystem.cs index 9f94e39fb7..cc74c1d141 100644 --- a/Content.Server/Forensics/Systems/ForensicsSystem.cs +++ b/Content.Server/Forensics/Systems/ForensicsSystem.cs @@ -12,6 +12,7 @@ using Content.Shared.Chemistry.Components.SolutionManager; using Content.Shared.DoAfter; using Content.Shared.Forensics; using Content.Shared.Forensics.Components; +using Content.Shared.Forensics.Systems; using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Inventory; @@ -23,7 +24,7 @@ using Content.Shared.Hands.Components; namespace Content.Server.Forensics { - public sealed class ForensicsSystem : EntitySystem + public sealed class ForensicsSystem : SharedForensicsSystem { [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly InventorySystem _inventory = default!; @@ -317,12 +318,7 @@ namespace Content.Server.Forensics } #region Public API - - /// - /// Give the entity a new, random DNA string and call an event to notify other systems like the bloodstream that it has been changed. - /// Does nothing if it does not have the DnaComponent. - /// - public void RandomizeDNA(Entity ent) + public override void RandomizeDNA(Entity ent) { if (!Resolve(ent, ref ent.Comp, false)) return; @@ -334,11 +330,7 @@ namespace Content.Server.Forensics RaiseLocalEvent(ent.Owner, ref ev); } - /// - /// Give the entity a new, random fingerprint string. - /// Does nothing if it does not have the FingerprintComponent. - /// - public void RandomizeFingerprint(Entity ent) + public override void RandomizeFingerprint(Entity ent) { if (!Resolve(ent, ref ent.Comp, false)) return; diff --git a/Content.Server/GameTicking/Rules/ChangelingRuleSystem.cs b/Content.Server/GameTicking/Rules/ChangelingRuleSystem.cs new file mode 100644 index 0000000000..a64b0e904a --- /dev/null +++ b/Content.Server/GameTicking/Rules/ChangelingRuleSystem.cs @@ -0,0 +1,23 @@ +using Content.Server.GameTicking.Rules.Components; +using Content.Server.Roles; +using Content.Shared.Changeling; + +namespace Content.Server.GameTicking.Rules; + +/// +/// Game rule system for Changelings +/// +public sealed class ChangelingRuleSystem : GameRuleSystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGetBriefing); + } + + private void OnGetBriefing(Entity ent, ref GetBriefingEvent args) + { + args.Append(Loc.GetString("changeling-briefing")); + } +} diff --git a/Content.Server/GameTicking/Rules/Components/ChangelingRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/ChangelingRuleComponent.cs new file mode 100644 index 0000000000..13891c1988 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Components/ChangelingRuleComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Server.GameTicking.Rules.Components; + +/// +/// Gamerule component for handling a changeling antagonist. +/// +[RegisterComponent] +public sealed partial class ChangelingRuleComponent : Component; diff --git a/Content.Server/GameTicking/Rules/Components/ParadoxCloneRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/ParadoxCloneRuleComponent.cs index e28a0bc35f..f1e8e41258 100644 --- a/Content.Server/GameTicking/Rules/Components/ParadoxCloneRuleComponent.cs +++ b/Content.Server/GameTicking/Rules/Components/ParadoxCloneRuleComponent.cs @@ -14,7 +14,7 @@ public sealed partial class ParadoxCloneRuleComponent : Component /// Cloning settings to be used. /// [DataField] - public ProtoId Settings = "Antag"; + public ProtoId Settings = "ParadoxCloningSettings"; /// /// Visual effect spawned when gibbing at round end. diff --git a/Content.Server/GameTicking/Rules/DragonRuleSystem.cs b/Content.Server/GameTicking/Rules/DragonRuleSystem.cs index de13218568..964b248beb 100644 --- a/Content.Server/GameTicking/Rules/DragonRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/DragonRuleSystem.cs @@ -1,11 +1,8 @@ using Content.Server.Antag; -using Content.Server.Dragon; using Content.Server.GameTicking.Rules.Components; using Content.Server.Mind; using Content.Server.Roles; -using Content.Server.Station.Components; using Content.Server.Station.Systems; -using Content.Shared.CharacterInfo; using Content.Shared.Localizations; using Robust.Server.GameObjects; @@ -56,10 +53,9 @@ public sealed class DragonRuleSystem : GameRuleSystem var dragonXform = Transform(dragon); - var station = _station.GetStationInMap(dragonXform.MapID); EntityUid? stationGrid = null; - if (TryComp(station, out var stationData)) - stationGrid = _station.GetLargestGrid(stationData); + if (_station.GetStationInMap(dragonXform.MapID) is { } station) + stationGrid = _station.GetLargestGrid(station); if (stationGrid is not null) { diff --git a/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs b/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs index 33ee91f8a5..cd93eae502 100644 --- a/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs +++ b/Content.Server/GameTicking/Rules/GameRuleSystem.Utility.cs @@ -1,14 +1,12 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; -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 Content.Shared.Station.Components; using Robust.Shared.Collections; using Robust.Shared.Map; using Robust.Shared.Map.Components; -using Robust.Shared.Random; namespace Content.Server.GameTicking.Rules; diff --git a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs index f687c9dcc7..17d9e4c847 100644 --- a/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs @@ -24,6 +24,7 @@ using Robust.Shared.Map; using Robust.Shared.Random; using Robust.Shared.Utility; using System.Linq; +using Content.Shared.Station.Components; using Content.Shared.Store.Components; using Robust.Shared.Prototypes; diff --git a/Content.Server/GameTicking/Rules/RoundstartStationVariationRuleSystem.cs b/Content.Server/GameTicking/Rules/RoundstartStationVariationRuleSystem.cs index 570889155b..66f0d1f22c 100644 --- a/Content.Server/GameTicking/Rules/RoundstartStationVariationRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/RoundstartStationVariationRuleSystem.cs @@ -1,11 +1,10 @@ -using System.Linq; -using Content.Server.GameTicking.Rules.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.Station.Components; using Content.Shared.Storage; -using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.Server.GameTicking.Rules; diff --git a/Content.Server/GameTicking/Rules/VariationPass/BaseEntityReplaceVariationPassSystem.cs b/Content.Server/GameTicking/Rules/VariationPass/BaseEntityReplaceVariationPassSystem.cs index 00b2546e78..1a5b97c327 100644 --- a/Content.Server/GameTicking/Rules/VariationPass/BaseEntityReplaceVariationPassSystem.cs +++ b/Content.Server/GameTicking/Rules/VariationPass/BaseEntityReplaceVariationPassSystem.cs @@ -58,7 +58,7 @@ public abstract class BaseEntityReplaceVariationPassSystem ent, List replacements) diff --git a/Content.Server/GameTicking/Rules/VariationPass/EntitySpawnVariationPassSystem.cs b/Content.Server/GameTicking/Rules/VariationPass/EntitySpawnVariationPassSystem.cs index 7247bd98aa..462c89e016 100644 --- a/Content.Server/GameTicking/Rules/VariationPass/EntitySpawnVariationPassSystem.cs +++ b/Content.Server/GameTicking/Rules/VariationPass/EntitySpawnVariationPassSystem.cs @@ -9,7 +9,7 @@ public sealed class EntitySpawnVariationPassSystem : VariationPassSystem ent, ref StationVariationPassEvent args) { - var totalTiles = Stations.GetTileCount(args.Station); + var totalTiles = Stations.GetTileCount(args.Station.AsNullable()); var dirtyMod = Random.NextGaussian(ent.Comp.TilesPerEntityAverage, ent.Comp.TilesPerEntityStdDev); var trashTiles = Math.Max((int) (totalTiles * (1 / dirtyMod)), 0); diff --git a/Content.Server/GameTicking/Rules/VariationPass/PuddleMessVariationPassSystem.cs b/Content.Server/GameTicking/Rules/VariationPass/PuddleMessVariationPassSystem.cs index 41cdbd87f8..2895416a7f 100644 --- a/Content.Server/GameTicking/Rules/VariationPass/PuddleMessVariationPassSystem.cs +++ b/Content.Server/GameTicking/Rules/VariationPass/PuddleMessVariationPassSystem.cs @@ -15,7 +15,7 @@ public sealed class PuddleMessVariationPassSystem : VariationPassSystem ent, ref StationVariationPassEvent args) { - var totalTiles = Stations.GetTileCount(args.Station); + var totalTiles = Stations.GetTileCount(args.Station.AsNullable()); if (!_proto.TryIndex(ent.Comp.RandomPuddleSolutionFill, out var proto)) return; diff --git a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs index a38940c667..9fab98446a 100644 --- a/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs +++ b/Content.Server/GameTicking/Rules/ZombieRuleSystem.cs @@ -4,7 +4,6 @@ using Content.Server.GameTicking.Rules.Components; using Content.Server.Popups; using Content.Server.Roles; using Content.Server.RoundEnd; -using Content.Server.Station.Components; using Content.Server.Station.Systems; using Content.Server.Zombies; using Content.Shared.GameTicking.Components; @@ -190,7 +189,7 @@ public sealed class ZombieRuleSystem : GameRuleSystem { foreach (var station in _station.GetStationsSet()) { - if (TryComp(station, out var data) && _station.GetLargestGrid(data) is { } grid) + if (_station.GetLargestGrid(station) is { } grid) stationGrids.Add(grid); } } diff --git a/Content.Server/Implants/Components/ScramImplantComponent.cs b/Content.Server/Implants/Components/ScramImplantComponent.cs deleted file mode 100644 index f3bbc9e584..0000000000 --- a/Content.Server/Implants/Components/ScramImplantComponent.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Content.Server.Implants; -using Robust.Shared.Audio; - -namespace Content.Server.Implants.Components; - -/// -/// Randomly teleports entity when triggered. -/// -[RegisterComponent] -public sealed partial class ScramImplantComponent : Component -{ - /// - /// Up to how far to teleport the user - /// - [DataField, ViewVariables(VVAccess.ReadWrite)] - public float TeleportRadius = 100f; - - [DataField, ViewVariables(VVAccess.ReadWrite)] - public SoundSpecifier TeleportSound = new SoundPathSpecifier("/Audio/Effects/teleport_arrival.ogg"); -} diff --git a/Content.Server/Implants/SubdermalImplantSystem.cs b/Content.Server/Implants/SubdermalImplantSystem.cs index e2482b7b60..f0530358a6 100644 --- a/Content.Server/Implants/SubdermalImplantSystem.cs +++ b/Content.Server/Implants/SubdermalImplantSystem.cs @@ -1,66 +1,21 @@ -using Content.Server.Cuffs; -using Content.Server.Forensics; -using Content.Server.Humanoid; -using Content.Server.Implants.Components; using Content.Server.Store.Components; using Content.Server.Store.Systems; -using Content.Shared.Cuffs.Components; -using Content.Shared.Forensics; -using Content.Shared.Forensics.Components; -using Content.Shared.Humanoid; using Content.Shared.Implants; -using Content.Shared.Implants.Components; using Content.Shared.Interaction; -using Content.Shared.Physics; using Content.Shared.Popups; -using Content.Shared.Preferences; -using Robust.Shared.Audio.Systems; -using Robust.Shared.Map; -using Robust.Shared.Physics; -using Robust.Shared.Physics.Components; -using Robust.Shared.Random; -using System.Numerics; -using Content.Shared.Movement.Pulling.Components; -using Content.Shared.Movement.Pulling.Systems; -using Content.Server.IdentityManagement; -using Content.Shared.DetailExaminable; using Content.Shared.Store.Components; -using Robust.Shared.Collections; -using Robust.Shared.Map.Components; namespace Content.Server.Implants; public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem { - [Dependency] private readonly CuffableSystem _cuffable = default!; - [Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!; - [Dependency] private readonly IRobustRandom _random = default!; - [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly StoreSystem _store = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; - [Dependency] private readonly SharedTransformSystem _xform = default!; - [Dependency] private readonly ForensicsSystem _forensicsSystem = default!; - [Dependency] private readonly PullingSystem _pullingSystem = default!; - [Dependency] private readonly EntityLookupSystem _lookupSystem = default!; - [Dependency] private readonly SharedMapSystem _mapSystem = default!; - [Dependency] private readonly IdentitySystem _identity = default!; - - private EntityQuery _physicsQuery; - private HashSet> _targetGrids = []; - public override void Initialize() { base.Initialize(); - _physicsQuery = GetEntityQuery(); - - SubscribeLocalEvent(OnFreedomImplant); SubscribeLocalEvent>(OnStoreRelay); - SubscribeLocalEvent(OnActivateImplantEvent); - SubscribeLocalEvent(OnScramImplant); - SubscribeLocalEvent(OnDnaScramblerImplant); - } private void OnStoreRelay(EntityUid uid, StoreComponent store, ImplantRelayEvent implantRelay) @@ -85,148 +40,4 @@ public sealed class SubdermalImplantSystem : SharedSubdermalImplantSystem var msg = Loc.GetString("store-currency-inserted-implant", ("used", args.Used)); _popup.PopupEntity(msg, args.User, args.User); } - - private void OnFreedomImplant(EntityUid uid, SubdermalImplantComponent component, UseFreedomImplantEvent args) - { - if (!TryComp(component.ImplantedEntity, out var cuffs) || cuffs.Container.ContainedEntities.Count < 1) - return; - - _cuffable.Uncuff(component.ImplantedEntity.Value, cuffs.LastAddedCuffs, cuffs.LastAddedCuffs); - args.Handled = true; - } - - private void OnActivateImplantEvent(EntityUid uid, SubdermalImplantComponent component, ActivateImplantEvent args) - { - args.Handled = true; - } - - private void OnScramImplant(EntityUid uid, SubdermalImplantComponent component, UseScramImplantEvent args) - { - if (component.ImplantedEntity is not { } ent) - return; - - if (!TryComp(uid, out var implant)) - return; - - // We need stop the user from being pulled so they don't just get "attached" with whoever is pulling them. - // This can for example happen when the user is cuffed and being pulled. - if (TryComp(ent, out var pull) && _pullingSystem.IsPulled(ent, pull)) - _pullingSystem.TryStopPull(ent, pull); - - // Check if the user is pulling anything, and drop it if so - if (TryComp(ent, out var puller) && TryComp(puller.Pulling, out var pullable)) - _pullingSystem.TryStopPull(puller.Pulling.Value, pullable); - - var xform = Transform(ent); - var targetCoords = SelectRandomTileInRange(xform, implant.TeleportRadius); - - if (targetCoords != null) - { - _xform.SetCoordinates(ent, targetCoords.Value); - _audio.PlayPvs(implant.TeleportSound, ent); - args.Handled = true; - } - } - - private EntityCoordinates? SelectRandomTileInRange(TransformComponent userXform, float radius) - { - var userCoords = _xform.ToMapCoordinates(userXform.Coordinates); - _targetGrids.Clear(); - _lookupSystem.GetEntitiesInRange(userCoords, radius, _targetGrids); - Entity? targetGrid = null; - - if (_targetGrids.Count == 0) - return null; - - // Give preference to the grid the entity is currently on. - // This does not guarantee that if the probability fails that the owner's grid won't be picked. - // In reality the probability is higher and depends on the number of grids. - if (userXform.GridUid != null && TryComp(userXform.GridUid, out var gridComp)) - { - var userGrid = new Entity(userXform.GridUid.Value, gridComp); - if (_random.Prob(0.5f)) - { - _targetGrids.Remove(userGrid); - targetGrid = userGrid; - } - } - - if (targetGrid == null) - targetGrid = _random.GetRandom().PickAndTake(_targetGrids); - - EntityCoordinates? targetCoords = null; - - do - { - var valid = false; - - var range = (float) Math.Sqrt(radius); - var box = Box2.CenteredAround(userCoords.Position, new Vector2(range, range)); - var tilesInRange = _mapSystem.GetTilesEnumerator(targetGrid.Value.Owner, targetGrid.Value.Comp, box, false); - var tileList = new ValueList(); - - while (tilesInRange.MoveNext(out var tile)) - { - tileList.Add(tile.GridIndices); - } - - while (tileList.Count != 0) - { - var tile = tileList.RemoveSwap(_random.Next(tileList.Count)); - valid = true; - foreach (var entity in _mapSystem.GetAnchoredEntities(targetGrid.Value.Owner, targetGrid.Value.Comp, - tile)) - { - if (!_physicsQuery.TryGetComponent(entity, out var body)) - continue; - - if (body.BodyType != BodyType.Static || - !body.Hard || - (body.CollisionLayer & (int) CollisionGroup.MobMask) == 0) - continue; - - valid = false; - break; - } - - if (valid) - { - targetCoords = new EntityCoordinates(targetGrid.Value.Owner, - _mapSystem.TileCenterToVector(targetGrid.Value, tile)); - break; - } - } - - if (valid || _targetGrids.Count == 0) // if we don't do the check here then PickAndTake will blow up on an empty set. - break; - - targetGrid = _random.GetRandom().PickAndTake(_targetGrids); - } while (true); - - return targetCoords; - } - - private void OnDnaScramblerImplant(EntityUid uid, SubdermalImplantComponent component, UseDnaScramblerImplantEvent args) - { - if (component.ImplantedEntity is not { } ent) - return; - - if (TryComp(ent, out var humanoid)) - { - var newProfile = HumanoidCharacterProfile.RandomWithSpecies(humanoid.Species); - _humanoidAppearance.LoadProfile(ent, newProfile, humanoid); - _metaData.SetEntityName(ent, newProfile.Name, raiseEvents: false); // raising events would update ID card, station record, etc. - - // If the entity has the respecive components, then scramble the dna and fingerprint strings - _forensicsSystem.RandomizeDNA(ent); - _forensicsSystem.RandomizeFingerprint(ent); - - RemComp(ent); // remove MRP+ custom description if one exists - _identity.QueueIdentityUpdate(ent); // manually queue identity update since we don't raise the event - _popup.PopupEntity(Loc.GetString("scramble-implant-activated-popup"), ent, ent); - } - - args.Handled = true; - QueueDel(uid); - } } diff --git a/Content.Server/Kitchen/EntitySystems/SharpSystem.cs b/Content.Server/Kitchen/EntitySystems/SharpSystem.cs index 0ba9d0990a..0275e4d1a7 100644 --- a/Content.Server/Kitchen/EntitySystems/SharpSystem.cs +++ b/Content.Server/Kitchen/EntitySystems/SharpSystem.cs @@ -117,19 +117,18 @@ public sealed class SharpSystem : EntitySystem popupEnt = Spawn(proto, coords.Offset(_robustRandom.NextVector2(0.25f))); } - var hasBody = TryComp(args.Args.Target.Value, out var body); - // only show a big popup when butchering living things. - var popupType = PopupType.Small; - if (hasBody) - popupType = PopupType.LargeCaution; + // Meant to differentiate cutting up clothes and cutting up your boss. + var popupType = HasComp(args.Args.Target.Value) + ? PopupType.LargeCaution + : PopupType.Small; _popupSystem.PopupEntity(Loc.GetString("butcherable-knife-butchered-success", ("target", args.Args.Target.Value), ("knife", Identity.Entity(uid, EntityManager))), - popupEnt, args.Args.User, popupType); - - if (hasBody) - _bodySystem.GibBody(args.Args.Target.Value, body: body); + popupEnt, + args.Args.User, + popupType); + _bodySystem.GibBody(args.Args.Target.Value); // does nothing if ent can't be gibbed _destructibleSystem.DestroyEntity(args.Args.Target.Value); args.Handled = true; diff --git a/Content.Server/Maps/GameMapPrototype.cs b/Content.Server/Maps/GameMapPrototype.cs index f0f40fe06d..df9046652b 100644 --- a/Content.Server/Maps/GameMapPrototype.cs +++ b/Content.Server/Maps/GameMapPrototype.cs @@ -1,9 +1,8 @@ -using Content.Server.Station; using JetBrains.Annotations; using Robust.Shared.Prototypes; using Robust.Shared.Utility; using System.Diagnostics; -using System.Numerics; +using Content.Shared.Station; namespace Content.Server.Maps; diff --git a/Content.Server/Mind/Filters/TargetObjectiveMindFilter.cs b/Content.Server/Mind/Filters/TargetObjectiveMindFilter.cs new file mode 100644 index 0000000000..dae8079ac1 --- /dev/null +++ b/Content.Server/Mind/Filters/TargetObjectiveMindFilter.cs @@ -0,0 +1,43 @@ +using Content.Server.Objectives.Components; +using Content.Shared.Mind; +using Content.Shared.Mind.Filters; +using Content.Shared.Whitelist; + +namespace Content.Server.Mind.Filters; + +/// +/// A mind filter that removes minds if you have an objective targeting them matching a blacklist. +/// +/// +/// Used to prevent assigning multiple kill objectives for the same person. +/// +public sealed partial class TargetObjectiveMindFilter : MindFilter +{ + /// + /// A blacklist to check objectives against, for removing a mind. + /// If null then any objective targeting it will remove minds. + /// + [DataField] + public EntityWhitelist? Blacklist; + + protected override bool ShouldRemove(Entity mind, EntityUid? excluded, IEntityManager entMan, SharedMindSystem mindSys) + { + // ignore this filter if there is no user to check + if (!entMan.TryGetComponent(excluded, out var excludedMind)) + return false; + + var whitelistSys = entMan.System(); + foreach (var objective in excludedMind.Objectives) + { + // if the player has an objective targeting this mind + if (entMan.TryGetComponent(objective, out var kill) && kill.Target == mind.Owner) + { + // remove the mind if this objective is blacklisted + if (whitelistSys.IsBlacklistPassOrNull(Blacklist, objective)) + return true; + } + } + + return false; + } +} diff --git a/Content.Server/Morgue/MorgueSystem.cs b/Content.Server/Morgue/MorgueSystem.cs index a07accf777..92ed16a06b 100644 --- a/Content.Server/Morgue/MorgueSystem.cs +++ b/Content.Server/Morgue/MorgueSystem.cs @@ -1,6 +1,6 @@ using Content.Server.Storage.Components; -using Content.Shared.Body.Components; using Content.Shared.Examine; +using Content.Shared.Mobs.Components; using Content.Shared.Morgue; using Content.Shared.Morgue.Components; using Robust.Shared.Audio.Systems; @@ -59,7 +59,7 @@ public sealed class MorgueSystem : EntitySystem foreach (var ent in storage.Contents.ContainedEntities) { - if (!hasMob && HasComp(ent)) + if (!hasMob && HasComp(ent)) hasMob = true; if (HasComp(ent)) diff --git a/Content.Server/Nuke/Commands/SendNukeCodesCommand.cs b/Content.Server/Nuke/Commands/SendNukeCodesCommand.cs index 8ac4f95a97..e44811a835 100644 --- a/Content.Server/Nuke/Commands/SendNukeCodesCommand.cs +++ b/Content.Server/Nuke/Commands/SendNukeCodesCommand.cs @@ -1,6 +1,6 @@ using Content.Server.Administration; -using Content.Server.Station.Components; using Content.Shared.Administration; +using Content.Shared.Station.Components; using Robust.Shared.Console; namespace Content.Server.Nuke.Commands; diff --git a/Content.Server/Nuke/NukeCodePaperSystem.cs b/Content.Server/Nuke/NukeCodePaperSystem.cs index aac2d2361d..af6751ed71 100644 --- a/Content.Server/Nuke/NukeCodePaperSystem.cs +++ b/Content.Server/Nuke/NukeCodePaperSystem.cs @@ -2,9 +2,9 @@ using System.Diagnostics.CodeAnalysis; using Content.Server.Chat.Systems; using Content.Server.Fax; using Content.Shared.Fax.Components; -using Content.Server.Station.Components; using Content.Server.Station.Systems; using Content.Shared.Paper; +using Content.Shared.Station.Components; using Robust.Shared.Random; using Robust.Shared.Utility; diff --git a/Content.Server/Objectives/Components/PickRandomHeadComponent.cs b/Content.Server/Objectives/Components/PickRandomHeadComponent.cs deleted file mode 100644 index 38ed252264..0000000000 --- a/Content.Server/Objectives/Components/PickRandomHeadComponent.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Content.Server.Objectives.Components; - -/// -/// Sets the target for to a random head. -/// If there are no heads it will fallback to any person. -/// -[RegisterComponent] -public sealed partial class PickRandomHeadComponent : Component; diff --git a/Content.Server/Objectives/Components/PickRandomPersonComponent.cs b/Content.Server/Objectives/Components/PickRandomPersonComponent.cs index bf4135e2a9..2c864a80d4 100644 --- a/Content.Server/Objectives/Components/PickRandomPersonComponent.cs +++ b/Content.Server/Objectives/Components/PickRandomPersonComponent.cs @@ -1,7 +1,26 @@ +using Content.Server.Objectives.Systems; +using Content.Shared.Mind.Filters; + namespace Content.Server.Objectives.Components; /// -/// Sets the target for to a random person. +/// Sets the target for to a random person from a pool and filters. /// -[RegisterComponent] -public sealed partial class PickRandomPersonComponent : Component; +/// +/// Don't copy paste this for a new objective, if you need a new filter just make a new filter and set it in YAML. +/// +[RegisterComponent, Access(typeof(PickObjectiveTargetSystem))] +public sealed partial class PickRandomPersonComponent : Component +{ + /// + /// A pool to pick potential targets from. + /// + [DataField] + public IMindPool Pool = new AliveHumansPool(); + + /// + /// Filters to apply to . + /// + [DataField] + public List Filters = new(); +} diff --git a/Content.Server/Objectives/Components/RandomTraitorAliveComponent.cs b/Content.Server/Objectives/Components/RandomTraitorAliveComponent.cs deleted file mode 100644 index 1c45cb45d5..0000000000 --- a/Content.Server/Objectives/Components/RandomTraitorAliveComponent.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Server.Objectives.Components; - -/// -/// Sets the target for to a random traitor. -/// -[RegisterComponent] -public sealed partial class RandomTraitorAliveComponent : Component; diff --git a/Content.Server/Objectives/Components/RandomTraitorProgressComponent.cs b/Content.Server/Objectives/Components/RandomTraitorProgressComponent.cs deleted file mode 100644 index f2da9778eb..0000000000 --- a/Content.Server/Objectives/Components/RandomTraitorProgressComponent.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Server.Objectives.Components; - -/// -/// Sets the target for to a random traitor. -/// -[RegisterComponent] -public sealed partial class RandomTraitorProgressComponent : Component; diff --git a/Content.Server/Objectives/Systems/PickObjectiveTargetSystem.cs b/Content.Server/Objectives/Systems/PickObjectiveTargetSystem.cs index 2977e10569..0fa20c27e5 100644 --- a/Content.Server/Objectives/Systems/PickObjectiveTargetSystem.cs +++ b/Content.Server/Objectives/Systems/PickObjectiveTargetSystem.cs @@ -25,10 +25,6 @@ public sealed class PickObjectiveTargetSystem : EntitySystem SubscribeLocalEvent(OnSpecificPersonAssigned); SubscribeLocalEvent(OnRandomPersonAssigned); - SubscribeLocalEvent(OnRandomHeadAssigned); - - SubscribeLocalEvent(OnRandomTraitorProgressAssigned); - SubscribeLocalEvent(OnRandomTraitorAliveAssigned); } private void OnSpecificPersonAssigned(Entity ent, ref ObjectiveAssignedEvent args) @@ -63,7 +59,7 @@ public sealed class PickObjectiveTargetSystem : EntitySystem private void OnRandomPersonAssigned(Entity ent, ref ObjectiveAssignedEvent args) { // invalid objective prototype - if (!TryComp(ent.Owner, out var target)) + if (!TryComp(ent, out var target)) { args.Cancelled = true; return; @@ -73,140 +69,13 @@ public sealed class PickObjectiveTargetSystem : EntitySystem if (target.Target != null) return; - var allHumans = _mind.GetAliveHumans(args.MindId); - - // Can't have multiple objectives to kill the same person - foreach (var objective in args.Mind.Objectives) - { - if (HasComp(objective) && TryComp(objective, out var kill)) - { - allHumans.RemoveWhere(x => x.Owner == kill.Target); - } - } - - // no other humans to kill - if (allHumans.Count == 0) + // couldn't find a target :( + if (_mind.PickFromPool(ent.Comp.Pool, ent.Comp.Filters, args.MindId) is not {} picked) { args.Cancelled = true; return; } - _target.SetTarget(ent.Owner, _random.Pick(allHumans), target); - } - - private void OnRandomHeadAssigned(Entity ent, ref ObjectiveAssignedEvent args) - { - // invalid prototype - if (!TryComp(ent.Owner, out var target)) - { - args.Cancelled = true; - return; - } - - // target already assigned - if (target.Target != null) - return; - - // no other humans to kill - var allHumans = _mind.GetAliveHumans(args.MindId); - if (allHumans.Count == 0) - { - args.Cancelled = true; - return; - } - - var allHeads = new HashSet>(); - foreach (var person in allHumans) - { - if (TryComp(person, out var mind) && mind.OwnedEntity is { } owned && HasComp(owned)) - allHeads.Add(person); - } - - if (allHeads.Count == 0) - allHeads = allHumans; // fallback to non-head target - - _target.SetTarget(ent.Owner, _random.Pick(allHeads), target); - } - - private void OnRandomTraitorProgressAssigned(Entity ent, ref ObjectiveAssignedEvent args) - { - // invalid prototype - if (!TryComp(ent.Owner, out var target)) - { - args.Cancelled = true; - return; - } - - var traitors = _traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind).ToHashSet(); - - // cant help anyone who is tasked with helping: - // 1. thats boring - // 2. no cyclic progress dependencies!!! - foreach (var traitor in traitors) - { - // TODO: replace this with TryComp(traitor) or something when objectives are moved out of mind - if (!TryComp(traitor.Id, out var mind)) - continue; - - foreach (var objective in mind.Objectives) - { - if (HasComp(objective)) - traitors.RemoveWhere(x => x.Mind == mind); - } - } - - // Can't have multiple objectives to help/save the same person - foreach (var objective in args.Mind.Objectives) - { - if (HasComp(objective) || HasComp(objective)) - { - if (TryComp(objective, out var help)) - { - traitors.RemoveWhere(x => x.Id == help.Target); - } - } - } - - // no more helpable traitors - if (traitors.Count == 0) - { - args.Cancelled = true; - return; - } - - _target.SetTarget(ent.Owner, _random.Pick(traitors).Id, target); - } - - private void OnRandomTraitorAliveAssigned(Entity ent, ref ObjectiveAssignedEvent args) - { - // invalid prototype - if (!TryComp(ent.Owner, out var target)) - { - args.Cancelled = true; - return; - } - - var traitors = _traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind).ToHashSet(); - - // Can't have multiple objectives to help/save the same person - foreach (var objective in args.Mind.Objectives) - { - if (HasComp(objective) || HasComp(objective)) - { - if (TryComp(objective, out var help)) - { - traitors.RemoveWhere(x => x.Id == help.Target); - } - } - } - - // You are the first/only traitor. - if (traitors.Count == 0) - { - args.Cancelled = true; - return; - } - - _target.SetTarget(ent.Owner, _random.Pick(traitors).Id, target); + _target.SetTarget(ent, picked, target); } } diff --git a/Content.Server/Polymorph/Systems/PolymorphSystem.Map.cs b/Content.Server/Polymorph/Systems/PolymorphSystem.Map.cs index 1db624439a..fd24ad9774 100644 --- a/Content.Server/Polymorph/Systems/PolymorphSystem.Map.cs +++ b/Content.Server/Polymorph/Systems/PolymorphSystem.Map.cs @@ -31,7 +31,9 @@ public sealed partial class PolymorphSystem if (PausedMap != null && Exists(PausedMap)) return; - PausedMap = _map.CreateMap(); - _map.SetPaused(PausedMap.Value, true); + var mapUid = _map.CreateMap(); + _metaData.SetEntityName(mapUid, Loc.GetString("polymorph-paused-map-name")); + _map.SetPaused(mapUid, true); + PausedMap = mapUid; } } diff --git a/Content.Server/RoundEnd/RoundEndSystem.cs b/Content.Server/RoundEnd/RoundEndSystem.cs index 2d4f582bfc..a464796a74 100644 --- a/Content.Server/RoundEnd/RoundEndSystem.cs +++ b/Content.Server/RoundEnd/RoundEndSystem.cs @@ -9,7 +9,6 @@ using Content.Server.GameTicking; using Content.Server.Screens.Components; using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Systems; -using Content.Server.Station.Components; using Content.Server.Station.Systems; using Content.Shared.Database; using Content.Shared.DeviceNetwork; @@ -20,6 +19,7 @@ using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Timing; using Content.Shared.DeviceNetwork.Components; +using Content.Shared.Station.Components; using Timer = Robust.Shared.Timing.Timer; namespace Content.Server.RoundEnd @@ -97,10 +97,10 @@ namespace Content.Server.RoundEnd /// public EntityUid? GetStation() { - AllEntityQuery().MoveNext(out _, out _, out var data); + AllEntityQuery().MoveNext(out var uid, out _, out var data); if (data == null) return null; - var targetGrid = _stationSystem.GetLargestGrid(data); + var targetGrid = _stationSystem.GetLargestGrid((uid, data)); return targetGrid == null ? null : Transform(targetGrid.Value).MapUid; } diff --git a/Content.Server/Salvage/SalvageSystem.Runner.cs b/Content.Server/Salvage/SalvageSystem.Runner.cs index 9050db3971..ceee9c3784 100644 --- a/Content.Server/Salvage/SalvageSystem.Runner.cs +++ b/Content.Server/Salvage/SalvageSystem.Runner.cs @@ -2,7 +2,6 @@ using System.Numerics; using Content.Server.Salvage.Expeditions; using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Events; -using Content.Server.Station.Components; using Content.Shared.Chat; using Content.Shared.Humanoid; using Content.Shared.Mobs.Components; @@ -10,6 +9,7 @@ using Content.Shared.Mobs.Systems; using Content.Shared.Salvage.Expeditions; using Content.Shared.Shuttles.Components; using Content.Shared.Localizations; +using Content.Shared.Station.Components; using Robust.Shared.Map.Components; using Robust.Shared.Player; diff --git a/Content.Server/Shuttles/Systems/ArrivalsSystem.cs b/Content.Server/Shuttles/Systems/ArrivalsSystem.cs index aa1c2e6dff..20976ca019 100644 --- a/Content.Server/Shuttles/Systems/ArrivalsSystem.cs +++ b/Content.Server/Shuttles/Systems/ArrivalsSystem.cs @@ -11,7 +11,6 @@ using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Events; using Content.Server.Spawners.Components; using Content.Server.Spawners.EntitySystems; -using Content.Server.Station.Components; using Content.Server.Station.Events; using Content.Server.Station.Systems; using Content.Shared.Administration; @@ -224,7 +223,7 @@ public sealed class ArrivalsSystem : EntitySystem if (component.FirstRun) { - var station = _station.GetLargestGrid(Comp(component.Station)); + var station = _station.GetLargestGrid(component.Station); sourceMap = station == null ? null : Transform(station.Value)?.MapUid; arrivalsDelay += RoundStartFTLDuration; component.FirstRun = false; @@ -470,7 +469,7 @@ public sealed class ArrivalsSystem : EntitySystem { while (query.MoveNext(out var uid, out var comp, out var shuttle, out var xform)) { - if (comp.NextTransfer > curTime || !TryComp(comp.Station, out var data)) + if (comp.NextTransfer > curTime) continue; var tripTime = _shuttles.DefaultTravelTime + _shuttles.DefaultStartupTime; @@ -486,7 +485,7 @@ public sealed class ArrivalsSystem : EntitySystem // Go to station else { - var targetGrid = _station.GetLargestGrid(data); + var targetGrid = _station.GetLargestGrid(comp.Station); if (targetGrid != null) _shuttles.FTLToDock(uid, shuttle, targetGrid.Value); diff --git a/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs b/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs index 9eff1455f9..53714c846a 100644 --- a/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs +++ b/Content.Server/Shuttles/Systems/EmergencyShuttleSystem.cs @@ -14,7 +14,6 @@ using Content.Server.RoundEnd; using Content.Server.Screens.Components; using Content.Server.Shuttles.Components; using Content.Server.Shuttles.Events; -using Content.Server.Station.Components; using Content.Server.Station.Events; using Content.Server.Station.Systems; using Content.Shared.Access.Systems; @@ -181,7 +180,7 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem return; } - var targetGrid = _station.GetLargestGrid(Comp(station.Value)); + var targetGrid = _station.GetLargestGrid(station.Value); if (targetGrid == null) return; @@ -270,7 +269,7 @@ public sealed partial class EmergencyShuttleSystem : EntitySystem return null; } - var targetGrid = _station.GetLargestGrid(Comp(stationUid)); + var targetGrid = _station.GetLargestGrid(stationUid); // UHH GOOD LUCK if (targetGrid == null) diff --git a/Content.Server/Shuttles/Systems/ShuttleSystem.GridFill.cs b/Content.Server/Shuttles/Systems/ShuttleSystem.GridFill.cs index a794ee0104..2e6ebe396c 100644 --- a/Content.Server/Shuttles/Systems/ShuttleSystem.GridFill.cs +++ b/Content.Server/Shuttles/Systems/ShuttleSystem.GridFill.cs @@ -1,9 +1,7 @@ using System.Numerics; using Content.Server.Shuttles.Components; -using Content.Server.Station.Components; using Content.Server.Station.Events; using Content.Shared.CCVar; -using Content.Shared.Salvage; using Content.Shared.Shuttles.Components; using Content.Shared.Station.Components; using Robust.Shared.Collections; @@ -62,10 +60,7 @@ public sealed partial class ShuttleSystem if (!_cfg.GetCVar(CCVars.GridFill)) return; - if (!TryComp(uid, out StationDataComponent? dataComp)) - return; - - var targetGrid = _station.GetLargestGrid(dataComp); + var targetGrid = _station.GetLargestGrid(uid); if (targetGrid == null) return; @@ -165,12 +160,7 @@ public sealed partial class ShuttleSystem if (!_cfg.GetCVar(CCVars.GridFill)) return; - if (!TryComp(uid, out var data)) - { - return; - } - - var targetGrid = _station.GetLargestGrid(data); + var targetGrid = _station.GetLargestGrid(uid); if (targetGrid == null) return; diff --git a/Content.Server/Silicons/StationAi/StationAiSystem.cs b/Content.Server/Silicons/StationAi/StationAiSystem.cs index 9b272a00f9..45b3dda431 100644 --- a/Content.Server/Silicons/StationAi/StationAiSystem.cs +++ b/Content.Server/Silicons/StationAi/StationAiSystem.cs @@ -1,33 +1,32 @@ -using System.Linq; -using Content.Server.Chat.Managers; using Content.Server.Chat.Systems; -using Content.Shared.Chat; -using Content.Shared.Mind; -using Content.Shared.Roles; +using Content.Shared.Chat.Prototypes; +using Content.Shared.DeviceNetwork.Components; using Content.Shared.Silicons.StationAi; using Content.Shared.StationAi; -using Robust.Shared.Audio; +using Content.Shared.Turrets; +using Content.Shared.Weapons.Ranged.Events; using Robust.Shared.Map.Components; using Robust.Shared.Player; +using Robust.Shared.Prototypes; using static Content.Server.Chat.Systems.ChatSystem; namespace Content.Server.Silicons.StationAi; public sealed class StationAiSystem : SharedStationAiSystem { - [Dependency] private readonly IChatManager _chats = default!; [Dependency] private readonly EntityLookupSystem _lookup = default!; [Dependency] private readonly SharedTransformSystem _xforms = default!; - [Dependency] private readonly SharedMindSystem _mind = default!; - [Dependency] private readonly SharedRoleSystem _roles = default!; - private readonly HashSet> _ais = new(); + private readonly HashSet> _stationAiCores = new(); + private readonly ProtoId _turretIsAttackingChatNotificationPrototype = "TurretIsAttacking"; + private readonly ProtoId _aiWireSnippedChatNotificationPrototype = "AiWireSnipped"; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnExpandICChatRecipients); + SubscribeLocalEvent(OnAmmoShot); } private void OnExpandICChatRecipients(ExpandICChatRecipientsEvent ev) @@ -61,15 +60,33 @@ public sealed class StationAiSystem : SharedStationAiSystem } } + private void OnAmmoShot(Entity ent, ref AmmoShotEvent args) + { + var xform = Transform(ent); + + if (!TryComp(xform.GridUid, out MapGridComponent? grid)) + return; + + var ais = GetStationAIs(xform.GridUid.Value); + + foreach (var ai in ais) + { + var ev = new ChatNotificationEvent(_turretIsAttackingChatNotificationPrototype, ent); + + if (TryComp(ent, out var deviceNetwork)) + ev.SourceNameOverride = Loc.GetString("station-ai-turret-component-name", ("name", Name(ent)), ("address", deviceNetwork.Address)); + + RaiseLocalEvent(ai, ref ev); + } + } + public override bool SetVisionEnabled(Entity entity, bool enabled, bool announce = false) { if (!base.SetVisionEnabled(entity, enabled, announce)) return false; if (announce) - { AnnounceSnip(entity.Owner); - } return true; } @@ -80,54 +97,59 @@ public sealed class StationAiSystem : SharedStationAiSystem return false; if (announce) - { AnnounceSnip(entity.Owner); - } return true; } - public override void AnnounceIntellicardUsage(EntityUid uid, SoundSpecifier? cue = null) + private void AnnounceSnip(EntityUid uid) { - if (!TryComp(uid, out var actor)) - return; - - var msg = Loc.GetString("ai-consciousness-download-warning"); - var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", msg)); - _chats.ChatMessageToOne(ChatChannel.Server, msg, wrappedMessage, default, false, actor.PlayerSession.Channel, colorOverride: Color.Red); - - if (cue != null && _mind.TryGetMind(uid, out var mindId, out _)) - _roles.MindPlaySound(mindId, cue); - } - - private void AnnounceSnip(EntityUid entity) - { - var xform = Transform(entity); + var xform = Transform(uid); if (!TryComp(xform.GridUid, out MapGridComponent? grid)) return; - _ais.Clear(); - _lookup.GetChildEntities(xform.GridUid.Value, _ais); - var filter = Filter.Empty(); + var ais = GetStationAIs(xform.GridUid.Value); - foreach (var ai in _ais) + foreach (var ai in ais) { - // TODO: Filter API? - if (TryComp(ai.Owner, out ActorComponent? actorComp)) - { - filter.AddPlayer(actorComp.PlayerSession); - } + if (!StationAiCanDetectWireSnipping(ai)) + continue; + + var ev = new ChatNotificationEvent(_aiWireSnippedChatNotificationPrototype, uid); + + var tile = Maps.LocalToTile(xform.GridUid.Value, grid, xform.Coordinates); + ev.SourceNameOverride = tile.ToString(); + + RaiseLocalEvent(ai, ref ev); + } + } + + private bool StationAiCanDetectWireSnipping(EntityUid uid) + { + // TODO: The ability to detect snipped AI interaction wires + // should be a MALF ability and/or a purchased upgrade rather + // than something available to the station AI by default. + // When these systems are added, add the appropriate checks here. + + return false; + } + + public HashSet GetStationAIs(EntityUid gridUid) + { + _stationAiCores.Clear(); + _lookup.GetChildEntities(gridUid, _stationAiCores); + + var hashSet = new HashSet(); + + foreach (var stationAiCore in _stationAiCores) + { + if (!TryGetHeld((stationAiCore, stationAiCore.Comp), out var insertedAi)) + continue; + + hashSet.Add(insertedAi); } - // TEST - // filter = Filter.Broadcast(); - - // No easy way to do chat notif embeds atm. - var tile = Maps.LocalToTile(xform.GridUid.Value, grid, xform.Coordinates); - var msg = Loc.GetString("ai-wire-snipped", ("coords", tile)); - - _chats.ChatMessageToMany(ChatChannel.Notifications, msg, msg, entity, false, true, filter.Recipients.Select(o => o.Channel)); - // Apparently there's no sound for this. + return hashSet; } } diff --git a/Content.Server/Singularity/EntitySystems/EventHorizonSystem.cs b/Content.Server/Singularity/EntitySystems/EventHorizonSystem.cs index 9c69422797..7090ca77ef 100644 --- a/Content.Server/Singularity/EntitySystems/EventHorizonSystem.cs +++ b/Content.Server/Singularity/EntitySystems/EventHorizonSystem.cs @@ -1,15 +1,13 @@ using System.Numerics; using Content.Server.Administration.Logs; using Content.Server.Singularity.Events; -using Content.Server.Station.Components; using Content.Shared.Database; -using Content.Shared.Ghost; using Content.Shared.Mind.Components; using Content.Shared.Singularity.Components; using Content.Shared.Singularity.EntitySystems; +using Content.Shared.Station.Components; using Content.Shared.Tag; using Robust.Shared.Containers; -using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Physics.Components; diff --git a/Content.Server/Speech/EntitySystems/VocalSystem.cs b/Content.Server/Speech/EntitySystems/VocalSystem.cs index fb88238288..275140ff5b 100644 --- a/Content.Server/Speech/EntitySystems/VocalSystem.cs +++ b/Content.Server/Speech/EntitySystems/VocalSystem.cs @@ -2,6 +2,7 @@ using Content.Server.Actions; using Content.Server.Chat.Systems; using Content.Server.Speech.Components; using Content.Shared.Chat.Prototypes; +using Content.Shared.Cloning.Events; using Content.Shared.Humanoid; using Content.Shared.Speech; using Content.Shared.Speech.Components; @@ -31,6 +32,25 @@ public sealed class VocalSystem : EntitySystem SubscribeLocalEvent(OnScreamAction); } + /// + /// Copy this component's datafields from one entity to another. + /// This can't use CopyComp because of the ScreamActionEntity DataField, which should not be copied. + /// + public void CopyComponent(Entity source, EntityUid target) + { + if (!Resolve(source, ref source.Comp)) + return; + + var targetComp = EnsureComp(target); + targetComp.Sounds = source.Comp.Sounds; + targetComp.ScreamId = source.Comp.ScreamId; + targetComp.Wilhelm = source.Comp.Wilhelm; + targetComp.WilhelmProbability = source.Comp.WilhelmProbability; + LoadSounds(target, targetComp); + + Dirty(target, targetComp); + } + private void OnMapInit(EntityUid uid, VocalComponent component, MapInitEvent args) { // try to add scream action when vocal comp added diff --git a/Content.Server/Station/Commands/StationCommand.cs b/Content.Server/Station/Commands/StationCommand.cs index 8fe8c67646..f9dcb83d93 100644 --- a/Content.Server/Station/Commands/StationCommand.cs +++ b/Content.Server/Station/Commands/StationCommand.cs @@ -2,12 +2,12 @@ using System.Diagnostics; using System.Linq; using Content.Server.Administration; using Content.Server.Cargo.Systems; -using Content.Server.Station.Components; using Content.Server.Station.Systems; using Content.Shared.Administration; +using Content.Shared.Station; +using Content.Shared.Station.Components; using Robust.Shared.Toolshed; using Robust.Shared.Toolshed.Errors; -using Robust.Shared.Toolshed.Syntax; using Robust.Shared.Utility; namespace Content.Server.Station.Commands; @@ -54,7 +54,7 @@ public sealed class StationsCommand : ToolshedCommand public EntityUid? LargestGrid([PipedArgument] EntityUid input) { _station ??= GetSys(); - return _station.GetLargestGrid(Comp(input)); + return _station.GetLargestGrid(input); } [CommandImplementation("largestgrid")] diff --git a/Content.Server/Station/Events/StationPostInitEvent.cs b/Content.Server/Station/Events/StationPostInitEvent.cs index 54b8eeeb31..934d482bf2 100644 --- a/Content.Server/Station/Events/StationPostInitEvent.cs +++ b/Content.Server/Station/Events/StationPostInitEvent.cs @@ -1,4 +1,4 @@ -using Content.Server.Station.Components; +using Content.Shared.Station.Components; namespace Content.Server.Station.Events; diff --git a/Content.Server/Station/Systems/StationBiomeSystem.cs b/Content.Server/Station/Systems/StationBiomeSystem.cs index c12e2f47e4..61bc70794a 100644 --- a/Content.Server/Station/Systems/StationBiomeSystem.cs +++ b/Content.Server/Station/Systems/StationBiomeSystem.cs @@ -20,12 +20,10 @@ public sealed partial class StationBiomeSystem : EntitySystem private void OnStationPostInit(Entity map, ref StationPostInitEvent args) { - if (!TryComp(map, out StationDataComponent? dataComp)) + var station = _station.GetLargestGrid(map.Owner); + if (station == null) return; - var station = _station.GetLargestGrid(dataComp); - if (station == null) return; - var mapId = Transform(station.Value).MapID; var mapUid = _map.GetMapOrInvalid(mapId); diff --git a/Content.Server/Station/Systems/StationNameSystem.cs b/Content.Server/Station/Systems/StationNameSystem.cs index fbbf690465..ea17830a47 100644 --- a/Content.Server/Station/Systems/StationNameSystem.cs +++ b/Content.Server/Station/Systems/StationNameSystem.cs @@ -1,4 +1,5 @@ using Content.Server.Station.Components; +using Content.Shared.Station.Components; namespace Content.Server.Station.Systems; diff --git a/Content.Server/Station/Systems/StationSystem.cs b/Content.Server/Station/Systems/StationSystem.cs index 456d3c6b80..7100c6144f 100644 --- a/Content.Server/Station/Systems/StationSystem.cs +++ b/Content.Server/Station/Systems/StationSystem.cs @@ -3,20 +3,16 @@ using Content.Server.Chat.Systems; using Content.Server.GameTicking; using Content.Server.Station.Components; using Content.Server.Station.Events; -using Content.Shared.CCVar; using Content.Shared.Station; using Content.Shared.Station.Components; using JetBrains.Annotations; -using Robust.Server.GameObjects; using Robust.Server.GameStates; using Robust.Server.Player; using Robust.Shared.Collections; -using Robust.Shared.Configuration; using Robust.Shared.Enums; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Player; -using Robust.Shared.Random; using Robust.Shared.Utility; namespace Content.Server.Station.Systems; @@ -34,7 +30,6 @@ public sealed partial class StationSystem : SharedStationSystem [Dependency] private readonly ChatSystem _chatSystem = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; [Dependency] private readonly MetaDataSystem _metaData = default!; - [Dependency] private readonly MapSystem _map = default!; [Dependency] private readonly PvsOverrideSystem _pvsOverride = default!; private ISawmill _sawmill = default!; @@ -42,8 +37,8 @@ public sealed partial class StationSystem : SharedStationSystem private EntityQuery _gridQuery; private EntityQuery _xformQuery; - private ValueList _mapIds = new(); - private ValueList<(Box2Rotated Bounds, MapId MapId)> _gridBounds = new(); + private ValueList _mapIds; + private ValueList<(Box2Rotated Bounds, MapId MapId)> _gridBounds; /// public override void Initialize() @@ -79,6 +74,7 @@ public sealed partial class StationSystem : SharedStationSystem return; stationData.Grids.Remove(uid); + Dirty(uid, component); } public override void Shutdown() @@ -193,44 +189,6 @@ public sealed partial class StationSystem : SharedStationSystem #endregion Event handlers - /// - /// Gets the largest member grid from a station. - /// - public EntityUid? GetLargestGrid(StationDataComponent component) - { - EntityUid? largestGrid = null; - Box2 largestBounds = new Box2(); - - foreach (var gridUid in component.Grids) - { - if (!TryComp(gridUid, out var grid) || - grid.LocalAABB.Size.LengthSquared() < largestBounds.Size.LengthSquared()) - continue; - - largestBounds = grid.LocalAABB; - largestGrid = gridUid; - } - - return largestGrid; - } - - /// - /// Returns the total number of tiles contained in the station's grids. - /// - public int GetTileCount(StationDataComponent component) - { - var count = 0; - foreach (var gridUid in component.Grids) - { - if (!TryComp(gridUid, out var grid)) - continue; - - count += _map.GetAllTiles(gridUid, grid).Count(); - } - - return count; - } - /// /// Tries to retrieve a filter for everything in the station the source is on. /// @@ -384,6 +342,7 @@ public sealed partial class StationSystem : SharedStationSystem var stationMember = EnsureComp(mapGrid); stationMember.Station = station; stationData.Grids.Add(mapGrid); + Dirty(station, stationData); RaiseLocalEvent(station, new StationGridAddedEvent(mapGrid, station, false), true); @@ -407,6 +366,7 @@ public sealed partial class StationSystem : SharedStationSystem RemComp(mapGrid); stationData.Grids.Remove(mapGrid); + Dirty(station, stationData); RaiseLocalEvent(station, new StationGridRemovedEvent(mapGrid, station), true); _sawmill.Info($"Removing grid {mapGrid} from station {Name(station)} ({station})"); @@ -450,110 +410,6 @@ public sealed partial class StationSystem : SharedStationSystem QueueDel(station); } - - public EntityUid? GetOwningStation(EntityUid? entity, TransformComponent? xform = null) - { - if (entity == null) - return null; - - return GetOwningStation(entity.Value, xform); - } - - /// - /// Gets the station that "owns" the given entity (essentially, the station the grid it's on is attached to) - /// - /// Entity to find the owner of. - /// Resolve pattern, transform of the entity. - /// The owning station, if any. - /// - /// This does not remember what station an entity started on, it simply checks where it is currently located. - /// - public EntityUid? GetOwningStation(EntityUid entity, TransformComponent? xform = null) - { - if (!Resolve(entity, ref xform)) - throw new ArgumentException("Tried to use an abstract entity!", nameof(entity)); - - if (TryComp(entity, out _)) - { - // We are the station, just return ourselves. - return entity; - } - - if (TryComp(entity, out _)) - { - // We are the station, just check ourselves. - return CompOrNull(entity)?.Station; - } - - if (xform.GridUid == EntityUid.Invalid) - { - Log.Debug("Unable to get owning station - GridUid invalid."); - return null; - } - - return CompOrNull(xform.GridUid)?.Station; - } - - public List GetStations() - { - var stations = new List(); - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out _)) - { - stations.Add(uid); - } - - return stations; - } - - public HashSet GetStationsSet() - { - var stations = new HashSet(); - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out _)) - { - stations.Add(uid); - } - - return stations; - } - - public List<(string Name, NetEntity Entity)> GetStationNames() - { - var stations = GetStationsSet(); - var stats = new List<(string Name, NetEntity Station)>(); - - foreach (var weh in stations) - { - stats.Add((MetaData(weh).EntityName, GetNetEntity(weh))); - } - - return stats; - } - - /// - /// Returns the first station that has a grid in a certain map. - /// If the map has no stations, null is returned instead. - /// - /// - /// If there are multiple stations on a map it is probably arbitrary which one is returned. - /// - public EntityUid? GetStationInMap(MapId map) - { - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var data)) - { - foreach (var gridUid in data.Grids) - { - if (Transform(gridUid).MapID == map) - { - return uid; - } - } - } - - return null; - } } /// diff --git a/Content.Server/StationEvents/Events/AnomalySpawnRule.cs b/Content.Server/StationEvents/Events/AnomalySpawnRule.cs index 06da91e256..4c9cfd87ea 100644 --- a/Content.Server/StationEvents/Events/AnomalySpawnRule.cs +++ b/Content.Server/StationEvents/Events/AnomalySpawnRule.cs @@ -1,7 +1,7 @@ using Content.Server.Anomaly; -using Content.Server.Station.Components; using Content.Server.StationEvents.Components; -using Content.Shared.GameTicking.Components; +using Content.Shared.GameTicking.Components; +using Content.Shared.Station.Components; namespace Content.Server.StationEvents.Events; @@ -31,7 +31,7 @@ public sealed class AnomalySpawnRule : StationEventSystem(chosenStation, out var stationData)) return; - var grid = StationSystem.GetLargestGrid(stationData); + var grid = StationSystem.GetLargestGrid((chosenStation.Value, stationData)); if (grid is null) return; diff --git a/Content.Server/StationEvents/Events/CargoGiftsRule.cs b/Content.Server/StationEvents/Events/CargoGiftsRule.cs index 91baf2ecad..d38de1e0d8 100644 --- a/Content.Server/StationEvents/Events/CargoGiftsRule.cs +++ b/Content.Server/StationEvents/Events/CargoGiftsRule.cs @@ -2,9 +2,9 @@ using System.Linq; using Content.Server.Cargo.Components; using Content.Server.Cargo.Systems; using Content.Server.GameTicking; -using Content.Server.Station.Components; using Content.Server.StationEvents.Components; using Content.Shared.GameTicking.Components; +using Content.Shared.Station.Components; using Robust.Shared.Prototypes; namespace Content.Server.StationEvents.Events; diff --git a/Content.Server/StationEvents/Events/MeteorSwarmSystem.cs b/Content.Server/StationEvents/Events/MeteorSwarmSystem.cs index 948fedf6fc..e1987789b6 100644 --- a/Content.Server/StationEvents/Events/MeteorSwarmSystem.cs +++ b/Content.Server/StationEvents/Events/MeteorSwarmSystem.cs @@ -1,7 +1,6 @@ using System.Numerics; using Content.Server.Chat.Systems; using Content.Server.GameTicking.Rules; -using Content.Server.Station.Components; using Content.Server.Station.Systems; using Content.Server.StationEvents.Components; using Content.Shared.GameTicking.Components; @@ -49,7 +48,7 @@ public sealed class MeteorSwarmSystem : GameRuleSystem return; var station = RobustRandom.Pick(_station.GetStations()); - if (_station.GetLargestGrid(Comp(station)) is not { } grid) + if (_station.GetLargestGrid(station) is not { } grid) return; var mapId = Transform(grid).MapID; diff --git a/Content.Server/StationEvents/Events/SpaceSpawnRule.cs b/Content.Server/StationEvents/Events/SpaceSpawnRule.cs index 6fccaaa5cf..08e43b4c68 100644 --- a/Content.Server/StationEvents/Events/SpaceSpawnRule.cs +++ b/Content.Server/StationEvents/Events/SpaceSpawnRule.cs @@ -1,6 +1,4 @@ using Content.Server.Antag; -using Content.Server.GameTicking.Rules.Components; -using Content.Server.Station.Components; using Content.Server.StationEvents.Components; using Content.Shared.GameTicking.Components; using Robust.Shared.Map; @@ -32,10 +30,8 @@ public sealed class SpaceSpawnRule : StationEventSystem return; } - var stationData = Comp(station.Value); - // find a station grid - var gridUid = StationSystem.GetLargestGrid(stationData); + var gridUid = StationSystem.GetLargestGrid(station.Value); if (gridUid == null || !TryComp(gridUid, out var grid)) { Sawmill.Warning("Chosen station has no grids, cannot pick location for {ToPrettyString(uid):rule}"); diff --git a/Content.Server/UserInterface/StatValuesCommand.cs b/Content.Server/UserInterface/StatValuesCommand.cs index d64984b574..263dfa8b9e 100644 --- a/Content.Server/UserInterface/StatValuesCommand.cs +++ b/Content.Server/UserInterface/StatValuesCommand.cs @@ -6,10 +6,12 @@ using Content.Server.EUI; using Content.Server.Item; using Content.Server.Power.Components; using Content.Shared.Administration; +using Content.Shared.Damage.Prototypes; using Content.Shared.Item; using Content.Shared.Research.Prototypes; using Content.Shared.UserInterface; using Content.Shared.Weapons.Melee; +using Content.Shared.Wieldable.Components; using Robust.Shared.Console; using Robust.Shared.Prototypes; @@ -170,10 +172,13 @@ public sealed class StatValuesCommand : IConsoleCommand return state; } + private static readonly ProtoId StructuralDamageType = "Structural"; + private StatValuesEuiMessage GetMelee() { var values = new List(); var meleeName = _entManager.ComponentFactory.GetComponentName(); + var increaseDamageName = _entManager.ComponentFactory.GetComponentName(); foreach (var proto in _proto.EnumeratePrototypes()) { @@ -186,26 +191,45 @@ public sealed class StatValuesCommand : IConsoleCommand var comp = (MeleeWeaponComponent) meleeComp.Component; - // TODO: Wielded damage // TODO: Esword damage + var structuralDamage = comp.Damage.DamageDict.GetValueOrDefault(StructuralDamageType); + var baseDamage = comp.Damage.GetTotal() - comp.Damage.DamageDict.GetValueOrDefault(StructuralDamageType); + + var wieldedStructuralDamage = "-"; + var wieldedDamage = "-"; + if (proto.Components.TryGetValue(increaseDamageName, out var increaseDamageComp)) + { + var comp2 = (IncreaseDamageOnWieldComponent) increaseDamageComp.Component; + + wieldedStructuralDamage = (structuralDamage + comp2.BonusDamage.DamageDict.GetValueOrDefault(StructuralDamageType)).ToString(); + wieldedDamage = (baseDamage + comp2.BonusDamage.GetTotal() - comp2.BonusDamage.DamageDict.GetValueOrDefault(StructuralDamageType)).ToString(); + } + values.Add(new[] { proto.ID, - (comp.Damage.GetTotal() * comp.AttackRate).ToString(), - comp.AttackRate.ToString(CultureInfo.CurrentCulture), - comp.Damage.GetTotal().ToString(), - comp.Range.ToString(CultureInfo.CurrentCulture), + baseDamage.ToString(), + wieldedDamage, + comp.AttackRate.ToString("0.00", CultureInfo.CurrentCulture), + (comp.AttackRate * baseDamage).Float().ToString("0.00", CultureInfo.CurrentCulture), + structuralDamage.ToString(), + wieldedStructuralDamage, }); } - var state = new StatValuesEuiMessage() + var state = new StatValuesEuiMessage { - Title = "Cargo sell prices", - Headers = new List() + Title = Loc.GetString("stat-melee-values"), + Headers = new List { - "ID", - "Price", + Loc.GetString("stat-melee-id"), + Loc.GetString("stat-melee-base-damage"), + Loc.GetString("stat-melee-wield-damage"), + Loc.GetString("stat-melee-attack-rate"), + Loc.GetString("stat-melee-dps"), + Loc.GetString("stat-melee-structural-damage"), + Loc.GetString("stat-melee-structural-wield-damage"), }, Values = values, }; diff --git a/Content.Server/Wagging/WaggingSystem.cs b/Content.Server/Wagging/WaggingSystem.cs index 7ccc19e20c..88b82a9ca5 100644 --- a/Content.Server/Wagging/WaggingSystem.cs +++ b/Content.Server/Wagging/WaggingSystem.cs @@ -1,5 +1,6 @@ using Content.Server.Actions; using Content.Server.Humanoid; +using Content.Shared.Cloning.Events; using Content.Shared.Humanoid; using Content.Shared.Humanoid.Markings; using Content.Shared.Mobs; @@ -26,6 +27,15 @@ public sealed class WaggingSystem : EntitySystem SubscribeLocalEvent(OnWaggingShutdown); SubscribeLocalEvent(OnWaggingToggle); SubscribeLocalEvent(OnMobStateChanged); + SubscribeLocalEvent(OnCloning); + } + + private void OnCloning(Entity ent, ref CloningEvent args) + { + if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name)) + return; + + EnsureComp(args.CloneUid); } private void OnWaggingMapInit(EntityUid uid, WaggingComponent component, MapInitEvent args) diff --git a/Content.Shared/Anomaly/Components/InnerBodyAnomalyComponent.cs b/Content.Shared/Anomaly/Components/InnerBodyAnomalyComponent.cs index a7e07ae2b5..dfc1d561c4 100644 --- a/Content.Shared/Anomaly/Components/InnerBodyAnomalyComponent.cs +++ b/Content.Shared/Anomaly/Components/InnerBodyAnomalyComponent.cs @@ -1,5 +1,6 @@ using Content.Shared.Anomaly.Effects; using Content.Shared.Body.Prototypes; +using Content.Shared.Humanoid.Prototypes; using Robust.Shared.Audio; using Robust.Shared.GameStates; using Robust.Shared.Prototypes; @@ -62,7 +63,7 @@ public sealed partial class InnerBodyAnomalyComponent : Component /// Ability to use unique sprites for different body types /// [DataField, AutoNetworkedField] - public Dictionary, SpriteSpecifier> SpeciesSprites = new(); + public Dictionary, SpriteSpecifier> SpeciesSprites = new(); /// /// The key of the entity layer into which the sprite will be inserted diff --git a/Content.Shared/Bed/Cryostorage/SharedCryostorageSystem.cs b/Content.Shared/Bed/Cryostorage/SharedCryostorageSystem.cs index a93f2e0618..757a707d40 100644 --- a/Content.Shared/Bed/Cryostorage/SharedCryostorageSystem.cs +++ b/Content.Shared/Bed/Cryostorage/SharedCryostorageSystem.cs @@ -25,6 +25,7 @@ public abstract class SharedCryostorageSystem : EntitySystem [Dependency] protected readonly IGameTiming Timing = default!; [Dependency] protected readonly ISharedAdminLogManager AdminLog = default!; [Dependency] protected readonly SharedMindSystem Mind = default!; + [Dependency] private readonly MetaDataSystem _meta = default!; protected EntityUid? PausedMap { get; private set; } @@ -167,8 +168,10 @@ public abstract class SharedCryostorageSystem : EntitySystem if (PausedMap != null && Exists(PausedMap)) return; - PausedMap = _map.CreateMap(); - _map.SetPaused(PausedMap.Value, true); + var mapUid = _map.CreateMap(); + _meta.SetEntityName(mapUid, Loc.GetString("cryostorage-paused-map-name")); + _map.SetPaused(mapUid, true); + PausedMap = mapUid; } public bool IsInPausedMap(Entity entity) diff --git a/Content.Shared/Changeling/ChangelingIdentityComponent.cs b/Content.Shared/Changeling/ChangelingIdentityComponent.cs new file mode 100644 index 0000000000..461315f4ce --- /dev/null +++ b/Content.Shared/Changeling/ChangelingIdentityComponent.cs @@ -0,0 +1,35 @@ +using Content.Shared.Cloning; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Changeling; + +/// +/// The storage component for Changelings, it handles the link between a changeling and its consumed identities +/// that exist on a paused map. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ChangelingIdentityComponent : Component +{ + /// + /// The list of entities that exist on a paused map. They are paused clones of the victims that the ling has consumed, with all relevant components copied from the original. + /// + // TODO: Store a reference to the original entity as well so you cannot infinitely devour somebody. Currently very tricky due the inability to send over EntityUid if the original is ever deleted. Can be fixed by something like WeakEntityReference. + [DataField, AutoNetworkedField] + public List ConsumedIdentities = new(); + + + /// + /// The currently assumed identity. + /// + [DataField, AutoNetworkedField] + public EntityUid? CurrentIdentity; + + /// + /// The cloning settings passed to the CloningSystem, contains a list of all components to copy or have handled by their + /// respective systems. + /// + public ProtoId IdentityCloningSettings = "ChangelingCloningSettings"; + + public override bool SendOnlyToOwner => true; +} diff --git a/Content.Shared/Changeling/ChangelingIdentitySystem.cs b/Content.Shared/Changeling/ChangelingIdentitySystem.cs new file mode 100644 index 0000000000..f68f72f853 --- /dev/null +++ b/Content.Shared/Changeling/ChangelingIdentitySystem.cs @@ -0,0 +1,185 @@ +using System.Numerics; +using Content.Shared.Cloning; +using Content.Shared.Humanoid; +using Content.Shared.Mind.Components; +using Content.Shared.NameModifier.EntitySystems; +using Robust.Shared.GameStates; +using Robust.Shared.Map; +using Robust.Shared.Network; +using Robust.Shared.Player; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Changeling; + +public sealed class ChangelingIdentitySystem : EntitySystem +{ + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + [Dependency] private readonly MetaDataSystem _metaSystem = default!; + [Dependency] private readonly NameModifierSystem _nameMod = default!; + [Dependency] private readonly SharedCloningSystem _cloningSystem = default!; + [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidSystem = default!; + [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly SharedPvsOverrideSystem _pvsOverrideSystem = default!; + + public MapId? PausedMapId; + private int _numberOfStoredIdentities = 0; // TODO: remove this + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnMindAdded); + SubscribeLocalEvent(OnMindRemoved); + SubscribeLocalEvent(OnStoredRemove); + } + + private void OnMindAdded(Entity ent, ref MindAddedMessage args) + { + if (!TryComp(args.Container.Owner, out var actor)) + return; + + HandOverPvsOverride(actor.PlayerSession, ent.Comp); + } + + private void OnMindRemoved(Entity ent, ref MindRemovedMessage args) + { + CleanupPvsOverride(ent, args.Container.Owner); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + // Make a backup of our current identity so we can transform back. + var clone = CloneToPausedMap(ent, ent.Owner); + ent.Comp.CurrentIdentity = clone; + } + + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + CleanupPvsOverride(ent, ent.Owner); + CleanupChangelingNullspaceIdentities(ent); + } + + private void OnStoredRemove(Entity ent, ref ComponentRemove args) + { + // The last stored identity is being deleted, we can clean up the map. + if (_net.IsServer && PausedMapId != null && Count() <= 1) + _map.QueueDeleteMap(PausedMapId.Value); + } + + /// + /// Cleanup all nullspaced Identities when the changeling no longer exists + /// + /// the changeling + public void CleanupChangelingNullspaceIdentities(Entity ent) + { + if (_net.IsClient) + return; + + foreach (var consumedIdentity in ent.Comp.ConsumedIdentities) + { + QueueDel(consumedIdentity); + } + } + + /// + /// Clone a target humanoid into nullspace and add it to the Changelings list of identities. + /// It creates a perfect copy of the target and can be used to pull components down for future use + /// + /// the Changeling + /// the targets uid + public EntityUid? CloneToPausedMap(Entity ent, EntityUid target) + { + // Don't create client side duplicate clones or a clientside map. + if (_net.IsClient) + return null; + + if (!TryComp(target, out var humanoid) + || !_prototype.Resolve(humanoid.Species, out var speciesPrototype) + || !_prototype.Resolve(ent.Comp.IdentityCloningSettings, out var settings)) + return null; + + EnsurePausedMap(); + // TODO: Setting the spawn location is a shitty bandaid to prevent admins from crashing our servers. + // Movercontrollers and mob collisions are currently being calculated even for paused entities. + // Spawning all of them in the same spot causes severe performance problems. + // Cryopods and Polymorph have the same problem. + var mob = Spawn(speciesPrototype.Prototype, new MapCoordinates(new Vector2(2 * _numberOfStoredIdentities++, 0), PausedMapId!.Value)); + + var storedIdentity = EnsureComp(mob); + storedIdentity.OriginalEntity = target; // TODO: network this once we have WeakEntityReference or the autonetworking source gen is fixed + + if (TryComp(target, out var actor)) + storedIdentity.OriginalSession = actor.PlayerSession; + + _humanoidSystem.CloneAppearance(target, mob); + _cloningSystem.CloneComponents(target, mob, settings); + + var targetName = _nameMod.GetBaseName(target); + _metaSystem.SetEntityName(mob, targetName); + ent.Comp.ConsumedIdentities.Add(mob); + + Dirty(ent); + HandlePvsOverride(ent, mob); + + return mob; + } + + /// + /// Simple helper to add a PVS override to a Nullspace Identity + /// + /// + /// + private void HandlePvsOverride(EntityUid uid, EntityUid target) + { + if (!TryComp(uid, out var actor)) + return; + + _pvsOverrideSystem.AddSessionOverride(target, actor.PlayerSession); + } + + /// + /// Cleanup all Pvs Overrides for the owner of the ChangelingIdentity + /// + /// the Changeling itself + /// Who specifically to cleanup from, usually just the same owner, but in the case of a mindswap we want to clean up the victim + private void CleanupPvsOverride(Entity ent, EntityUid entityUid) + { + if (!TryComp(entityUid, out var actor)) + return; + + foreach (var identity in ent.Comp.ConsumedIdentities) + { + _pvsOverrideSystem.RemoveSessionOverride(identity, actor.PlayerSession); + } + } + + /// + /// Inform another Session of the entities stored for Transformation + /// + /// The Session you wish to inform + /// The Target storage of identities + public void HandOverPvsOverride(ICommonSession session, ChangelingIdentityComponent comp) + { + foreach (var entity in comp.ConsumedIdentities) + { + _pvsOverrideSystem.AddSessionOverride(entity, session); + } + } + + /// + /// Create a paused map for storing devoured identities as a clone of the player. + /// + private void EnsurePausedMap() + { + if (_map.MapExists(PausedMapId)) + return; + + var mapUid = _map.CreateMap(out var newMapId); + _metaSystem.SetEntityName(mapUid, Loc.GetString("changeling-paused-map-name")); + PausedMapId = newMapId; + _map.SetPaused(mapUid, true); + } +} diff --git a/Content.Shared/Changeling/ChangelingRoleComponent.cs b/Content.Shared/Changeling/ChangelingRoleComponent.cs new file mode 100644 index 0000000000..d2e9c1eccb --- /dev/null +++ b/Content.Shared/Changeling/ChangelingRoleComponent.cs @@ -0,0 +1,9 @@ +using Content.Shared.Roles; + +namespace Content.Shared.Changeling; + +/// +/// The Mindrole for Changeling Antags +/// +[RegisterComponent] +public sealed partial class ChangelingRoleComponent : BaseMindRoleComponent; diff --git a/Content.Shared/Changeling/ChangelingStoredIdentityComponent.cs b/Content.Shared/Changeling/ChangelingStoredIdentityComponent.cs new file mode 100644 index 0000000000..44583190e6 --- /dev/null +++ b/Content.Shared/Changeling/ChangelingStoredIdentityComponent.cs @@ -0,0 +1,29 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Player; + +namespace Content.Shared.Changeling; + +/// +/// Marker component for cloned identities devoured by a changeling. +/// These are stored on a paused map so that the changeling can transform into them. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class ChangelingStoredIdentityComponent : Component +{ + /// + /// The original entity the identity was cloned from. + /// + /// + /// TODO: Not networked at the moment because it will create PVS errors when the original is somehow deleted. + /// Use WeakEntityReference once it's merged. + /// + [DataField] + public EntityUid? OriginalEntity; + + /// + /// The player session of the original entity, if any. + /// Used for admin logging purposes. + /// + [ViewVariables] + public ICommonSession? OriginalSession; +} diff --git a/Content.Shared/Changeling/Devour/ChangelingDevourComponent.cs b/Content.Shared/Changeling/Devour/ChangelingDevourComponent.cs new file mode 100644 index 0000000000..7798c6fec9 --- /dev/null +++ b/Content.Shared/Changeling/Devour/ChangelingDevourComponent.cs @@ -0,0 +1,133 @@ +using Content.Shared.Damage; +using Content.Shared.Damage.Prototypes; +using Content.Shared.FixedPoint; +using Content.Shared.Whitelist; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Changeling.Devour; + +/// +/// Component responsible for Changelings Devour attack. Including the amount of damage +/// and how long it takes to devour someone +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause] +[Access(typeof(ChangelingDevourSystem))] +public sealed partial class ChangelingDevourComponent : Component +{ + /// + /// The Action for devouring + /// + [DataField] + public EntProtoId? ChangelingDevourAction = "ActionChangelingDevour"; + + /// + /// The action entity associated with devouring + /// + [DataField, AutoNetworkedField] + public EntityUid? ChangelingDevourActionEntity; + + /// + /// The whitelist of targets for devouring + /// + [DataField, AutoNetworkedField] + public EntityWhitelist? Whitelist = new() + { + Components = + [ + "MobState", + "HumanoidAppearance", + ], + }; + + /// + /// The Sound to use during consumption of a victim + /// + /// + /// 6 distance due to the default 15 being hearable all the way across PVS. Changeling is meant to be stealthy. + /// 6 still allows the sound to be hearable, but not across an entire department. + /// + [DataField, AutoNetworkedField] + public SoundSpecifier? ConsumeNoise = new SoundCollectionSpecifier("ChangelingDevourConsume", AudioParams.Default.WithMaxDistance(6)); + + /// + /// The Sound to use during the windup before consuming a victim + /// + /// + /// 6 distance due to the default 15 being hearable all the way across PVS. Changeling is meant to be stealthy. + /// 6 still allows the sound to be hearable, but not across an entire department. + /// + [DataField, AutoNetworkedField] + public SoundSpecifier? DevourWindupNoise = new SoundCollectionSpecifier("ChangelingDevourWindup", AudioParams.Default.WithMaxDistance(6)); + + /// + /// The time between damage ticks + /// + [DataField, AutoNetworkedField] + public TimeSpan DamageTimeBetweenTicks = TimeSpan.FromSeconds(1); + + /// + /// The windup time before the changeling begins to engage in devouring the identity of a target + /// + [DataField, AutoNetworkedField] + public TimeSpan DevourWindupTime = TimeSpan.FromSeconds(2); + + /// + /// The time it takes to FULLY consume someones identity. + /// + [DataField, AutoNetworkedField] + public TimeSpan DevourConsumeTime = TimeSpan.FromSeconds(10); + + /// + /// Damage cap that a target is allowed to be caused due to IdentityConsumption + /// + [DataField, AutoNetworkedField] + public float DevourConsumeDamageCap = 350f; + + /// + /// The Currently active devour sound in the world + /// + [DataField] + public EntityUid? CurrentDevourSound; + + /// + /// The damage profile for a single tick of devour damage + /// + [DataField, AutoNetworkedField] + public DamageSpecifier DamagePerTick = new() + { + DamageDict = new Dictionary + { + { "Slash", 10}, + { "Piercing", 10 }, + { "Blunt", 5 }, + }, + }; + + /// + /// The list of protective damage types capable of preventing a devour if over the threshold + /// + [DataField, AutoNetworkedField] + public List> ProtectiveDamageTypes = new() + { + "Slash", + "Piercing", + "Blunt", + }; + + /// + /// The next Tick to deal damage on (utilized during the consumption "do-during" (a do after with an attempt event)) + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField] + public TimeSpan NextTick = TimeSpan.Zero; + + /// + /// The percentage of ANY brute damage resistance that will prevent devouring + /// + [DataField, AutoNetworkedField] + public float DevourPreventionPercentageThreshold = 0.1f; + + public override bool SendOnlyToOwner => true; +} diff --git a/Content.Shared/Changeling/Devour/ChangelingDevourSystem.Events.cs b/Content.Shared/Changeling/Devour/ChangelingDevourSystem.Events.cs new file mode 100644 index 0000000000..d063737e5c --- /dev/null +++ b/Content.Shared/Changeling/Devour/ChangelingDevourSystem.Events.cs @@ -0,0 +1,22 @@ +using Content.Shared.Actions; +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared.Changeling.Devour; + +/// +/// Action event for Devour, someone has initiated a devour on someone, begin to windup. +/// +public sealed partial class ChangelingDevourActionEvent : EntityTargetActionEvent; + +/// +/// A windup has either successfully been completed or has been canceled. If successful start the devouring DoAfter. +/// +[Serializable, NetSerializable] +public sealed partial class ChangelingDevourWindupDoAfterEvent : SimpleDoAfterEvent; + +/// +/// The Consumption DoAfter has either successfully been completed or was canceled. +/// +[Serializable, NetSerializable] +public sealed partial class ChangelingDevourConsumeDoAfterEvent : SimpleDoAfterEvent; diff --git a/Content.Shared/Changeling/Devour/ChangelingDevourSystem.cs b/Content.Shared/Changeling/Devour/ChangelingDevourSystem.cs new file mode 100644 index 0000000000..83a589a8e3 --- /dev/null +++ b/Content.Shared/Changeling/Devour/ChangelingDevourSystem.cs @@ -0,0 +1,276 @@ +using Content.Shared.Actions; +using Content.Shared.Administration.Logs; +using Content.Shared.Armor; +using Content.Shared.Atmos.Rotting; +using Content.Shared.Body.Components; +using Content.Shared.Damage; +using Content.Shared.Database; +using Content.Shared.DoAfter; +using Content.Shared.Humanoid; +using Content.Shared.IdentityManagement; +using Content.Shared.Inventory; +using Content.Shared.Mobs.Systems; +using Content.Shared.Nutrition.Components; +using Content.Shared.Popups; +using Content.Shared.Storage; +using Content.Shared.Whitelist; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Network; +using Robust.Shared.Random; +using Robust.Shared.Timing; + +namespace Content.Shared.Changeling.Devour; + +public sealed class ChangelingDevourSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!; + [Dependency] private readonly DamageableSystem _damageable = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + [Dependency] private readonly ChangelingIdentitySystem _changelingIdentitySystem = default!; + [Dependency] private readonly InventorySystem _inventorySystem = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly IRobustRandom _robustRandom = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnDevourAction); + SubscribeLocalEvent(OnDevourWindup); + SubscribeLocalEvent(OnDevourConsume); + SubscribeLocalEvent>(OnConsumeAttemptTick); + SubscribeLocalEvent(OnShutdown); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + _actionsSystem.AddAction(ent, ref ent.Comp.ChangelingDevourActionEntity, ent.Comp.ChangelingDevourAction); + } + + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + if (ent.Comp.ChangelingDevourActionEntity != null) + { + _actionsSystem.RemoveAction(ent.Owner, ent.Comp.ChangelingDevourActionEntity); + } + } + + //TODO: Allow doafters to have proper update loop support. Attempt events should not be doing state changes. + private void OnConsumeAttemptTick(Entity ent, + ref DoAfterAttemptEvent eventData) + { + + var curTime = _timing.CurTime; + + if (curTime < ent.Comp.NextTick) + return; + + ConsumeDamageTick(eventData.Event.Target, ent.Comp, eventData.Event.User); + ent.Comp.NextTick += ent.Comp.DamageTimeBetweenTicks; + Dirty(ent, ent.Comp); + } + + private void ConsumeDamageTick(EntityUid? target, ChangelingDevourComponent comp, EntityUid? user) + { + if (target == null) + return; + + if (!TryComp(target, out var damage)) + return; + + foreach (var damagePoints in comp.DamagePerTick.DamageDict) + { + + if (damage.Damage.DamageDict.TryGetValue(damagePoints.Key, out var val) && val > comp.DevourConsumeDamageCap) + return; + } + _damageable.TryChangeDamage(target, comp.DamagePerTick, true, true, damage, user); + } + + /// + /// Checkes if the targets outerclothing is beyond a DamageCoefficientThreshold to protect them from being devoured. + /// + /// The Targeted entity + /// Changelings Devour Component + /// Is the target Protected from the attack + private bool IsTargetProtected(EntityUid target, Entity ent) + { + var ev = new CoefficientQueryEvent(SlotFlags.OUTERCLOTHING); + + RaiseLocalEvent(target, ev); + + foreach (var compProtectiveDamageType in ent.Comp.ProtectiveDamageTypes) + { + if (!ev.DamageModifiers.Coefficients.TryGetValue(compProtectiveDamageType, out var coefficient)) + continue; + if (coefficient < 1f - ent.Comp.DevourPreventionPercentageThreshold) + return true; + } + + return false; + } + + private void OnDevourAction(Entity ent, ref ChangelingDevourActionEvent args) + { + if (args.Handled || _whitelistSystem.IsWhitelistFailOrNull(ent.Comp.Whitelist, args.Target) + || !HasComp(ent)) + return; + + args.Handled = true; + var target = args.Target; + + if (target == ent.Owner) + return; // don't eat yourself + + if (HasComp(target)) + { + _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-rotting"), args.Performer, args.Performer, PopupType.Medium); + return; + } + + if (IsTargetProtected(target, ent)) + { + _popupSystem.PopupClient(Loc.GetString("changeling-devour-attempt-failed-protected"), ent, ent, PopupType.Medium); + return; + } + + if (_net.IsServer) + { + var pvsSound = _audio.PlayPvs(ent.Comp.DevourWindupNoise, ent); + if (pvsSound != null) + ent.Comp.CurrentDevourSound = pvsSound.Value.Entity; + } + + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ent:player} started changeling devour windup against {target:player}"); + + _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, ent, ent.Comp.DevourWindupTime, new ChangelingDevourWindupDoAfterEvent(), ent, target: target, used: ent) + { + BreakOnMove = true, + BlockDuplicate = true, + DuplicateCondition = DuplicateConditions.None, + }); + + var selfMessage = Loc.GetString("changeling-devour-begin-windup-self", ("user", Identity.Entity(ent.Owner, EntityManager))); + var othersMessage = Loc.GetString("changeling-devour-begin-windup-others", ("user", Identity.Entity(ent.Owner, EntityManager))); + _popupSystem.PopupPredicted( + selfMessage, + othersMessage, + args.Performer, + args.Performer, + PopupType.MediumCaution); + } + + private void OnDevourWindup(Entity ent, ref ChangelingDevourWindupDoAfterEvent args) + { + var curTime = _timing.CurTime; + args.Handled = true; + + if (!EntityManager.EntityExists(ent.Comp.CurrentDevourSound)) + _audio.Stop(ent.Comp.CurrentDevourSound!); + + if (args.Cancelled) + return; + + var selfMessage = Loc.GetString("changeling-devour-begin-consume-self", ("user", Identity.Entity(ent.Owner, EntityManager))); + var othersMessage = Loc.GetString("changeling-devour-begin-consume-others", ("user", Identity.Entity(ent.Owner, EntityManager))); + _popupSystem.PopupPredicted( + selfMessage, + othersMessage, + args.User, + args.User, + PopupType.LargeCaution); + + if (_net.IsServer) + { + var pvsSound = _audio.PlayPvs(ent.Comp.ConsumeNoise, ent); + + if (pvsSound != null) + ent.Comp.CurrentDevourSound = pvsSound.Value.Entity; + } + + + ent.Comp.NextTick = curTime + ent.Comp.DamageTimeBetweenTicks; + + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} began to devour {ToPrettyString(args.Target):player} identity"); + + _doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, + ent, + ent.Comp.DevourConsumeTime, + new ChangelingDevourConsumeDoAfterEvent(), + ent, + target: args.Target, + used: ent) + { + AttemptFrequency = AttemptFrequency.EveryTick, + BreakOnMove = true, + BlockDuplicate = true, + DuplicateCondition = DuplicateConditions.None, + }); + } + + private void OnDevourConsume(Entity ent, ref ChangelingDevourConsumeDoAfterEvent args) + { + args.Handled = true; + var target = args.Target; + + if (target == null) + return; + + if (EntityManager.EntityExists(ent.Comp.CurrentDevourSound)) + _audio.Stop(ent.Comp.CurrentDevourSound!); + + if (args.Cancelled) + return; + + if (!_mobState.IsDead((EntityUid)target)) + { + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} unsuccessfully devoured {ToPrettyString(args.Target):player}'s identity"); + _popupSystem.PopupClient(Loc.GetString("changeling-devour-consume-failed-not-dead"), args.User, args.User, PopupType.Medium); + return; + } + + var selfMessage = Loc.GetString("changeling-devour-consume-complete-self", ("user", Identity.Entity(args.User, EntityManager))); + var othersMessage = Loc.GetString("changeling-devour-consume-complete-others", ("user", Identity.Entity(args.User, EntityManager))); + _popupSystem.PopupPredicted( + selfMessage, + othersMessage, + args.User, + args.User, + PopupType.LargeCaution); + + if (_mobState.IsDead(target.Value) + && TryComp(target, out var body) + && HasComp(target) + && TryComp(args.User, out var identityStorage)) + { + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} successfully devoured {ToPrettyString(args.Target):player}'s identity"); + _changelingIdentitySystem.CloneToPausedMap((ent, identityStorage), target.Value); + + if (_inventorySystem.TryGetSlotEntity(target.Value, "jumpsuit", out var item) + && TryComp(item, out var butcherable)) + RipClothing(target.Value, (item.Value, butcherable)); + } + + Dirty(ent); + } + + private void RipClothing(EntityUid victim, Entity item) + { + var spawnEntities = EntitySpawnCollection.GetSpawns(item.Comp.SpawnedEntities, _robustRandom); + + foreach (var proto in spawnEntities) + { + // TODO: once predictedRandom is in, make this a Coordinate offset of 0.25f from the victims position + PredictedSpawnNextToOrDrop(proto, victim); + } + + PredictedQueueDel(item.Owner); + } +} diff --git a/Content.Shared/Changeling/Transform/ChangelingTransformComponent.cs b/Content.Shared/Changeling/Transform/ChangelingTransformComponent.cs new file mode 100644 index 0000000000..0a3b3f1985 --- /dev/null +++ b/Content.Shared/Changeling/Transform/ChangelingTransformComponent.cs @@ -0,0 +1,54 @@ +using Content.Shared.Cloning; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Changeling.Transform; + +/// +/// The component containing information about Changelings Transformation action +/// Like how long their windup is, the sounds as well as the Target Cloning settings for changing between identities +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(ChangelingTransformSystem))] +public sealed partial class ChangelingTransformComponent : Component +{ + /// + /// The action Prototype for Transforming + /// + [DataField] + public EntProtoId? ChangelingTransformAction = "ActionChangelingTransform"; + + /// + /// The Action Entity for transforming associated with this Component + /// + [DataField, AutoNetworkedField] + public EntityUid? ChangelingTransformActionEntity; + + /// + /// Time it takes to Transform + /// + [DataField, AutoNetworkedField] + public TimeSpan TransformWindup = TimeSpan.FromSeconds(5); + + /// + /// The noise used when attempting to transform + /// + [DataField, AutoNetworkedField] + public SoundSpecifier? TransformAttemptNoise = new SoundCollectionSpecifier("ChangelingTransformAttempt", AudioParams.Default.WithMaxDistance(6)); // 6 distance due to the default 15 being hearable all the way across PVS. Changeling is meant to be stealthy. 6 still allows the sound to be hearable, but not across an entire department. + + /// + /// The currently active transform in the world + /// + [DataField] + public EntityUid? CurrentTransformSound; + + /// + /// The cloning settings passed to the CloningSystem, contains a list of all components to copy or have handled by their + /// respective systems. + /// + public ProtoId TransformCloningSettings = "ChangelingCloningSettings"; + + public override bool SendOnlyToOwner => true; +} + diff --git a/Content.Shared/Changeling/Transform/ChangelingTransformSystem.Events.cs b/Content.Shared/Changeling/Transform/ChangelingTransformSystem.Events.cs new file mode 100644 index 0000000000..cfe2a56933 --- /dev/null +++ b/Content.Shared/Changeling/Transform/ChangelingTransformSystem.Events.cs @@ -0,0 +1,16 @@ +using Content.Shared.Actions; +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared.Changeling.Transform; + +/// +/// Action event for opening the changeling transformation radial menu. +/// +public sealed partial class ChangelingTransformActionEvent : InstantActionEvent; + +/// +/// DoAfterevent used to transform a changeling into one of their stored identities. +/// +[Serializable, NetSerializable] +public sealed partial class ChangelingTransformDoAfterEvent : SimpleDoAfterEvent; diff --git a/Content.Shared/Changeling/Transform/ChangelingTransformSystem.UI.cs b/Content.Shared/Changeling/Transform/ChangelingTransformSystem.UI.cs new file mode 100644 index 0000000000..0383867698 --- /dev/null +++ b/Content.Shared/Changeling/Transform/ChangelingTransformSystem.UI.cs @@ -0,0 +1,33 @@ +using Robust.Shared.Serialization; + +namespace Content.Shared.Changeling.Transform; + +/// +/// Send when a player selects an intentity to transform into in the radial menu. +/// +[Serializable, NetSerializable] +public sealed class ChangelingTransformIdentitySelectMessage(NetEntity targetIdentity) : BoundUserInterfaceMessage +{ + /// + /// The uid of the cloned identity. + /// + public readonly NetEntity TargetIdentity = targetIdentity; +} + +// TODO: Replace with component states. +// We are already networking the ChangelingIdentityComponent, which contains all this information, +// so we can just read it from them from the component and update the UI in an AfterAuotHandleState subscription. +[Serializable, NetSerializable] +public sealed class ChangelingTransformBoundUserInterfaceState(List identities) : BoundUserInterfaceState +{ + /// + /// The uids of the cloned identities. + /// + public readonly List Identites = identities; +} + +[Serializable, NetSerializable] +public enum TransformUI : byte +{ + Key, +} diff --git a/Content.Shared/Changeling/Transform/ChangelingTransformSystem.cs b/Content.Shared/Changeling/Transform/ChangelingTransformSystem.cs new file mode 100644 index 0000000000..dbc5356448 --- /dev/null +++ b/Content.Shared/Changeling/Transform/ChangelingTransformSystem.cs @@ -0,0 +1,180 @@ +using Content.Shared.Actions; +using Content.Shared.Administration.Logs; +using Content.Shared.Cloning; +using Content.Shared.Database; +using Content.Shared.DoAfter; +using Content.Shared.Humanoid; +using Content.Shared.IdentityManagement; +using Content.Shared.Popups; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Changeling.Transform; + +public sealed partial class ChangelingTransformSystem : EntitySystem +{ + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly SharedActionsSystem _actionsSystem = default!; + [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidAppearanceSystem = default!; + [Dependency] private readonly MetaDataSystem _metaSystem = default!; + [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedCloningSystem _cloningSystem = default!; + [Dependency] private readonly IPrototypeManager _prototype = default!; + + private const string ChangelingBuiXmlGeneratedName = "ChangelingTransformBoundUserInterface"; + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnTransformAction); + SubscribeLocalEvent(OnSuccessfulTransform); + SubscribeLocalEvent(OnTransformSelected); + SubscribeLocalEvent(OnShutdown); + } + + private void OnMapInit(Entity ent, ref MapInitEvent init) + { + _actionsSystem.AddAction(ent, ref ent.Comp.ChangelingTransformActionEntity, ent.Comp.ChangelingTransformAction); + + var userInterfaceComp = EnsureComp(ent); + _uiSystem.SetUi((ent, userInterfaceComp), TransformUI.Key, new InterfaceData(ChangelingBuiXmlGeneratedName)); + } + + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + if (ent.Comp.ChangelingTransformActionEntity != null) + { + _actionsSystem.RemoveAction(ent.Owner, ent.Comp.ChangelingTransformActionEntity); + } + } + + private void OnTransformAction(Entity ent, + ref ChangelingTransformActionEvent args) + { + if (!TryComp(ent, out var userInterfaceComp)) + return; + + if (!TryComp(ent, out var userIdentity)) + return; + + if (!_uiSystem.IsUiOpen((ent, userInterfaceComp), TransformUI.Key, args.Performer)) + { + _uiSystem.OpenUi((ent, userInterfaceComp), TransformUI.Key, args.Performer); + + var identityData = new List(); + + foreach (var consumedIdentity in userIdentity.ConsumedIdentities) + { + identityData.Add(GetNetEntity(consumedIdentity)); + } + + _uiSystem.SetUiState((ent, userInterfaceComp), TransformUI.Key, new ChangelingTransformBoundUserInterfaceState(identityData)); + } //TODO: Can add a Else here with TransformInto and CloseUI to make a quick switch, + // issue right now is that Radials cover the Action buttons so clicking the action closes the UI (due to clicking off a radial causing it to close, even with UI) + // but pressing the number does. + } + + /// + /// Transform the changeling into another identity. + /// This can be any cloneable humanoid and doesn't have to be stored in the ChangelingIdentiyComponent, + /// so make sure to validate the target before. + /// + public void TransformInto(Entity ent, EntityUid targetIdentity) + { + if (!Resolve(ent, ref ent.Comp)) + return; + + var selfMessage = Loc.GetString("changeling-transform-attempt-self", ("user", Identity.Entity(ent.Owner, EntityManager))); + var othersMessage = Loc.GetString("changeling-transform-attempt-others", ("user", Identity.Entity(ent.Owner, EntityManager))); + _popupSystem.PopupPredicted( + selfMessage, + othersMessage, + ent, + ent, + PopupType.MediumCaution); + + if (_net.IsServer) + ent.Comp.CurrentTransformSound = _audio.PlayPvs(ent.Comp.TransformAttemptNoise, ent)?.Entity; + + if(TryComp(targetIdentity, out var storedIdentity) && storedIdentity.OriginalSession != null) + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} begun an attempt to transform into \"{Name(targetIdentity)}\" ({storedIdentity.OriginalSession:player}) "); + else + _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(ent.Owner):player} begun an attempt to transform into \"{Name(targetIdentity)}\""); + + var result = _doAfterSystem.TryStartDoAfter(new DoAfterArgs( + EntityManager, + ent, + ent.Comp.TransformWindup, + new ChangelingTransformDoAfterEvent(), + ent, + target: targetIdentity) + { + BreakOnMove = true, + BreakOnWeightlessMove = true, + DuplicateCondition = DuplicateConditions.None, + RequireCanInteract = false, + DistanceThreshold = null, + }); + } + + private void OnTransformSelected(Entity ent, + ref ChangelingTransformIdentitySelectMessage args) + { + _uiSystem.CloseUi(ent.Owner, TransformUI.Key, ent); + + if (!TryGetEntity(args.TargetIdentity, out var targetIdentity)) + return; + + if (!TryComp(ent, out var identity)) + return; + + if (identity.CurrentIdentity == targetIdentity) + return; // don't transform into ourselves + + if (!identity.ConsumedIdentities.Contains(targetIdentity.Value)) + return; // this identity does not belong to this player + + TransformInto(ent.AsNullable(), targetIdentity.Value); + } + + private void OnSuccessfulTransform(Entity ent, + ref ChangelingTransformDoAfterEvent args) + { + args.Handled = true; + + if (EntityManager.EntityExists(ent.Comp.CurrentTransformSound)) + _audio.Stop(ent.Comp.CurrentTransformSound); + + if (args.Cancelled) + return; + + if (!_prototype.Resolve(ent.Comp.TransformCloningSettings, out var settings)) + return; + + if (args.Target is not { } targetIdentity) + return; + + _humanoidAppearanceSystem.CloneAppearance(targetIdentity, args.User); + _cloningSystem.CloneComponents(targetIdentity, args.User, settings); + + if(TryComp(targetIdentity, out var storedIdentity) && storedIdentity.OriginalSession != null) + _adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(ent.Owner):player} successfully transformed into \"{Name(targetIdentity)}\" ({storedIdentity.OriginalSession:player})"); + else + _adminLogger.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(ent.Owner):player} successfully transformed into \"{Name(targetIdentity)}\""); + _metaSystem.SetEntityName(ent, Name(targetIdentity), raiseEvents: false); + + Dirty(ent); + + if (TryComp(ent, out var identity)) // in case we ever get changelings that don't store identities + { + identity.CurrentIdentity = targetIdentity; + Dirty(ent.Owner, identity); + } + } +} diff --git a/Content.Shared/Chat/Prototypes/ChatNotificationPrototype.cs b/Content.Shared/Chat/Prototypes/ChatNotificationPrototype.cs new file mode 100644 index 0000000000..58315161a5 --- /dev/null +++ b/Content.Shared/Chat/Prototypes/ChatNotificationPrototype.cs @@ -0,0 +1,74 @@ +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Chat.Prototypes; + +/// +/// A predefined notification used to warn a player of specific events. +/// +[Prototype("chatNotification")] +public sealed partial class ChatNotificationPrototype : IPrototype +{ + [ViewVariables] + [IdDataField] + public string ID { get; private set; } = default!; + + /// + /// The notification that the player receives. + /// + /// + /// Use '{$source}', '{user}', and '{target}' in the fluent message + /// to insert the source, user, and target names respectively. + /// + [DataField(required: true)] + public LocId Message = string.Empty; + + /// + /// Font color for the notification. + /// + [DataField] + public Color Color = Color.White; + + /// + /// Sound played upon receiving the notification. + /// + [DataField] + public SoundSpecifier? Sound; + + /// + /// The period during which duplicate chat notifications are blocked after a player receives one. + /// Blocked notifications will never be delivered to the player. + /// + [DataField] + public TimeSpan NextDelay = TimeSpan.FromSeconds(10.0); + + /// + /// Determines whether notification delays should be determined by the source + /// entity or by the notification prototype (i.e., individual notifications + /// vs grouping the notifications together). + /// + [DataField] + public bool NotifyBySource = false; +} + +/// +/// Raised when an specific player should be notified via a chat message of a predefined event occuring. +/// +/// The prototype used to define the chat notification. +/// The entity that the triggered the notification. +/// The entity that ultimately responsible for triggering the notification. +[ByRefEvent] +public record ChatNotificationEvent(ProtoId ChatNotification, EntityUid Source, EntityUid? User = null) +{ + /// + /// Set this variable if you want to change the name of the notification source + /// (if the name is included in the chat notification). + /// + public string? SourceNameOverride; + + /// + /// Set this variable if you wish to change the name of the user who triggered the notification + /// (if the name is included in the chat notification). + /// + public string? UserNameOverride; +} diff --git a/Content.Shared/Chat/TypingIndicator/TypingIndicatorComponent.cs b/Content.Shared/Chat/TypingIndicator/TypingIndicatorComponent.cs index f263de4913..7861da4546 100644 --- a/Content.Shared/Chat/TypingIndicator/TypingIndicatorComponent.cs +++ b/Content.Shared/Chat/TypingIndicator/TypingIndicatorComponent.cs @@ -7,13 +7,13 @@ namespace Content.Shared.Chat.TypingIndicator; /// Show typing indicator icon when player typing text in chat box. /// Added automatically when player poses entity. /// -[RegisterComponent, NetworkedComponent] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] [Access(typeof(SharedTypingIndicatorSystem))] public sealed partial class TypingIndicatorComponent : Component { /// /// Prototype id that store all visual info about typing indicator. /// - [DataField("proto")] + [DataField("proto"), AutoNetworkedField] public ProtoId TypingIndicatorPrototype = "default"; } diff --git a/Content.Shared/Cloning/SharedCloningSystem.cs b/Content.Shared/Cloning/SharedCloningSystem.cs new file mode 100644 index 0000000000..d8ab8a2aa1 --- /dev/null +++ b/Content.Shared/Cloning/SharedCloningSystem.cs @@ -0,0 +1,14 @@ +namespace Content.Shared.Cloning; + +public abstract partial class SharedCloningSystem : EntitySystem +{ + /// + /// Copy components from one entity to another based on a CloningSettingsPrototype. + /// + /// The orignal Entity to clone components from. + /// The target Entity to clone components to. + /// The clone settings prototype containing the list of components to clone. + public virtual void CloneComponents(EntityUid original, EntityUid clone, CloningSettingsPrototype settings) + { + } +} diff --git a/Content.Shared/Disposal/Unit/SharedDisposalUnitSystem.cs b/Content.Shared/Disposal/Unit/SharedDisposalUnitSystem.cs index bdf8b5ba07..292cfd2b3c 100644 --- a/Content.Shared/Disposal/Unit/SharedDisposalUnitSystem.cs +++ b/Content.Shared/Disposal/Unit/SharedDisposalUnitSystem.cs @@ -1,7 +1,6 @@ using System.Linq; using Content.Shared.ActionBlocker; using Content.Shared.Administration.Logs; -using Content.Shared.Body.Components; using Content.Shared.Climbing.Systems; using Content.Shared.Containers; using Content.Shared.Database; @@ -15,6 +14,7 @@ using Content.Shared.Hands.EntitySystems; using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Item; +using Content.Shared.Mobs.Components; using Content.Shared.Movement.Events; using Content.Shared.Popups; using Content.Shared.Power; @@ -450,7 +450,7 @@ public abstract class SharedDisposalUnitSystem : EntitySystem return false; var storable = HasComp(entity); - if (!storable && !HasComp(entity)) + if (!storable && !HasComp(entity)) return false; if (_whitelistSystem.IsBlacklistPass(component.Blacklist, entity) || diff --git a/Content.Shared/Forensics/Systems/SharedForensicsSystem.cs b/Content.Shared/Forensics/Systems/SharedForensicsSystem.cs new file mode 100644 index 0000000000..1220b75fff --- /dev/null +++ b/Content.Shared/Forensics/Systems/SharedForensicsSystem.cs @@ -0,0 +1,18 @@ +using Content.Shared.Forensics.Components; + +namespace Content.Shared.Forensics.Systems; + +public abstract class SharedForensicsSystem : EntitySystem +{ + /// + /// Give the entity a new, random DNA string and call an event to notify other systems like the bloodstream that it has been changed. + /// Does nothing if it does not have the DnaComponent. + /// + public virtual void RandomizeDNA(Entity ent) { } + + /// + /// Give the entity a new, random fingerprint string. + /// Does nothing if it does not have the FingerprintComponent. + /// + public virtual void RandomizeFingerprint(Entity ent) { } +} diff --git a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs index 3d3af84a30..1df46e53d6 100644 --- a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs +++ b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs @@ -11,6 +11,7 @@ using Content.Shared.Inventory; using Content.Shared.Preferences; using Robust.Shared; using Robust.Shared.Configuration; +using Robust.Shared.Enums; using Robust.Shared.GameObjects.Components.Localization; using Robust.Shared.Network; using Robust.Shared.Player; @@ -152,16 +153,12 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem targetHumanoid.SkinColor = sourceHumanoid.SkinColor; targetHumanoid.EyeColor = sourceHumanoid.EyeColor; targetHumanoid.Age = sourceHumanoid.Age; - SetSex(target, sourceHumanoid.Sex, false, targetHumanoid); targetHumanoid.CustomBaseLayers = new(sourceHumanoid.CustomBaseLayers); targetHumanoid.MarkingSet = new(sourceHumanoid.MarkingSet); - targetHumanoid.Gender = sourceHumanoid.Gender; + SetSex(target, sourceHumanoid.Sex, false, targetHumanoid); + SetGender((target, targetHumanoid), sourceHumanoid.Gender); - if (TryComp(target, out var grammar)) - _grammarSystem.SetGender((target, grammar), sourceHumanoid.Gender); - - _identity.QueueIdentityUpdate(target); Dirty(target, targetHumanoid); } @@ -264,6 +261,23 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem Dirty(uid, humanoid); } + /// + /// Sets the gender in the entity's HumanoidAppearanceComponent and GrammarComponent. + /// + public void SetGender(Entity ent, Gender gender) + { + if (!Resolve(ent, ref ent.Comp)) + return; + + ent.Comp.Gender = gender; + Dirty(ent); + + if (TryComp(ent, out var grammar)) + _grammarSystem.SetGender((ent, grammar), gender); + + _identity.QueueIdentityUpdate(ent); + } + /// /// Sets the skin color of this humanoid mob. Will only affect base layers that are not custom, /// custom base layers should use instead. diff --git a/Content.Shared/Implants/Components/SubdermalImplantComponent.cs b/Content.Shared/Implants/Components/SubdermalImplantComponent.cs index bd0ff09678..390d113dfb 100644 --- a/Content.Shared/Implants/Components/SubdermalImplantComponent.cs +++ b/Content.Shared/Implants/Components/SubdermalImplantComponent.cs @@ -49,7 +49,7 @@ public sealed partial class SubdermalImplantComponent : Component /// [DataField] public EntityWhitelist? Blacklist; - + /// /// If set, this ProtoId is used when attempting to draw the implant instead. /// Useful if the implant is a child to another implant and you don't want to differentiate between them when drawing. @@ -66,11 +66,6 @@ public sealed partial class OpenStorageImplantEvent : InstantActionEvent } -public sealed partial class UseFreedomImplantEvent : InstantActionEvent -{ - -} - /// /// Used for triggering trigger events on the implant via action /// @@ -86,13 +81,3 @@ public sealed partial class OpenUplinkImplantEvent : InstantActionEvent { } - -public sealed partial class UseScramImplantEvent : InstantActionEvent -{ - -} - -public sealed partial class UseDnaScramblerImplantEvent : InstantActionEvent -{ - -} diff --git a/Content.Shared/Implants/SharedSubdermalImplantSystem.cs b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs index 177e24ff02..4c015f1209 100644 --- a/Content.Shared/Implants/SharedSubdermalImplantSystem.cs +++ b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs @@ -44,7 +44,8 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem _actionsSystem.AddAction(component.ImplantedEntity.Value, ref component.Action, component.ImplantAction, uid); } - //replace micro bomb with macro bomb + // replace micro bomb with macro bomb + // TODO: this shouldn't be hardcoded here if (_container.TryGetContainer(component.ImplantedEntity.Value, ImplanterComponent.ImplantSlotId, out var implantContainer) && _tag.HasTag(uid, MacroBombTag)) { foreach (var implant in implantContainer.ContainedEntities) diff --git a/Content.Shared/Intellicard/Components/IntellicardComponent.cs b/Content.Shared/Intellicard/Components/IntellicardComponent.cs index e27174977f..5a299b1f80 100644 --- a/Content.Shared/Intellicard/Components/IntellicardComponent.cs +++ b/Content.Shared/Intellicard/Components/IntellicardComponent.cs @@ -20,20 +20,4 @@ public sealed partial class IntellicardComponent : Component /// [DataField, AutoNetworkedField] public int UploadTime = 3; - - /// - /// The sound that plays for the AI - /// when they are being downloaded - /// - [DataField, AutoNetworkedField] - public SoundSpecifier? WarningSound = new SoundPathSpecifier("/Audio/Misc/notice2.ogg"); - - /// - /// The delay before allowing the warning to play again in seconds. - /// - [DataField, AutoNetworkedField] - public TimeSpan WarningDelay = TimeSpan.FromSeconds(8); - - [ViewVariables] - public TimeSpan NextWarningAllowed = TimeSpan.Zero; } diff --git a/Content.Shared/Inventory/InventorySystem.Equip.cs b/Content.Shared/Inventory/InventorySystem.Equip.cs index 3ed068070a..ae038b63f3 100644 --- a/Content.Shared/Inventory/InventorySystem.Equip.cs +++ b/Content.Shared/Inventory/InventorySystem.Equip.cs @@ -194,8 +194,6 @@ public abstract partial class InventorySystem if (triggerHandContact && !((slotDefinition.SlotFlags & SlotFlags.GLOVES) == 0)) TriggerHandContactInteraction(target); - Dirty(target, inventory); - _movementSpeed.RefreshMovementSpeedModifiers(target); return true; @@ -487,8 +485,6 @@ public abstract partial class InventorySystem if (triggerHandContact && !((slotDefinition.SlotFlags & SlotFlags.GLOVES) == 0)) TriggerHandContactInteraction(target); - Dirty(target, inventory); - _movementSpeed.RefreshMovementSpeedModifiers(target); return true; diff --git a/Content.Shared/Inventory/InventorySystem.Slots.cs b/Content.Shared/Inventory/InventorySystem.Slots.cs index e9fb62f2ad..3f0bdf10d4 100644 --- a/Content.Shared/Inventory/InventorySystem.Slots.cs +++ b/Content.Shared/Inventory/InventorySystem.Slots.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; +using Content.Shared.DisplacementMap; using Content.Shared.Inventory.Events; using Content.Shared.Storage; using Robust.Shared.Containers; @@ -55,7 +56,25 @@ public partial class InventorySystem : EntitySystem return false; } - private void OnInit(Entity ent, ref ComponentInit args) + /// + /// Copy this component's datafields from one entity to another. + /// This can't use CopyComp because the template needs to be applied using the API method. + /// + public void CopyComponent(Entity source, EntityUid target) + { + if (!Resolve(source, ref source.Comp)) + return; + + var targetComp = EnsureComp(target); + targetComp.SpeciesId = source.Comp.SpeciesId; + targetComp.Displacements = new Dictionary(source.Comp.Displacements); + targetComp.FemaleDisplacements = new Dictionary(source.Comp.FemaleDisplacements); + targetComp.MaleDisplacements = new Dictionary(source.Comp.MaleDisplacements); + SetTemplateId((target, targetComp), source.Comp.TemplateId); + Dirty(target, targetComp); + } + + protected virtual void OnInit(Entity ent, ref ComponentInit args) { UpdateInventoryTemplate(ent); } @@ -92,6 +111,9 @@ public partial class InventorySystem : EntitySystem container.OccludesLight = false; ent.Comp.Containers[i] = container; } + + var ev = new InventoryTemplateUpdated(); + RaiseLocalEvent(ent, ref ev); } private void OnOpenSlotStorage(OpenSlotStorageNetworkMessage ev, EntitySessionEventArgs args) diff --git a/Content.Shared/Mind/Filters/AliveHumansPool.cs b/Content.Shared/Mind/Filters/AliveHumansPool.cs new file mode 100644 index 0000000000..c8e5c55bae --- /dev/null +++ b/Content.Shared/Mind/Filters/AliveHumansPool.cs @@ -0,0 +1,12 @@ +namespace Content.Shared.Mind.Filters; + +/// +/// A mind pool that uses . +/// +public sealed partial class AliveHumansPool : IMindPool +{ + void IMindPool.FindMinds(HashSet> minds, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys) + { + mindSys.AddAliveHumans(minds, exclude); + } +} diff --git a/Content.Shared/Mind/Filters/AntagonistMindFilter.cs b/Content.Shared/Mind/Filters/AntagonistMindFilter.cs new file mode 100644 index 0000000000..d805138ac3 --- /dev/null +++ b/Content.Shared/Mind/Filters/AntagonistMindFilter.cs @@ -0,0 +1,15 @@ +using Content.Shared.Roles; + +namespace Content.Shared.Mind.Filters; + +/// +/// A mind filter that requires minds to have an antagonist role. +/// +public sealed partial class AntagonistMindFilter : MindFilter +{ + protected override bool ShouldRemove(Entity mind, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys) + { + var roleSys = entMan.System(); + return !roleSys.MindIsAntagonist(mind); + } +} diff --git a/Content.Shared/Mind/Filters/BodyMindFilter.cs b/Content.Shared/Mind/Filters/BodyMindFilter.cs new file mode 100644 index 0000000000..334539634f --- /dev/null +++ b/Content.Shared/Mind/Filters/BodyMindFilter.cs @@ -0,0 +1,21 @@ +using Content.Shared.Whitelist; + +namespace Content.Shared.Mind.Filters; + +/// +/// A mind filter that checks the mind's owned entity against a whitelist. +/// +public sealed partial class BodyMindFilter : MindFilter +{ + [DataField(required: true)] + public EntityWhitelist Whitelist = new(); + + protected override bool ShouldRemove(Entity ent, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys) + { + if (ent.Comp.OwnedEntity is not {} mob) + return true; + + var sys = entMan.System(); + return sys.IsWhitelistFail(Whitelist, mob); + } +} diff --git a/Content.Shared/Mind/Filters/HasRoleMindFilter.cs b/Content.Shared/Mind/Filters/HasRoleMindFilter.cs new file mode 100644 index 0000000000..22f250dd29 --- /dev/null +++ b/Content.Shared/Mind/Filters/HasRoleMindFilter.cs @@ -0,0 +1,22 @@ +using Content.Shared.Roles; +using Content.Shared.Whitelist; + +namespace Content.Shared.Mind.Filters; + +/// +/// A mind filter that requires minds to have a role matching a whitelist. +/// +public sealed partial class HasRoleMindFilter : MindFilter +{ + /// + /// The whitelist a role must match for the mind to pass the filter. + /// + [DataField(required: true)] + public EntityWhitelist Whitelist; + + protected override bool ShouldRemove(Entity mind, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys) + { + var roleSys = entMan.System(); + return roleSys.MindHasRole(mind, Whitelist); + } +} diff --git a/Content.Shared/Mind/Filters/IMindPool.cs b/Content.Shared/Mind/Filters/IMindPool.cs new file mode 100644 index 0000000000..263d15d812 --- /dev/null +++ b/Content.Shared/Mind/Filters/IMindPool.cs @@ -0,0 +1,19 @@ +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Content.Shared.Mind.Filters; + +/// +/// A mind pool that can find minds to use for objectives etc. +/// Further filtered by . +/// +[ImplicitDataDefinitionForInheritors] +public partial interface IMindPool +{ + /// + /// Add minds for this pool to a hashset. + /// The hashset gets reused and is cleared before this is called. + /// + /// The hashset to add to + /// A mind entity that must not be returned + void FindMinds(HashSet> minds, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys); +} diff --git a/Content.Shared/Mind/Filters/JobMindFilter.cs b/Content.Shared/Mind/Filters/JobMindFilter.cs new file mode 100644 index 0000000000..a6565e4d33 --- /dev/null +++ b/Content.Shared/Mind/Filters/JobMindFilter.cs @@ -0,0 +1,21 @@ +using Content.Shared.Roles; +using Content.Shared.Roles.Jobs; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Mind.Filters; + +/// +/// A mind filter that requires minds to have a specific job. +/// This uses mind roles, not ID cards. +/// +public sealed partial class JobMindFilter : MindFilter +{ + [DataField(required: true)] + public ProtoId Job; + + protected override bool ShouldRemove(Entity mind, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys) + { + var jobSys = entMan.System(); + return jobSys.MindHasJobWithId(mind, Job); + } +} diff --git a/Content.Shared/Mind/Filters/MindFilter.cs b/Content.Shared/Mind/Filters/MindFilter.cs new file mode 100644 index 0000000000..c2daf3e361 --- /dev/null +++ b/Content.Shared/Mind/Filters/MindFilter.cs @@ -0,0 +1,32 @@ +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Content.Shared.Mind.Filters; + +/// +/// A mind filter that can be used to filter out minds from a . +/// +[ImplicitDataDefinitionForInheritors] +public abstract partial class MindFilter +{ + /// + /// The actual filter function, this has to return false for minds that get removed from the pool. + /// An excluded mind will be the same one passed to . + /// + /// The mind to check + /// The same mind passed to FindMinds + protected abstract bool ShouldRemove(Entity mind, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys); + + /// + /// The high-level filter function to be used by the mind system. + /// + public bool Filter(Entity mind, EntityUid? exclude, EntityManager entMan, SharedMindSystem mindSys) + { + return ShouldRemove(mind, exclude, entMan, mindSys) ^ Inverted; + } + + /// + /// Whether to invert functionality, only keeping minds that would otherwise be removed. + /// + [DataField] + public bool Inverted; +} diff --git a/Content.Shared/Mind/Filters/ObjectiveMindFilter.cs b/Content.Shared/Mind/Filters/ObjectiveMindFilter.cs new file mode 100644 index 0000000000..2d3cf6a263 --- /dev/null +++ b/Content.Shared/Mind/Filters/ObjectiveMindFilter.cs @@ -0,0 +1,25 @@ +using Content.Shared.Whitelist; + +namespace Content.Shared.Mind.Filters; + +/// +/// A mind filter that removes minds with a blacklist objective. +/// +public sealed partial class ObjectiveMindFilter : MindFilter +{ + [DataField(required: true)] + public EntityWhitelist Blacklist = new(); + + protected override bool ShouldRemove(Entity mind, EntityUid? exclude, IEntityManager entMan, SharedMindSystem mindSys) + { + var whitelistSys = entMan.System(); + foreach (var obj in mind.Comp.Objectives) + { + // mind has a blacklisted objective, remove it from the pool + if (whitelistSys.IsBlacklistPass(Blacklist, obj)) + return true; + } + + return false; + } +} diff --git a/Content.Shared/Mind/SharedMindSystem.cs b/Content.Shared/Mind/SharedMindSystem.cs index 271639725f..98ff77810c 100644 --- a/Content.Shared/Mind/SharedMindSystem.cs +++ b/Content.Shared/Mind/SharedMindSystem.cs @@ -9,6 +9,7 @@ using Content.Shared.Humanoid; using Content.Shared.Interaction.Events; using Content.Shared.Movement.Components; using Content.Shared.Mind.Components; +using Content.Shared.Mind.Filters; using Content.Shared.Mobs.Components; using Content.Shared.Mobs.Systems; using Content.Shared.Objectives.Systems; @@ -19,6 +20,7 @@ using Content.Shared.Whitelist; using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Player; +using Robust.Shared.Random; using Robust.Shared.Utility; namespace Content.Shared.Mind; @@ -27,6 +29,7 @@ public abstract partial class SharedMindSystem : EntitySystem { [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly SharedObjectivesSystem _objectives = default!; [Dependency] private readonly SharedPlayerSystem _player = default!; @@ -37,6 +40,8 @@ public abstract partial class SharedMindSystem : EntitySystem [ViewVariables] protected readonly Dictionary UserMinds = new(); + private HashSet> _pickingMinds = new(); + public override void Initialize() { base.Initialize(); @@ -618,23 +623,70 @@ public abstract partial class SharedMindSystem : EntitySystem /// /// Returns a list of every living humanoid player's minds, except for a single one which is exluded. + /// A new hashset is allocated for every call, consider using instead. /// public HashSet> GetAliveHumans(EntityUid? exclude = null) { var allHumans = new HashSet>(); + AddAliveHumans(allHumans, exclude); + return allHumans; + } + + /// + /// Adds to a hashset every living humanoid player's minds, except for a single one which is exluded. + /// + public void AddAliveHumans(HashSet> allHumans, EntityUid? exclude = null) + { // HumanoidAppearanceComponent is used to prevent mice, pAIs, etc from being chosen - var query = EntityQueryEnumerator(); - while (query.MoveNext(out var uid, out var mobState, out _)) + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _, out var mobState)) { // the player needs to have a mind and not be the excluded one + // the player has to be alive if (!TryGetMind(uid, out var mind, out var mindComp) || mind == exclude || !_mobState.IsAlive(uid, mobState)) continue; - allHumans.Add(new Entity(mind, mindComp)); + allHumans.Add((mind, mindComp)); } + } - return allHumans; + /// + /// Picks a random mind from a pool after applying a list of filters. + /// Returns null if no valid mind could be found. + /// + public Entity? PickFromPool(IMindPool pool, List filters, EntityUid? exclude = null) + { + _pickingMinds.Clear(); + pool.FindMinds(_pickingMinds, exclude, EntityManager, this); + FilterMinds(_pickingMinds, filters, exclude); + + if (_pickingMinds.Count == 0) + return null; + + return _random.Pick(_pickingMinds); + } + + /// + /// Filters minds from a hashset using a single . + /// + public void FilterMinds(HashSet> minds, MindFilter filter, EntityUid? exclude = null) + { + minds.RemoveWhere(mind => filter.Filter(mind, exclude, EntityManager, this)); + } + + /// + /// Filters minds from a hashset using a list of s to apply sequentially. + /// + public void FilterMinds(HashSet> minds, List filters, EntityUid? exclude = null) + { + foreach (var filter in filters) + { + // no point calling it if there are none left + if (minds.Count == 0) + break; + + FilterMinds(minds, filter, exclude); + } } /// diff --git a/Content.Shared/Morgue/EntityStorageLayingDownOverrideSystem.cs b/Content.Shared/Morgue/EntityStorageLayingDownOverrideSystem.cs index 9341168ba3..630135f36a 100644 --- a/Content.Shared/Morgue/EntityStorageLayingDownOverrideSystem.cs +++ b/Content.Shared/Morgue/EntityStorageLayingDownOverrideSystem.cs @@ -1,4 +1,3 @@ -using Content.Shared.Body.Components; using Content.Shared.Morgue.Components; using Content.Shared.Standing; using Content.Shared.Storage.Components; @@ -20,7 +19,9 @@ public sealed class EntityStorageLayingDownOverrideSystem : EntitySystem { foreach (var ent in args.Contents) { - if (HasComp(ent) && !_standing.IsDown(ent)) + // Explicitly check for standing state component, as entities without it will return false for IsDown() + // which prevents inserting any kind of non-mobs into this container (which is unintended) + if (TryComp(ent, out var standingState) && !_standing.IsDown(ent, standingState)) args.Contents.Remove(ent); } } diff --git a/Content.Shared/Movement/Systems/MovementSpeedModifierSystem.cs b/Content.Shared/Movement/Systems/MovementSpeedModifierSystem.cs index 4584e4401a..d0faad8b50 100644 --- a/Content.Shared/Movement/Systems/MovementSpeedModifierSystem.cs +++ b/Content.Shared/Movement/Systems/MovementSpeedModifierSystem.cs @@ -54,6 +54,21 @@ namespace Content.Shared.Movement.Systems RefreshMovementSpeedModifiers(entity); } + /// + /// Copy this component's datafields from one entity to another. + /// This needs to refresh the modifiers after using CopyComp. + /// + public void CopyComponent(Entity source, EntityUid target) + { + if (!Resolve(source, ref source.Comp)) + return; + + CopyComp(source, target, source.Comp); + RefreshWeightlessModifiers(target); + RefreshMovementSpeedModifiers(target); + RefreshFrictionModifiers(target); + } + public void RefreshWeightlessModifiers(EntityUid uid, MovementSpeedModifierComponent? move = null) { if (!Resolve(uid, ref move, false)) diff --git a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.cs b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.cs index cdd366ba50..470747fa3f 100644 --- a/Content.Shared/Nutrition/EntitySystems/IngestionSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/IngestionSystem.cs @@ -249,17 +249,17 @@ public sealed partial class IngestionSystem : EntitySystem // Can we digest the specific item we're trying to eat? if (!IsDigestibleBy(args.Ingested, stomachs)) { + if (!args.Ingest) + return; + if (forceFed) - { _popup.PopupClient(Loc.GetString("ingestion-cant-digest-other", ("target", entity), ("entity", food)), entity, args.User); - } else _popup.PopupClient(Loc.GetString("ingestion-cant-digest", ("entity", food)), entity, entity); return; } - // Exit early if we're just trying to get verbs if (!args.Ingest) { @@ -466,7 +466,10 @@ public sealed partial class IngestionSystem : EntitySystem } else { - _popup.PopupClient(Loc.GetString(edible.Message, ("food", entity.Owner), ("flavors", flavors)), args.User, args.User); + _popup.PopupPredicted(Loc.GetString(edible.Message, ("food", entity.Owner), ("flavors", flavors)), + Loc.GetString(edible.OtherMessage), + args.User, + args.User); // log successful voluntary eating _adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} ate {ToPrettyString(entity):food}"); diff --git a/Content.Shared/Nutrition/EntitySystems/OpenableSystem.cs b/Content.Shared/Nutrition/EntitySystems/OpenableSystem.cs index ab462557d9..588875d553 100644 --- a/Content.Shared/Nutrition/EntitySystems/OpenableSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/OpenableSystem.cs @@ -121,7 +121,7 @@ public sealed partial class OpenableSystem : EntitySystem private void OnTransferAttempt(Entity ent, ref SolutionTransferAttemptEvent args) { - if (ent.Comp.Opened) + if (!ent.Comp.Opened) args.Cancel(Loc.GetString(ent.Comp.ClosedPopup, ("owner", ent.Owner))); } diff --git a/Content.Shared/Nutrition/EntitySystems/SharedDrinkSystem.cs b/Content.Shared/Nutrition/EntitySystems/SharedDrinkSystem.cs index 303d94d55f..fe804dd2e6 100644 --- a/Content.Shared/Nutrition/EntitySystems/SharedDrinkSystem.cs +++ b/Content.Shared/Nutrition/EntitySystems/SharedDrinkSystem.cs @@ -141,8 +141,10 @@ public abstract partial class SharedDrinkSystem : EntitySystem } else { - _popup.PopupClient(Loc.GetString("edible-slurp", ("flavors", flavors)), args.User, args.User); - _popup.PopupEntity(Loc.GetString("edible-slurp"), args.User, Filter.PvsExcept(args.User), true); + _popup.PopupPredicted(Loc.GetString("edible-slurp", ("flavors", flavors)), + Loc.GetString("edible-slurp-other"), + args.User, + args.User); // log successful voluntary drinking _adminLogger.Add(LogType.Ingestion, LogImpact.Low, $"{ToPrettyString(args.User):target} drank {ToPrettyString(entity.Owner):drink}"); diff --git a/Content.Shared/Nutrition/Prototypes/EdiblePrototype.cs b/Content.Shared/Nutrition/Prototypes/EdiblePrototype.cs index 0f4c23846a..2cfe900167 100644 --- a/Content.Shared/Nutrition/Prototypes/EdiblePrototype.cs +++ b/Content.Shared/Nutrition/Prototypes/EdiblePrototype.cs @@ -21,11 +21,17 @@ public sealed partial class EdiblePrototype : IPrototype public SoundSpecifier UseSound = new SoundCollectionSpecifier("eating"); /// - /// The localization identifier for the ingestion message. + /// The localization identifier for the user's ingestion message. /// [DataField] public LocId Message; + /// + /// The localization identifier for an observer's or "others'" ingestion message. + /// + [DataField] + public LocId OtherMessage; + /// /// Localization verb used when consuming this item. /// diff --git a/Content.Shared/Roles/SharedRoleSystem.cs b/Content.Shared/Roles/SharedRoleSystem.cs index e45c5792bd..4f307d8b31 100644 --- a/Content.Shared/Roles/SharedRoleSystem.cs +++ b/Content.Shared/Roles/SharedRoleSystem.cs @@ -6,6 +6,7 @@ using Content.Shared.Database; using Content.Shared.GameTicking; using Content.Shared.Mind; using Content.Shared.Roles.Jobs; +using Content.Shared.Whitelist; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Configuration; @@ -19,13 +20,14 @@ namespace Content.Shared.Roles; public abstract class SharedRoleSystem : EntitySystem { - [Dependency] private readonly IConfigurationManager _cfg = default!; - [Dependency] private readonly IEntityManager _entityManager = default!; - [Dependency] private readonly IPrototypeManager _prototypes = default!; - [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] protected readonly ISharedPlayerManager Player = default!; - [Dependency] private readonly SharedAudioSystem _audio = default!; - [Dependency] private readonly SharedMindSystem _minds = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; + [Dependency] private readonly SharedMindSystem _minds = default!; + [Dependency] private readonly IPrototypeManager _prototypes = default!; private JobRequirementOverridePrototype? _requirementOverride; @@ -504,6 +506,20 @@ public abstract class SharedRoleSystem : EntitySystem return found; } + /// + /// Returns true if a mind has a role that matches a whitelist. + /// + public bool MindHasRole(Entity mind, EntityWhitelist whitelist) + { + foreach (var roleEnt in mind.Comp.MindRoles) + { + if (_whitelist.IsWhitelistPass(whitelist, roleEnt)) + return true; + } + + return false; + } + /// /// Finds the first mind role of a specific type on a mind entity. /// diff --git a/Content.Shared/Rootable/SharedRootableSystem.cs b/Content.Shared/Rootable/SharedRootableSystem.cs index c3deca0769..9165c3c111 100644 --- a/Content.Shared/Rootable/SharedRootableSystem.cs +++ b/Content.Shared/Rootable/SharedRootableSystem.cs @@ -1,6 +1,7 @@ using Content.Shared.Actions; using Content.Shared.Actions.Components; using Content.Shared.Alert; +using Content.Shared.Cloning.Events; using Content.Shared.Coordinates; using Content.Shared.Fluids.Components; using Content.Shared.Gravity; @@ -50,6 +51,20 @@ public abstract class SharedRootableSystem : EntitySystem SubscribeLocalEvent(OnIsWeightless); SubscribeLocalEvent(OnSlipAttempt); SubscribeLocalEvent(OnRefreshMovementSpeed); + SubscribeLocalEvent(OnCloning); + } + + private void OnCloning(Entity ent, ref CloningEvent args) + { + if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name)) + return; + + var cloneComp = EnsureComp(args.CloneUid); + cloneComp.TransferRate = ent.Comp.TransferRate; + cloneComp.TransferFrequency = ent.Comp.TransferFrequency; + cloneComp.SpeedModifier = ent.Comp.SpeedModifier; + cloneComp.RootSound = ent.Comp.RootSound; + Dirty(args.CloneUid, cloneComp); } private void OnRootableMapInit(Entity entity, ref MapInitEvent args) @@ -68,6 +83,7 @@ public abstract class SharedRootableSystem : EntitySystem var actions = new Entity(entity, comp); _actions.RemoveAction(actions, entity.Comp.ActionEntity); + _alerts.ClearAlert(entity, entity.Comp.RootedAlert); } private void OnRootableToggle(Entity entity, ref ToggleActionEvent args) diff --git a/Content.Shared/Sericulture/SericultureSystem.cs b/Content.Shared/Sericulture/SericultureSystem.cs index e5942a433e..e6086e67c2 100644 --- a/Content.Shared/Sericulture/SericultureSystem.cs +++ b/Content.Shared/Sericulture/SericultureSystem.cs @@ -38,7 +38,7 @@ public abstract partial class SharedSericultureSystem : EntitySystem private void OnClone(Entity ent, ref CloningEvent args) { - if(!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name)) + if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name)) return; var comp = EnsureComp(args.CloneUid); diff --git a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs index 265b46d24f..d76f16c446 100644 --- a/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs +++ b/Content.Shared/Silicons/StationAi/SharedStationAiSystem.cs @@ -1,6 +1,7 @@ using Content.Shared.ActionBlocker; using Content.Shared.Actions; using Content.Shared.Administration.Managers; +using Content.Shared.Chat.Prototypes; using Content.Shared.Containers.ItemSlots; using Content.Shared.Database; using Content.Shared.Doors.Systems; @@ -17,7 +18,6 @@ using Content.Shared.Power; using Content.Shared.Power.EntitySystems; using Content.Shared.StationAi; using Content.Shared.Verbs; -using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Map; @@ -27,8 +27,8 @@ using Robust.Shared.Physics; using Robust.Shared.Prototypes; using Robust.Shared.Serialization; using Robust.Shared.Timing; -using System.Diagnostics.CodeAnalysis; using Robust.Shared.Utility; +using System.Diagnostics.CodeAnalysis; namespace Content.Shared.Silicons.StationAi; @@ -70,6 +70,7 @@ public abstract partial class SharedStationAiSystem : EntitySystem private EntityQuery _gridQuery; private static readonly EntProtoId DefaultAi = "StationAiBrain"; + private readonly ProtoId _downloadChatNotificationPrototype = "IntellicardDownload"; private const float MaxVisionMultiplier = 5f; @@ -287,10 +288,10 @@ public abstract partial class SharedStationAiSystem : EntitySystem return; } - if (TryGetHeld((args.Target.Value, targetHolder), out var held) && _timing.CurTime > intelliComp.NextWarningAllowed) + if (TryGetHeld((args.Target.Value, targetHolder), out var held)) { - intelliComp.NextWarningAllowed = _timing.CurTime + intelliComp.WarningDelay; - AnnounceIntellicardUsage(held, intelliComp.WarningSound); + var ev = new ChatNotificationEvent(_downloadChatNotificationPrototype, args.Used, args.User); + RaiseLocalEvent(held, ref ev); } var doAfterArgs = new DoAfterArgs(EntityManager, args.User, cardHasAi ? intelliComp.UploadTime : intelliComp.DownloadTime, new IntellicardDoAfterEvent(), args.Target, ent.Owner) @@ -528,8 +529,6 @@ public abstract partial class SharedStationAiSystem : EntitySystem _appearance.SetData(entity.Owner, StationAiVisualState.Key, state); } - public virtual void AnnounceIntellicardUsage(EntityUid uid, SoundSpecifier? cue = null) { } - public virtual bool SetVisionEnabled(Entity entity, bool enabled, bool announce = false) { if (entity.Comp.Enabled == enabled) diff --git a/Content.Shared/Speech/SpeechComponent.cs b/Content.Shared/Speech/SpeechComponent.cs index 8c12fc918a..fddb41753e 100644 --- a/Content.Shared/Speech/SpeechComponent.cs +++ b/Content.Shared/Speech/SpeechComponent.cs @@ -16,29 +16,26 @@ namespace Content.Shared.Speech [Access(typeof(SpeechSystem), Friend = AccessPermissions.ReadWrite, Other = AccessPermissions.Read)] public bool Enabled = true; - [ViewVariables(VVAccess.ReadWrite)] - [DataField] + [DataField, AutoNetworkedField] public ProtoId? SpeechSounds; /// /// What speech verb prototype should be used by default for displaying this entity's messages? /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField] + [DataField, AutoNetworkedField] public ProtoId SpeechVerb = "Default"; /// /// What emotes allowed to use event if emote is false /// - [ViewVariables(VVAccess.ReadWrite)] - [DataField] + [DataField, AutoNetworkedField] public List> AllowedEmotes = new(); /// /// A mapping from chat suffixes loc strings to speech verb prototypes that should be conditionally used. /// For things like '?' changing to 'asks' or '!!' making text bold and changing to 'yells'. Can be overridden if necessary. /// - [DataField] + [DataField, AutoNetworkedField] public Dictionary> SuffixSpeechVerbs = new() { { "chat-speech-verb-suffix-exclamation-strong", "DefaultExclamationStrong" }, @@ -51,7 +48,6 @@ namespace Content.Shared.Speech [DataField] public AudioParams AudioParams = AudioParams.Default.WithVolume(-2f).WithRolloffFactor(4.5f); - [ViewVariables(VVAccess.ReadWrite)] [DataField] public float SoundCooldownTime { get; set; } = 0.5f; diff --git a/Content.Shared/Sprite/SharedScaleVisualsSystem.cs b/Content.Shared/Sprite/SharedScaleVisualsSystem.cs index fd2a522cd0..408f5984b1 100644 --- a/Content.Shared/Sprite/SharedScaleVisualsSystem.cs +++ b/Content.Shared/Sprite/SharedScaleVisualsSystem.cs @@ -27,6 +27,7 @@ public abstract class SharedScaleVisualsSystem : EntitySystem protected virtual void ResetScale(Entity ent) { + _appearance.RemoveData(ent.Owner, ScaleVisuals.Scale); var ev = new ScaleEntityEvent(ent.Owner, Vector2.One); RaiseLocalEvent(ent.Owner, ref ev); } diff --git a/Content.Server/Station/Components/StationDataComponent.cs b/Content.Shared/Station/Components/StationDataComponent.cs similarity index 55% rename from Content.Server/Station/Components/StationDataComponent.cs rename to Content.Shared/Station/Components/StationDataComponent.cs index d154c6936d..ef01525d31 100644 --- a/Content.Server/Station/Components/StationDataComponent.cs +++ b/Content.Shared/Station/Components/StationDataComponent.cs @@ -1,26 +1,23 @@ -using Content.Server.Shuttles.Systems; -using Content.Server.Station.Systems; -using Robust.Shared.Serialization.TypeSerializers.Implementations; -using Robust.Shared.Utility; +using Robust.Shared.GameStates; -namespace Content.Server.Station.Components; +namespace Content.Shared.Station.Components; /// /// Stores core information about a station, namely its config and associated grids. /// All station entities will have this component. /// -[RegisterComponent, Access(typeof(StationSystem))] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedStationSystem))] public sealed partial class StationDataComponent : Component { /// /// The game map prototype, if any, associated with this station. /// - [DataField("stationConfig")] - public StationConfig? StationConfig = null; + [DataField] + public StationConfig? StationConfig; /// /// List of all grids this station is part of. /// - [DataField("grids")] + [DataField, AutoNetworkedField] public HashSet Grids = new(); } diff --git a/Content.Shared/Station/SharedStationSystem.Tracker.cs b/Content.Shared/Station/SharedStationSystem.Tracker.cs new file mode 100644 index 0000000000..770420d47e --- /dev/null +++ b/Content.Shared/Station/SharedStationSystem.Tracker.cs @@ -0,0 +1,88 @@ +using Content.Shared.Station.Components; +using JetBrains.Annotations; +using Robust.Shared.Map; + +namespace Content.Shared.Station; + +public abstract partial class SharedStationSystem +{ + private void InitializeTracker() + { + SubscribeLocalEvent(OnTrackerMapInit); + SubscribeLocalEvent(OnTrackerRemove); + SubscribeLocalEvent(OnTrackerGridChanged); + SubscribeLocalEvent(OnMetaFlagRemoveAttempt); + } + + private void OnTrackerMapInit(Entity ent, ref MapInitEvent args) + { + _meta.AddFlag(ent, MetaDataFlags.ExtraTransformEvents); + UpdateStationTracker(ent.AsNullable()); + } + + private void OnTrackerRemove(Entity ent, ref ComponentRemove args) + { + _meta.RemoveFlag(ent, MetaDataFlags.ExtraTransformEvents); + } + + private void OnTrackerGridChanged(Entity ent, ref GridUidChangedEvent args) + { + UpdateStationTracker((ent, ent.Comp, args.Transform)); + } + + private void OnMetaFlagRemoveAttempt(Entity ent, ref MetaFlagRemoveAttemptEvent args) + { + if ((args.ToRemove & MetaDataFlags.ExtraTransformEvents) != 0 && + ent.Comp.LifeStage <= ComponentLifeStage.Running) + { + args.ToRemove &= ~MetaDataFlags.ExtraTransformEvents; + } + } + + /// + /// Updates the station tracker component based on entity's current location. + /// + [PublicAPI] + public void UpdateStationTracker(Entity ent) + { + if (!Resolve(ent, ref ent.Comp1)) + return; + + var xform = ent.Comp2; + + if (!_xformQuery.Resolve(ent, ref xform)) + return; + + // Entity is in nullspace or not on a grid + if (xform.MapID == MapId.Nullspace || xform.GridUid == null) + { + SetStation(ent, null); + return; + } + + // Check if the grid is part of a station + if (!_stationMemberQuery.TryGetComponent(xform.GridUid.Value, out var stationMember)) + { + SetStation(ent, null); + return; + } + + SetStation(ent, stationMember.Station); + } + + /// + /// Sets the station for a StationTrackerComponent. + /// + [PublicAPI] + public void SetStation(Entity ent, EntityUid? station) + { + if (!Resolve(ent, ref ent.Comp)) + return; + + if (ent.Comp.Station == station) + return; + + ent.Comp.Station = station; + Dirty(ent); + } +} diff --git a/Content.Shared/Station/SharedStationSystem.cs b/Content.Shared/Station/SharedStationSystem.cs index c067af610d..afd2e77258 100644 --- a/Content.Shared/Station/SharedStationSystem.cs +++ b/Content.Shared/Station/SharedStationSystem.cs @@ -1,11 +1,14 @@ +using System.Linq; using Content.Shared.Station.Components; using JetBrains.Annotations; using Robust.Shared.Map; +using Robust.Shared.Map.Components; namespace Content.Shared.Station; public abstract partial class SharedStationSystem : EntitySystem { + [Dependency] private readonly SharedMapSystem _map = default!; [Dependency] private readonly MetaDataSystem _meta = default!; private EntityQuery _xformQuery; @@ -16,96 +19,164 @@ public abstract partial class SharedStationSystem : EntitySystem { base.Initialize(); + InitializeTracker(); + _xformQuery = GetEntityQuery(); _stationMemberQuery = GetEntityQuery(); - - SubscribeLocalEvent(OnTrackerMapInit); - SubscribeLocalEvent(OnTrackerRemove); - SubscribeLocalEvent(OnTrackerGridChanged); - SubscribeLocalEvent(OnMetaFlagRemoveAttempt); - } - - private void OnTrackerMapInit(Entity ent, ref MapInitEvent args) - { - _meta.AddFlag(ent, MetaDataFlags.ExtraTransformEvents); - UpdateStationTracker(ent.AsNullable()); - } - - private void OnTrackerRemove(Entity ent, ref ComponentRemove args) - { - _meta.RemoveFlag(ent, MetaDataFlags.ExtraTransformEvents); - } - - private void OnTrackerGridChanged(Entity ent, ref GridUidChangedEvent args) - { - UpdateStationTracker((ent, ent.Comp, args.Transform)); - } - - private void OnMetaFlagRemoveAttempt(Entity ent, ref MetaFlagRemoveAttemptEvent args) - { - if ((args.ToRemove & MetaDataFlags.ExtraTransformEvents) != 0 && - ent.Comp.LifeStage <= ComponentLifeStage.Running) - { - args.ToRemove &= ~MetaDataFlags.ExtraTransformEvents; - } } /// - /// Updates the station tracker component based on entity's current location. + /// Gets the largest member grid from a station. /// - [PublicAPI] - public void UpdateStationTracker(Entity ent) - { - if (!Resolve(ent, ref ent.Comp1)) - return; - - var xform = ent.Comp2; - - if (!_xformQuery.Resolve(ent, ref xform)) - return; - - // Entity is in nullspace or not on a grid - if (xform.MapID == MapId.Nullspace || xform.GridUid == null) - { - SetStation(ent, null); - return; - } - - // Check if the grid is part of a station - if (!_stationMemberQuery.TryGetComponent(xform.GridUid.Value, out var stationMember)) - { - SetStation(ent, null); - return; - } - - SetStation(ent, stationMember.Station); - } - - /// - /// Sets the station for a StationTrackerComponent. - /// - [PublicAPI] - public void SetStation(Entity ent, EntityUid? station) + public EntityUid? GetLargestGrid(Entity ent) { if (!Resolve(ent, ref ent.Comp)) - return; + return null; - if (ent.Comp.Station == station) - return; + EntityUid? largestGrid = null; + Box2 largestBounds = new Box2(); - ent.Comp.Station = station; - Dirty(ent); + foreach (var gridUid in ent.Comp.Grids) + { + if (!TryComp(gridUid, out var grid) || + grid.LocalAABB.Size.LengthSquared() < largestBounds.Size.LengthSquared()) + continue; + + largestBounds = grid.LocalAABB; + largestGrid = gridUid; + } + + return largestGrid; } /// - /// Gets the station an entity is currently on, if any. + /// Returns the total number of tiles contained in the station's grids. /// - [PublicAPI] - public EntityUid? GetCurrentStation(Entity ent) + public int GetTileCount(Entity ent) { - if (!Resolve(ent, ref ent.Comp, logMissing: false)) + if (!Resolve(ent, ref ent.Comp)) + return 0; + + var count = 0; + foreach (var gridUid in ent.Comp.Grids) + { + if (!TryComp(gridUid, out var grid)) + continue; + + count += _map.GetAllTiles(gridUid, grid).Count(); + } + + return count; + } + + [PublicAPI] + public EntityUid? GetOwningStation(EntityUid? entity, TransformComponent? xform = null) + { + if (entity == null) return null; - return ent.Comp.Station; + return GetOwningStation(entity.Value, xform); + } + + /// + /// Gets the station that "owns" the given entity (essentially, the station the grid it's on is attached to) + /// + /// Entity to find the owner of. + /// Resolve pattern, transform of the entity. + /// The owning station, if any. + /// + /// This does not remember what station an entity started on, it simply checks where it is currently located. + /// + public EntityUid? GetOwningStation(EntityUid entity, TransformComponent? xform = null) + { + if (!Resolve(entity, ref xform)) + throw new ArgumentException("Tried to use an abstract entity!", nameof(entity)); + + if (TryComp(entity, out var stationTracker)) + { + // We have a specific station we are tracking and are tethered to. + return stationTracker.Station; + } + + if (HasComp(entity)) + { + // We are the station, just return ourselves. + return entity; + } + + if (HasComp(entity)) + { + // We are the station, just check ourselves. + return CompOrNull(entity)?.Station; + } + + if (xform.GridUid == EntityUid.Invalid) + { + Log.Debug("Unable to get owning station - GridUid invalid."); + return null; + } + + return CompOrNull(xform.GridUid)?.Station; + } + + public List GetStations() + { + var stations = new List(); + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _)) + { + stations.Add(uid); + } + + return stations; + } + + public HashSet GetStationsSet() + { + var stations = new HashSet(); + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out _)) + { + stations.Add(uid); + } + + return stations; + } + + public List<(string Name, NetEntity Entity)> GetStationNames() + { + var stations = GetStationsSet(); + var stats = new List<(string Name, NetEntity Station)>(); + + foreach (var weh in stations) + { + stats.Add((MetaData(weh).EntityName, GetNetEntity(weh))); + } + + return stats; + } + + /// + /// Returns the first station that has a grid in a certain map. + /// If the map has no stations, null is returned instead. + /// + /// + /// If there are multiple stations on a map it is probably arbitrary which one is returned. + /// + public EntityUid? GetStationInMap(MapId map) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var data)) + { + foreach (var gridUid in data.Grids) + { + if (Transform(gridUid).MapID == map) + { + return uid; + } + } + } + + return null; } } diff --git a/Content.Server/Station/StationConfig.cs b/Content.Shared/Station/StationConfig.cs similarity index 59% rename from Content.Server/Station/StationConfig.cs rename to Content.Shared/Station/StationConfig.cs index 8bc0caf1aa..5bdbf77422 100644 --- a/Content.Server/Station/StationConfig.cs +++ b/Content.Shared/Station/StationConfig.cs @@ -1,17 +1,15 @@ -using Content.Server.Maps.NameGenerators; -using JetBrains.Annotations; -using Robust.Shared.Prototypes; +using Robust.Shared.Prototypes; -namespace Content.Server.Station; +namespace Content.Shared.Station; /// /// A config for a station. Specifies name and component modifications. /// -[DataDefinition, PublicAPI] +[DataDefinition] public sealed partial class StationConfig { [DataField("stationProto", required: true)] - public string StationPrototype = default!; + public EntProtoId StationPrototype; [DataField("components", required: true)] public ComponentRegistry StationComponentOverrides = default!; diff --git a/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs b/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs index 2b73a9349f..fd7cb87b2a 100644 --- a/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs +++ b/Content.Shared/Storage/EntitySystems/SharedEntityStorageSystem.cs @@ -16,6 +16,7 @@ using Content.Shared.Verbs; using Content.Shared.Wall; using Content.Shared.Whitelist; using Content.Shared.ActionBlocker; +using Content.Shared.Mobs.Components; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.GameStates; @@ -355,7 +356,7 @@ public abstract class SharedEntityStorageSystem : EntitySystem return _whitelistSystem.IsValid(component.Whitelist, toInsert); // The inserted entity must be a mob or an item. - return HasComp(toInsert) || HasComp(toInsert); + return HasComp(toInsert) || HasComp(toInsert); } public bool TryOpenStorage(EntityUid user, EntityUid target, bool silent = false) diff --git a/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs b/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs index f3c9055910..74c47bbb25 100644 --- a/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs +++ b/Content.Shared/Storage/EntitySystems/SharedStorageSystem.cs @@ -232,7 +232,14 @@ public abstract class SharedStorageSystem : EntitySystem StoredItems = storedItems, SavedLocations = component.SavedLocations, Whitelist = component.Whitelist, - Blacklist = component.Blacklist + Blacklist = component.Blacklist, + QuickInsert = component.QuickInsert, + AreaInsert = component.AreaInsert, + StorageInsertSound = component.StorageInsertSound, + StorageRemoveSound = component.StorageRemoveSound, + StorageOpenSound = component.StorageOpenSound, + StorageCloseSound = component.StorageCloseSound, + DefaultStorageOrientation = component.DefaultStorageOrientation, }; } @@ -348,6 +355,44 @@ public abstract class SharedStorageSystem : EntitySystem args.Verbs.Add(verb); } + /// + /// Copy this component's datafields from one entity to another. + /// This can't use CopyComp because we don't want to copy the references to the items inside the storage. + /// + public void CopyComponent(Entity source, EntityUid target) + { + if (!Resolve(source, ref source.Comp)) + return; + + var targetComp = EnsureComp(target); + targetComp.Grid = new List(source.Comp.Grid); + targetComp.MaxItemSize = source.Comp.MaxItemSize; + targetComp.QuickInsert = source.Comp.QuickInsert; + targetComp.QuickInsertCooldown = source.Comp.QuickInsertCooldown; + targetComp.OpenUiCooldown = source.Comp.OpenUiCooldown; + targetComp.ClickInsert = source.Comp.ClickInsert; + targetComp.OpenOnActivate = source.Comp.OpenOnActivate; + targetComp.AreaInsert = source.Comp.AreaInsert; + targetComp.AreaInsertRadius = source.Comp.AreaInsertRadius; + targetComp.Whitelist = source.Comp.Whitelist; + targetComp.Blacklist = source.Comp.Blacklist; + targetComp.StorageInsertSound = source.Comp.StorageInsertSound; + targetComp.StorageRemoveSound = source.Comp.StorageRemoveSound; + targetComp.StorageOpenSound = source.Comp.StorageOpenSound; + targetComp.StorageCloseSound = source.Comp.StorageCloseSound; + targetComp.DefaultStorageOrientation = source.Comp.DefaultStorageOrientation; + targetComp.HideStackVisualsWhenClosed = source.Comp.HideStackVisualsWhenClosed; + targetComp.SilentStorageUserTag = source.Comp.SilentStorageUserTag; + targetComp.ShowVerb = source.Comp.ShowVerb; + + UpdateOccupied((target, targetComp)); + Dirty(target, targetComp); + + var targetUI = EnsureComp(target); + + UI.SetUi((target, targetUI), StorageComponent.StorageUiKey.Key, new InterfaceData("StorageBoundUserInterface")); + } + /// /// Tries to get the storage location of an item. /// @@ -1296,7 +1341,7 @@ public abstract class SharedStorageSystem : EntitySystem Angle startAngle; if (storageEnt.Comp.DefaultStorageOrientation == null) { - startAngle = Angle.FromDegrees(-itemEnt.Comp.StoredRotation); + startAngle = Angle.Zero; } else { @@ -1957,15 +2002,17 @@ public abstract class SharedStorageSystem : EntitySystem protected sealed class StorageComponentState : ComponentState { public Dictionary StoredItems = new(); - public Dictionary> SavedLocations = new(); - public List Grid = new(); - public ProtoId? MaxItemSize; - public EntityWhitelist? Whitelist; - public EntityWhitelist? Blacklist; + public bool QuickInsert; + public bool AreaInsert; + public SoundSpecifier? StorageInsertSound; + public SoundSpecifier? StorageRemoveSound; + public SoundSpecifier? StorageOpenSound; + public SoundSpecifier? StorageCloseSound; + public StorageDefaultOrientation? DefaultStorageOrientation; } } diff --git a/Content.Shared/Trigger/Components/Effects/DnaScrambleOnTriggerComponent.cs b/Content.Shared/Trigger/Components/Effects/DnaScrambleOnTriggerComponent.cs new file mode 100644 index 0000000000..1f3767b392 --- /dev/null +++ b/Content.Shared/Trigger/Components/Effects/DnaScrambleOnTriggerComponent.cs @@ -0,0 +1,11 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Trigger.Components.Effects; + +/// +/// Scrambles the entity's identity and DNA, turning them into a randomized humanoid of the same species. +/// If TargetUser is true the user will be scrambled instead. +/// Used for dna scrambler implants. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class DnaScrambleOnTriggerComponent : BaseXOnTriggerComponent; diff --git a/Content.Shared/Trigger/Components/Effects/RattleOnTriggerComponent.cs b/Content.Shared/Trigger/Components/Effects/RattleOnTriggerComponent.cs index 599a64339a..fa1175c3cb 100644 --- a/Content.Shared/Trigger/Components/Effects/RattleOnTriggerComponent.cs +++ b/Content.Shared/Trigger/Components/Effects/RattleOnTriggerComponent.cs @@ -24,7 +24,7 @@ public sealed partial class RattleOnTriggerComponent : BaseXOnTriggerComponent [DataField] public Dictionary Messages = new() { - {MobState.Critical, "deathrattle-implant-critical-message"}, - {MobState.Dead, "deathrattle-implant-dead-message"} + {MobState.Critical, "rattle-on-trigger-critical-message"}, + {MobState.Dead, "rattle-on-trigger-dead-message"} }; } diff --git a/Content.Shared/Trigger/Components/Effects/ScramOnTriggerComponent.cs b/Content.Shared/Trigger/Components/Effects/ScramOnTriggerComponent.cs new file mode 100644 index 0000000000..bacf0f69e8 --- /dev/null +++ b/Content.Shared/Trigger/Components/Effects/ScramOnTriggerComponent.cs @@ -0,0 +1,25 @@ +using Robust.Shared.Audio; +using Robust.Shared.GameStates; + +namespace Content.Shared.Trigger.Components.Effects; + +/// +/// Randomly teleports the entity when triggered. +/// If TargetUser is true the user will be teleported instead. +/// Used for scram implants. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class ScramOnTriggerComponent : BaseXOnTriggerComponent +{ + /// + /// Up to how far to teleport the entity. + /// + [DataField, AutoNetworkedField] + public float TeleportRadius = 100f; + + /// + /// the sound to play when teleporting. + /// + [DataField, AutoNetworkedField] + public SoundSpecifier TeleportSound = new SoundPathSpecifier("/Audio/Effects/teleport_arrival.ogg"); +} diff --git a/Content.Shared/Trigger/Components/Effects/UncuffOnTriggerComponent.cs b/Content.Shared/Trigger/Components/Effects/UncuffOnTriggerComponent.cs new file mode 100644 index 0000000000..770882f3e6 --- /dev/null +++ b/Content.Shared/Trigger/Components/Effects/UncuffOnTriggerComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Trigger.Components.Effects; + +/// +/// Removes a pair of handcuffs from the entity. +/// If TargetUser is true the user will be uncuffed instead. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class UncuffOnTriggerComponent : BaseXOnTriggerComponent; diff --git a/Content.Shared/Trigger/Systems/DnaScrambleOnTriggerSystem.cs b/Content.Shared/Trigger/Systems/DnaScrambleOnTriggerSystem.cs new file mode 100644 index 0000000000..246c6a8c7a --- /dev/null +++ b/Content.Shared/Trigger/Systems/DnaScrambleOnTriggerSystem.cs @@ -0,0 +1,62 @@ +using Content.Shared.DetailExaminable; +using Content.Shared.Forensics.Systems; +using Content.Shared.Humanoid; +using Content.Shared.IdentityManagement; +using Content.Shared.Preferences; +using Content.Shared.Popups; +using Content.Shared.Trigger.Components.Effects; +using Robust.Shared.Network; + +namespace Content.Shared.Trigger.Systems; + +public sealed class DnaScrambleOnTriggerSystem : EntitySystem +{ + [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly SharedHumanoidAppearanceSystem _humanoidAppearance = default!; + [Dependency] private readonly SharedIdentitySystem _identity = default!; + [Dependency] private readonly SharedForensicsSystem _forensics = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly INetManager _net = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnTrigger); + } + + private void OnTrigger(Entity ent, ref TriggerEvent args) + { + if (args.Key != null && !ent.Comp.KeysIn.Contains(args.Key)) + return; + + var target = ent.Comp.TargetUser ? args.User : ent.Owner; + + if (target == null) + return; + + if (!TryComp(target, out var humanoid)) + return; + + args.Handled = true; + + // Randomness will mispredict + // and LoadProfile causes a debug assert on the client at the moment. + if (_net.IsClient) + return; + + var newProfile = HumanoidCharacterProfile.RandomWithSpecies(humanoid.Species); + _humanoidAppearance.LoadProfile(target.Value, newProfile, humanoid); + _metaData.SetEntityName(target.Value, newProfile.Name, raiseEvents: false); // raising events would update ID card, station record, etc. + + // If the entity has the respective components, then scramble the dna and fingerprint strings. + _forensics.RandomizeDNA(target.Value); + _forensics.RandomizeFingerprint(target.Value); + + RemComp(target.Value); // remove MRP+ custom description if one exists + _identity.QueueIdentityUpdate(target.Value); // manually queue identity update since we don't raise the event + + // Can't use PopupClient or PopupPredicted because the trigger might be unpredicted. + _popup.PopupEntity(Loc.GetString("scramble-on-trigger-popup"), target.Value, target.Value); + } +} diff --git a/Content.Shared/Trigger/Systems/ScramOnTriggerSystem.cs b/Content.Shared/Trigger/Systems/ScramOnTriggerSystem.cs new file mode 100644 index 0000000000..163012cec5 --- /dev/null +++ b/Content.Shared/Trigger/Systems/ScramOnTriggerSystem.cs @@ -0,0 +1,151 @@ +using System.Numerics; +using Content.Shared.Movement.Pulling.Components; +using Content.Shared.Movement.Pulling.Systems; +using Content.Shared.Physics; +using Content.Shared.Trigger.Components.Effects; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Network; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Components; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Collections; +using Robust.Shared.Random; + +namespace Content.Shared.Trigger.Systems; + +public sealed class ScramOnTriggerSystem : EntitySystem +{ + [Dependency] private readonly PullingSystem _pulling = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly SharedMapSystem _map = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly INetManager _net = default!; + + private EntityQuery _physicsQuery; + private HashSet> _targetGrids = new(); + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnTrigger); + + _physicsQuery = GetEntityQuery(); + } + + private void OnTrigger(Entity ent, ref TriggerEvent args) + { + if (args.Key != null && !ent.Comp.KeysIn.Contains(args.Key)) + return; + + var target = ent.Comp.TargetUser ? args.User : ent.Owner; + + if (target == null) + return; + + // We need stop the user from being pulled so they don't just get "attached" with whoever is pulling them. + // This can for example happen when the user is cuffed and being pulled. + if (TryComp(target, out var pull) && _pulling.IsPulled(target.Value, pull)) + _pulling.TryStopPull(ent, pull); + + // Check if the user is pulling anything, and drop it if so. + if (TryComp(target, out var puller) && TryComp(puller.Pulling, out var pullable)) + _pulling.TryStopPull(puller.Pulling.Value, pullable); + + _audio.PlayPredicted(ent.Comp.TeleportSound, ent, args.User); + + // Can't predict picking random grids and the target location might be out of PVS range. + if (_net.IsClient) + return; + + var xform = Transform(target.Value); + var targetCoords = SelectRandomTileInRange(xform, ent.Comp.TeleportRadius); + + if (targetCoords != null) + { + _transform.SetCoordinates(target.Value, targetCoords.Value); + args.Handled = true; + } + } + + private EntityCoordinates? SelectRandomTileInRange(TransformComponent userXform, float radius) + { + var userCoords = _transform.ToMapCoordinates(userXform.Coordinates); + _targetGrids.Clear(); + _lookup.GetEntitiesInRange(userCoords, radius, _targetGrids); + Entity? targetGrid = null; + + if (_targetGrids.Count == 0) + return null; + + // Give preference to the grid the entity is currently on. + // This does not guarantee that if the probability fails that the owner's grid won't be picked. + // In reality the probability is higher and depends on the number of grids. + if (userXform.GridUid != null && TryComp(userXform.GridUid, out var gridComp)) + { + var userGrid = new Entity(userXform.GridUid.Value, gridComp); + if (_random.Prob(0.5f)) + { + _targetGrids.Remove(userGrid); + targetGrid = userGrid; + } + } + + if (targetGrid == null) + targetGrid = _random.GetRandom().PickAndTake(_targetGrids); + + EntityCoordinates? targetCoords = null; + + do + { + var valid = false; + + var range = (float)Math.Sqrt(radius); + var box = Box2.CenteredAround(userCoords.Position, new Vector2(range, range)); + var tilesInRange = _map.GetTilesEnumerator(targetGrid.Value.Owner, targetGrid.Value.Comp, box, false); + var tileList = new ValueList(); + + while (tilesInRange.MoveNext(out var tile)) + { + tileList.Add(tile.GridIndices); + } + + while (tileList.Count != 0) + { + var tile = tileList.RemoveSwap(_random.Next(tileList.Count)); + valid = true; + foreach (var entity in _map.GetAnchoredEntities(targetGrid.Value.Owner, targetGrid.Value.Comp, + tile)) + { + if (!_physicsQuery.TryGetComponent(entity, out var body)) + continue; + + if (body.BodyType != BodyType.Static || + !body.Hard || + (body.CollisionLayer & (int)CollisionGroup.MobMask) == 0) + continue; + + valid = false; + break; + } + + if (valid) + { + targetCoords = new EntityCoordinates(targetGrid.Value.Owner, + _map.TileCenterToVector(targetGrid.Value, tile)); + break; + } + } + + if (valid || _targetGrids.Count == 0) // if we don't do the check here then PickAndTake will blow up on an empty set. + break; + + targetGrid = _random.GetRandom().PickAndTake(_targetGrids); + } while (true); + + return targetCoords; + } +} diff --git a/Content.Shared/Trigger/Systems/UncuffOnTriggerSystem.cs b/Content.Shared/Trigger/Systems/UncuffOnTriggerSystem.cs new file mode 100644 index 0000000000..9b83c4cf8e --- /dev/null +++ b/Content.Shared/Trigger/Systems/UncuffOnTriggerSystem.cs @@ -0,0 +1,34 @@ +using Content.Shared.Cuffs; +using Content.Shared.Cuffs.Components; +using Content.Shared.Trigger.Components.Effects; + +namespace Content.Shared.Trigger.Systems; + +public sealed class UncuffOnTriggerSystem : EntitySystem +{ + [Dependency] private readonly SharedCuffableSystem _cuffable = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnTrigger); + } + + private void OnTrigger(Entity ent, ref TriggerEvent args) + { + if (args.Key != null && !ent.Comp.KeysIn.Contains(args.Key)) + return; + + var target = ent.Comp.TargetUser ? args.User : ent.Owner; + + if (target == null) + return; + + if (!TryComp(target.Value, out var cuffs) || cuffs.Container.ContainedEntities.Count < 1) + return; + + _cuffable.Uncuff(target.Value, args.User, cuffs.LastAddedCuffs); + args.Handled = true; + } +} diff --git a/Content.Shared/Turrets/StationAiTurretComponent.cs b/Content.Shared/Turrets/StationAiTurretComponent.cs new file mode 100644 index 0000000000..125195787e --- /dev/null +++ b/Content.Shared/Turrets/StationAiTurretComponent.cs @@ -0,0 +1,12 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Turrets; + +/// +/// This component designates a turret that is under the direct control of the station AI. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class StationAiTurretComponent : Component +{ + +} diff --git a/Content.Shared/Wieldable/Components/IncreaseDamageOnWieldComponent.cs b/Content.Shared/Wieldable/Components/IncreaseDamageOnWieldComponent.cs index 86afe1897b..52b087c5b9 100644 --- a/Content.Shared/Wieldable/Components/IncreaseDamageOnWieldComponent.cs +++ b/Content.Shared/Wieldable/Components/IncreaseDamageOnWieldComponent.cs @@ -6,5 +6,6 @@ namespace Content.Shared.Wieldable.Components; public sealed partial class IncreaseDamageOnWieldComponent : Component { [DataField("damage", required: true)] + [Access(Other = AccessPermissions.ReadExecute)] public DamageSpecifier BonusDamage = default!; } diff --git a/Resources/Audio/Ambience/Antag/attributions.yml b/Resources/Audio/Ambience/Antag/attributions.yml index c0c34eb880..5418d2a204 100644 --- a/Resources/Audio/Ambience/Antag/attributions.yml +++ b/Resources/Audio/Ambience/Antag/attributions.yml @@ -26,3 +26,8 @@ license: "CC-BY-SA-3.0" copyright: "Made by @ps3moira on Discord for SS14" source: "https://www.youtube.com/watch?v=jf1sYGYVLsw" +- files: ["changeling_start.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Taken from TG station at commit https://github.com/tgstation/tgstation/commit/436ba869ebcd0b60b63973fb7562f447ee655205" + source: "https://github.com/tgstation/tgstation/blob/master/sound/music/antag/ling_alert.ogg" + diff --git a/Resources/Audio/Ambience/Antag/changeling_start.ogg b/Resources/Audio/Ambience/Antag/changeling_start.ogg new file mode 100644 index 0000000000..afe654e4de Binary files /dev/null and b/Resources/Audio/Ambience/Antag/changeling_start.ogg differ diff --git a/Resources/Audio/Effects/Changeling/attributions.yml b/Resources/Audio/Effects/Changeling/attributions.yml new file mode 100644 index 0000000000..d7d7931cd6 --- /dev/null +++ b/Resources/Audio/Effects/Changeling/attributions.yml @@ -0,0 +1,19 @@ +- files: ["devour_suck.ogg"] + license: "CC0-1.0" + copyright: "4Cairnz on Freesound: June 5th 2023" + source: "https://freesound.org/people/4Cairnz/sounds/689640/" + +- files: ["devour_windup.ogg "] + license: "CC-BY-SA-3.0" + copyright: "Made by @DarkIcedCoffee on Discord for SS14, utilizing sounds from Caitlin_100, jedg and EricsSoundschmiede on freesound" + source: "https://youtu.be/iviCUO2xH_E" + +- files: ["devour_consume.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Made by @DarkIcedCoffee on Discord for SS14, utilizing sounds from jedg and reg7783 on freesound." + source: "https://youtu.be/iviCUO2xH_E" + +- files: ["changeling_transform.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Made by @DarkIcedCoffee on Discord for SS14" + source: "https://youtu.be/iviCUO2xH_E" diff --git a/Resources/Audio/Effects/Changeling/changeling_transform.ogg b/Resources/Audio/Effects/Changeling/changeling_transform.ogg new file mode 100644 index 0000000000..23379d246d Binary files /dev/null and b/Resources/Audio/Effects/Changeling/changeling_transform.ogg differ diff --git a/Resources/Audio/Effects/Changeling/devour_consume.ogg b/Resources/Audio/Effects/Changeling/devour_consume.ogg new file mode 100644 index 0000000000..9d05c9f541 Binary files /dev/null and b/Resources/Audio/Effects/Changeling/devour_consume.ogg differ diff --git a/Resources/Audio/Effects/Changeling/devour_suck.ogg b/Resources/Audio/Effects/Changeling/devour_suck.ogg new file mode 100644 index 0000000000..d756b0a55d Binary files /dev/null and b/Resources/Audio/Effects/Changeling/devour_suck.ogg differ diff --git a/Resources/Audio/Effects/Changeling/devour_windup.ogg b/Resources/Audio/Effects/Changeling/devour_windup.ogg new file mode 100644 index 0000000000..7c81d9a076 Binary files /dev/null and b/Resources/Audio/Effects/Changeling/devour_windup.ogg differ diff --git a/Resources/Audio/Lobby/attributions.yml b/Resources/Audio/Lobby/attributions.yml index ab96eb41a1..92c2e095b7 100644 --- a/Resources/Audio/Lobby/attributions.yml +++ b/Resources/Audio/Lobby/attributions.yml @@ -38,7 +38,7 @@ - files: ["title2.ogg"] license: "Custom" - copyright: "Originally composed by Jonathan Dunn for Robocop 2 Gameboy published by Ocean Software. Remixed by Eric Shumaker for his video Dilbert 3. As of October 9th, 2008 nobody owns the rights to this song and copyright action has never been taken against anyone using this song in their projects." + copyright: "Originally composed by Jonathan Dunn for RoboCop (Game Boy) published by Ocean Software. Remixed by Eric Shumaker for his video Dilbert 3. As of October 9th, 2008 nobody owns the rights to this song and copyright action has never been taken against anyone using this song in their projects." source: "https://www.youtube.com/watch?v=4o6_8Unj2mQ" - files: ["title3.ogg"] diff --git a/Resources/Changelog/Admin.yml b/Resources/Changelog/Admin.yml index fca3c513f7..988b9038a7 100644 --- a/Resources/Changelog/Admin.yml +++ b/Resources/Changelog/Admin.yml @@ -1315,5 +1315,20 @@ Entries: id: 160 time: '2025-08-06T17:23:06.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/39424 +- author: slarticodefast + changes: + - message: Paused maps from cryostorage and polymorphing are now named to make them + easier to find when debugging. + type: Tweak + id: 161 + time: '2025-08-07T14:55:26.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/39453 +- author: K-Dynamic + changes: + - message: Centcomm Official now starts with administration glasses. + type: Tweak + id: 162 + time: '2025-08-08T19:00:41.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/35531 Name: Admin Order: 2 diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 1286c1efd1..718d0605c9 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1,64 +1,4 @@ Entries: -- author: AgentSmithRadio - changes: - - message: New food crates are now available for purchase from the automated trade - station! - type: Add - - message: You can no longer use Sweetie's Pistachios to fulfill cargo's generic - fruit bounty. - type: Remove - id: 8321 - time: '2025-04-23T05:37:10.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/33286 -- author: SaphireLattice - changes: - - message: Steel sheets no longer have unintuitively wasteful plating placement - that can be done with steel tiles instead - type: Remove - id: 8322 - time: '2025-04-23T11:08:03.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/33443 -- author: ArchRBX - changes: - - message: pAI's can now install a variety of useful software through their new - Software Catalog. - type: Add - - message: pAI's no longer start with a Station Map or MIDI Player, and must purchase - these through the shop. - type: Tweak - id: 8323 - time: '2025-04-23T23:55:36.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/36857 -- author: NoElkaTheGod - changes: - - message: Toysword now actually looks almost like the real thing, as advertised - in the description - type: Tweak - id: 8324 - time: '2025-04-24T01:21:35.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/34199 -- author: Pronana - changes: - - message: Moths can now eat pills - type: Tweak - id: 8325 - time: '2025-04-24T02:28:34.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/35609 -- author: Coolsurf6 - changes: - - message: Updated more contraband items for Syndicate and Security. - type: Tweak - id: 8326 - time: '2025-04-24T02:57:54.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/35102 -- author: Beck Thompson - changes: - - message: You can how hide items inside cake by using a knife! Eating a cake with - an item inside will do minor damage. - type: Add - id: 8327 - time: '2025-04-24T03:00:51.0000000+00:00' - url: https://github.com/space-wizards/space-station-14/pull/31015 - author: RedBookcase changes: - message: Added Advanced Circular Saw as a Medical Doctor specific uplink item. @@ -3905,3 +3845,58 @@ id: 8833 time: '2025-08-06T16:58:07.0000000+00:00' url: https://github.com/space-wizards/space-station-14/pull/37363 +- author: slarticodefast + changes: + - message: Fixed inventory sprites flickering when hovering over the slot with an + item. + type: Fix + id: 8834 + time: '2025-08-06T20:14:29.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/39379 +- author: Thinbug0 + changes: + - message: New onion mutation, Bloonions! floating balloon-like onions. + type: Add + id: 8835 + time: '2025-08-07T01:51:30.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/33375 +- author: Kittygyat + changes: + - message: Made baseball bats 1x4 instead of 2x2 + type: Tweak + - message: New baseball bat inventory icon sprites + type: Add + id: 8836 + time: '2025-08-07T03:03:11.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/38392 +- author: lolman360 + changes: + - message: Fixed certain items (like pickaxes) failing to insert into containers + that have enough space to store them, sometimes. + type: Fix + id: 8837 + time: '2025-08-07T16:01:18.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/38896 +- author: chromiumboy + changes: + - message: The station AI will receive chat notifications when its sentry turrets + start firing upon hostile targets. + type: Add + id: 8838 + time: '2025-08-08T18:56:01.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/35277 +- author: K-Dynamic + changes: + - message: Captain and Head of Personnel starts with administration glasses and + hud respectively. + type: Add + id: 8839 + time: '2025-08-08T19:00:41.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/35531 +- author: perryprog + changes: + - message: The emote radial menu now shows the correct icons for its subcategories. + type: Fix + id: 8840 + time: '2025-08-08T19:27:46.0000000+00:00' + url: https://github.com/space-wizards/space-station-14/pull/39481 diff --git a/Resources/Locale/en-US/administration/antag.ftl b/Resources/Locale/en-US/administration/antag.ftl index 1433cc1dc4..161054aca2 100644 --- a/Resources/Locale/en-US/administration/antag.ftl +++ b/Resources/Locale/en-US/administration/antag.ftl @@ -7,6 +7,8 @@ admin-verb-make-pirate = Make the target into a pirate. Note this doesn't config admin-verb-make-head-rev = Make the target into a Head Revolutionary. admin-verb-make-thief = Make the target into a thief. admin-verb-make-paradox-clone = Create a Paradox Clone ghost role of the target. +admin-verb-make-changeling = Make the target into a Changeling. + admin-verb-text-make-traitor = Make Traitor admin-verb-text-make-initial-infected = Make Initial Infected @@ -16,5 +18,6 @@ admin-verb-text-make-pirate = Make Pirate admin-verb-text-make-head-rev = Make Head Rev admin-verb-text-make-thief = Make Thief admin-verb-text-make-paradox-clone = Create Paradox Clone +admin-verb-text-make-changeling = Make Changeling (WIP) admin-overlay-antag-classic = ANTAG diff --git a/Resources/Locale/en-US/bed/cryostorage/cryogenic-storage.ftl b/Resources/Locale/en-US/bed/cryostorage/cryogenic-storage.ftl index f966f30e2d..7d2e50e772 100644 --- a/Resources/Locale/en-US/bed/cryostorage/cryogenic-storage.ftl +++ b/Resources/Locale/en-US/bed/cryostorage/cryogenic-storage.ftl @@ -5,3 +5,5 @@ earlyleave-cryo-job-unknown = Unknown # {$entity} available for GENDER function purposes earlyleave-cryo-announcement = {$character} ({$job}) has entered cryogenic storage! earlyleave-cryo-sender = Station + +cryostorage-paused-map-name = Cryosleeper body storage map diff --git a/Resources/Locale/en-US/changeling/changeling.ftl b/Resources/Locale/en-US/changeling/changeling.ftl new file mode 100644 index 0000000000..423cb0811e --- /dev/null +++ b/Resources/Locale/en-US/changeling/changeling.ftl @@ -0,0 +1,22 @@ +roles-antag-changeling-name = Changeling +roles-antag-changeling-objective = A intelligent predator that assumes the identities of its victims. + +changeling-role-greeting = You are a Changeling, a highly intelligent predator. Your only goal is to escape the station alive via assuming the identities of the denizens of this station. You are hungry and will not make it long without sustenance... kill, consume, hide, survive. +changeling-briefing = You are a changeling, your goal is to survive. Consume humanoids to gain biomass and utilize it to evade termination. You are able to utilize and assume the identities of those you consume to evade a grim fate. + +changeling-devour-attempt-failed-rotting = This corpse has only rotted biomass. +changeling-devour-attempt-failed-protected = This victim's biomass is protected. + +changeling-devour-begin-windup-self = Our uncanny mouth reveals itself with otherworldly hunger. +changeling-devour-begin-windup-others = { CAPITALIZE(POSS-ADJ($user)) } uncanny mouth reveals itself with otherworldly hunger. +changeling-devour-begin-consume-self = The uncanny mouth digs deep into its victim. +changeling-devour-begin-consume-others = { CAPITALIZE(POSS-ADJ($user)) } uncanny mouth digs deep into { POSS-ADJ($user) } victim. + +changeling-devour-consume-failed-not-dead = This body yet lives! We cannot consume it alive! +changeling-devour-consume-complete-self = Our uncanny mouth retreats, biomass consumed. +changeling-devour-consume-complete-others = { CAPITALIZE(POSS-ADJ($user)) } uncanny mouth retreats. + +changeling-transform-attempt-self = Our bones snap, muscles tear, one flesh becomes another. +changeling-transform-attempt-others = { CAPITALIZE(POSS-ADJ($user)) } bones snap, muscles tear, body shifts into another. + +changeling-paused-map-name = Changeling identity storage map diff --git a/Resources/Locale/en-US/commands/stat-values-command.ftl b/Resources/Locale/en-US/commands/stat-values-command.ftl index 67a211adab..16b365897c 100644 --- a/Resources/Locale/en-US/commands/stat-values-command.ftl +++ b/Resources/Locale/en-US/commands/stat-values-command.ftl @@ -8,6 +8,16 @@ stat-cargo-values = Cargo sell prices stat-cargo-id = ID stat-cargo-price = Price +# Melee +stat-melee-values = Melee weapon damage +stat-melee-id = ID +stat-melee-base-damage = Base damage +stat-melee-wield-damage = Wielded damage +stat-melee-attack-rate = Attack rate +stat-melee-dps = DPS +stat-melee-structural-damage = Structure damage +stat-melee-structural-wield-damage = Wielded structure damage + # Lathe stat-lathe-values = Lathe sell prices stat-lathe-id = ID diff --git a/Resources/Locale/en-US/datasets/names/golem.ftl b/Resources/Locale/en-US/datasets/names/golem.ftl deleted file mode 100644 index 6a91ac773a..0000000000 --- a/Resources/Locale/en-US/datasets/names/golem.ftl +++ /dev/null @@ -1,1336 +0,0 @@ -names-golem-dataset-1 = Abelsonite -names-golem-dataset-2 = Abenakiite -names-golem-dataset-3 = Abernathyite -names-golem-dataset-4 = Abhurite -names-golem-dataset-5 = Abramovite -names-golem-dataset-6 = Abswurmbachite -names-golem-dataset-7 = Acanthite -names-golem-dataset-8 = Achavalite -names-golem-dataset-9 = Actinolite -names-golem-dataset-10 = Acuminite -names-golem-dataset-11 = Adamite -names-golem-dataset-12 = Adelite -names-golem-dataset-13 = Admontite -names-golem-dataset-14 = Aegirine -names-golem-dataset-15 = Aenigmatite -names-golem-dataset-16 = Aerinite -names-golem-dataset-17 = Aerugite -names-golem-dataset-18 = Aeschynite -names-golem-dataset-19 = Afghanite -names-golem-dataset-20 = Afwillite -names-golem-dataset-21 = Agardite -names-golem-dataset-22 = Agrellite -names-golem-dataset-23 = Agrinierite -names-golem-dataset-24 = Aguilarite -names-golem-dataset-25 = Aheylite -names-golem-dataset-26 = Ahlfeldite -names-golem-dataset-27 = Aikinite -names-golem-dataset-28 = Ajoite -names-golem-dataset-29 = Akaganeite -names-golem-dataset-30 = Akatoreite -names-golem-dataset-31 = Akdalaite -names-golem-dataset-32 = Åkermanite -names-golem-dataset-33 = Akhtenskite -names-golem-dataset-34 = Akimotoite -names-golem-dataset-35 = Akrochordite -names-golem-dataset-36 = Aksaite -names-golem-dataset-37 = Aktashite -names-golem-dataset-38 = Alabandite -names-golem-dataset-39 = Alacranite -names-golem-dataset-40 = Alamosite -names-golem-dataset-41 = Alarsite -names-golem-dataset-42 = Albite -names-golem-dataset-43 = Albrechtschraufite -names-golem-dataset-44 = Aldermanite -names-golem-dataset-45 = Aleksite -names-golem-dataset-46 = Alforsite -names-golem-dataset-47 = Algodonite -names-golem-dataset-48 = Aliettite -names-golem-dataset-49 = Allabogdanite -names-golem-dataset-50 = Allactite -names-golem-dataset-51 = Allanite -names-golem-dataset-52 = Allanpringite -names-golem-dataset-53 = Allargentum -names-golem-dataset-54 = Alleghanyite -names-golem-dataset-55 = Alloclasite -names-golem-dataset-56 = Allophane -names-golem-dataset-57 = Alluaivite -names-golem-dataset-58 = Alluaudite -names-golem-dataset-59 = Almandine -names-golem-dataset-60 = Almarudite -names-golem-dataset-61 = Alsakharovite -names-golem-dataset-62 = Alstonite -names-golem-dataset-63 = Altaite -names-golem-dataset-64 = Althausite -names-golem-dataset-65 = Althupite -names-golem-dataset-66 = Altisite -names-golem-dataset-67 = Alum -names-golem-dataset-68 = Aluminite -names-golem-dataset-69 = Aluminium -names-golem-dataset-70 = Alunite -names-golem-dataset-71 = Alunogen -names-golem-dataset-72 = Amakinite -names-golem-dataset-73 = Amarantite -names-golem-dataset-74 = Amblygonite -names-golem-dataset-75 = Ameghinite -names-golem-dataset-76 = Amesite -names-golem-dataset-77 = Amicite -names-golem-dataset-78 = Amphibole -names-golem-dataset-79 = Analcime -names-golem-dataset-80 = Anandite -names-golem-dataset-81 = Anapaite -names-golem-dataset-82 = Anatase -names-golem-dataset-83 = Ancylite -names-golem-dataset-84 = Andalusite -names-golem-dataset-85 = Andersonite -names-golem-dataset-86 = Andesine -names-golem-dataset-87 = Andorite -names-golem-dataset-88 = Andradite -names-golem-dataset-89 = Andyrobertsite -names-golem-dataset-90 = Anglesite -names-golem-dataset-91 = Anhydrite -names-golem-dataset-92 = Ankerite -names-golem-dataset-93 = Annabergite -names-golem-dataset-94 = Annite -names-golem-dataset-95 = Anorthite -names-golem-dataset-96 = Anorthoclase -names-golem-dataset-97 = Antarcticite -names-golem-dataset-98 = Anthonyite -names-golem-dataset-99 = Anthophyllite -names-golem-dataset-100 = Antigorite -names-golem-dataset-101 = Antimony -names-golem-dataset-102 = Antitaenite -names-golem-dataset-103 = Antlerite -names-golem-dataset-104 = Apachite -names-golem-dataset-105 = Apatite -names-golem-dataset-106 = Aphthitalite -names-golem-dataset-107 = Apophyllite -names-golem-dataset-108 = Aragonite -names-golem-dataset-109 = Arcanite -names-golem-dataset-110 = Archerite -names-golem-dataset-111 = Arctite -names-golem-dataset-112 = Arcubisite -names-golem-dataset-113 = Ardaite -names-golem-dataset-114 = Arfvedsonite -names-golem-dataset-115 = Argentite -names-golem-dataset-116 = Argutite -names-golem-dataset-117 = Argyrodite -names-golem-dataset-118 = Armalcolite -names-golem-dataset-119 = Arsenic -names-golem-dataset-120 = Arseniosiderite -names-golem-dataset-121 = Arsenoclasite -names-golem-dataset-122 = Arsenolite -names-golem-dataset-123 = Arsenopyrite -names-golem-dataset-124 = Arthurite -names-golem-dataset-125 = Artinite -names-golem-dataset-126 = Artroeite -names-golem-dataset-127 = Ashburtonite -names-golem-dataset-128 = Ashoverite -names-golem-dataset-129 = Asisite -names-golem-dataset-130 = Astrophyllite -names-golem-dataset-131 = Atacamite -names-golem-dataset-132 = Athabascaite -names-golem-dataset-133 = Atheneite -names-golem-dataset-134 = Aubertite -names-golem-dataset-135 = Augelite -names-golem-dataset-136 = Augite -names-golem-dataset-137 = Aurichalcite -names-golem-dataset-138 = Auricupride -names-golem-dataset-139 = Aurostibite -names-golem-dataset-140 = Austinite -names-golem-dataset-141 = Autunite -names-golem-dataset-142 = Avicennite -names-golem-dataset-143 = Avogadrite -names-golem-dataset-144 = Awaruite -names-golem-dataset-145 = Axinite -names-golem-dataset-146 = Azurite -names-golem-dataset-147 = Babefphite -names-golem-dataset-148 = Babingtonite -names-golem-dataset-149 = Baddeleyite -names-golem-dataset-150 = Bakerite -names-golem-dataset-151 = Balangeroite -names-golem-dataset-152 = Banalsite -names-golem-dataset-153 = Baotite -names-golem-dataset-154 = Bararite -names-golem-dataset-155 = Barrerite -names-golem-dataset-156 = Barstowite -names-golem-dataset-157 = Baryte -names-golem-dataset-158 = Barytocalcite -names-golem-dataset-159 = Bassanite -names-golem-dataset-160 = Bastnasite -names-golem-dataset-161 = Baumhauerite -names-golem-dataset-162 = Bayldonite -names-golem-dataset-163 = Bayleyite -names-golem-dataset-164 = Bazzite -names-golem-dataset-165 = Becquerelite -names-golem-dataset-166 = Benitoite -names-golem-dataset-167 = Benstonite -names-golem-dataset-168 = Bentorite -names-golem-dataset-169 = Beraunite -names-golem-dataset-170 = Berborite -names-golem-dataset-171 = Bergenite -names-golem-dataset-172 = Berlinite -names-golem-dataset-173 = Berryite -names-golem-dataset-174 = Berthierite -names-golem-dataset-175 = Bertrandite -names-golem-dataset-176 = Beryl -names-golem-dataset-177 = Beryllonite -names-golem-dataset-178 = Beudantite -names-golem-dataset-179 = Bicchulite -names-golem-dataset-180 = Biehlite -names-golem-dataset-181 = Bilinite -names-golem-dataset-182 = Billietite -names-golem-dataset-183 = Billwiseite -names-golem-dataset-184 = Biotite -names-golem-dataset-185 = Birnessite -names-golem-dataset-186 = Bischofite -names-golem-dataset-187 = Bismite -names-golem-dataset-188 = Bismuth -names-golem-dataset-189 = Bismuthinite -names-golem-dataset-190 = Bismutite -names-golem-dataset-191 = Bityite -names-golem-dataset-192 = Bixbyite -names-golem-dataset-193 = Blodite -names-golem-dataset-194 = Blossite -names-golem-dataset-195 = Bobfergusonite -names-golem-dataset-196 = Boehmite -names-golem-dataset-197 = Boleite -names-golem-dataset-198 = Boltwoodite -names-golem-dataset-199 = Bonaccordite -names-golem-dataset-200 = Boracite -names-golem-dataset-201 = Borax -names-golem-dataset-202 = Bornite -names-golem-dataset-203 = Botallackite -names-golem-dataset-204 = Botryogen -names-golem-dataset-205 = Boulangerite -names-golem-dataset-206 = Bournonite -names-golem-dataset-207 = Boussingaultite -names-golem-dataset-208 = Bowieite -names-golem-dataset-209 = Braggite -names-golem-dataset-210 = Brassite -names-golem-dataset-211 = Braunite -names-golem-dataset-212 = Brazilianite -names-golem-dataset-213 = Breithauptite -names-golem-dataset-214 = Brewsterite -names-golem-dataset-215 = Brezinaite -names-golem-dataset-216 = Brianite -names-golem-dataset-217 = Brianyoungite -names-golem-dataset-218 = Briartite -names-golem-dataset-219 = Bridgmanite -names-golem-dataset-220 = Brochantite -names-golem-dataset-221 = Brockite -names-golem-dataset-222 = Bromargyrite -names-golem-dataset-223 = Bromellite -names-golem-dataset-224 = Bronzite -names-golem-dataset-225 = Brookite -names-golem-dataset-226 = Brownleeite -names-golem-dataset-227 = Brownmillerite -names-golem-dataset-228 = Brucite -names-golem-dataset-229 = Brushite -names-golem-dataset-230 = Buddingtonite -names-golem-dataset-231 = Bukovite -names-golem-dataset-232 = Bukovskyite -names-golem-dataset-233 = Bultfonteinite -names-golem-dataset-234 = Bunsenite -names-golem-dataset-235 = Bustamite -names-golem-dataset-236 = Bystrite -names-golem-dataset-237 = Cabalzarite -names-golem-dataset-238 = Cabriite -names-golem-dataset-239 = Cacoxenite -names-golem-dataset-240 = Cadmium -names-golem-dataset-241 = Cadmoindite -names-golem-dataset-242 = Cadmoselite -names-golem-dataset-243 = Cadwaladerite -names-golem-dataset-244 = Cafarsite -names-golem-dataset-245 = Cafetite -names-golem-dataset-246 = Cahnite -names-golem-dataset-247 = Calaverite -names-golem-dataset-248 = Calciborite -names-golem-dataset-249 = Calcite -names-golem-dataset-250 = Calderite -names-golem-dataset-251 = Caledonite -names-golem-dataset-252 = Calumetite -names-golem-dataset-253 = Campigliaite -names-golem-dataset-254 = Canavesite -names-golem-dataset-255 = Cancrinite -names-golem-dataset-256 = Canfieldite -names-golem-dataset-257 = Carletonite -names-golem-dataset-258 = Carlosruizite -names-golem-dataset-259 = Carlsbergite -names-golem-dataset-260 = Carminite -names-golem-dataset-261 = Carnallite -names-golem-dataset-262 = Carnotite -names-golem-dataset-263 = Carobbiite -names-golem-dataset-264 = Carpathite -names-golem-dataset-265 = Carpholite -names-golem-dataset-266 = Carrollite -names-golem-dataset-267 = Caryopilite -names-golem-dataset-268 = Cassiterite -names-golem-dataset-269 = Cattierite -names-golem-dataset-270 = Cavansite -names-golem-dataset-271 = Celadonite -names-golem-dataset-272 = Celestine -names-golem-dataset-273 = Celsian -names-golem-dataset-274 = Cerite -names-golem-dataset-275 = Cerium -names-golem-dataset-276 = Cerussite -names-golem-dataset-277 = Cervandonite -names-golem-dataset-278 = Cervantite -names-golem-dataset-279 = Cesanite -names-golem-dataset-280 = Cesbronite -names-golem-dataset-281 = Chabazite -names-golem-dataset-282 = Chaidamuite -names-golem-dataset-283 = Chalcanthite -names-golem-dataset-284 = Chalcocite -names-golem-dataset-285 = Chalcophyllite -names-golem-dataset-286 = Chalcopyrite -names-golem-dataset-287 = Challacolloite -names-golem-dataset-288 = Chambersite -names-golem-dataset-289 = Chamosite -names-golem-dataset-290 = Changbaiite -names-golem-dataset-291 = Chaoite -names-golem-dataset-292 = Chapmanite -names-golem-dataset-293 = Charoite -names-golem-dataset-294 = Chatkalite -names-golem-dataset-295 = Chesterite -names-golem-dataset-296 = Chibaite -names-golem-dataset-297 = Childrenite -names-golem-dataset-298 = Chlorargyrite -names-golem-dataset-299 = Chlorite -names-golem-dataset-300 = Chloritoid -names-golem-dataset-301 = Chlormayenite -names-golem-dataset-302 = Chlorocalcite -names-golem-dataset-303 = Chloroxiphite -names-golem-dataset-304 = Chondrodite -names-golem-dataset-305 = Chrisstanleyite -names-golem-dataset-306 = Christite -names-golem-dataset-307 = Chromite -names-golem-dataset-308 = Chromium -names-golem-dataset-309 = Chrysoberyl -names-golem-dataset-310 = Chrysocolla -names-golem-dataset-311 = Chrysotile -names-golem-dataset-312 = Chvaleticeite -names-golem-dataset-313 = Cinnabar -names-golem-dataset-314 = Clarkeite -names-golem-dataset-315 = Claudetite -names-golem-dataset-316 = Clausthalite -names-golem-dataset-317 = Clearcreekite -names-golem-dataset-318 = Cleusonite -names-golem-dataset-319 = Clinoclase -names-golem-dataset-320 = Clinohedrite -names-golem-dataset-321 = Clinohumite -names-golem-dataset-322 = Clinoptilolite -names-golem-dataset-323 = Clinozoisite -names-golem-dataset-324 = Clintonite -names-golem-dataset-325 = Cobaltite -names-golem-dataset-326 = Coccinite -names-golem-dataset-327 = Coconinoite -names-golem-dataset-328 = Coesite -names-golem-dataset-329 = Coffinite -names-golem-dataset-330 = Cohenite -names-golem-dataset-331 = Colemanite -names-golem-dataset-332 = Colimaite -names-golem-dataset-333 = Collinsite -names-golem-dataset-334 = Coloradoite -names-golem-dataset-335 = Columbite -names-golem-dataset-336 = Combeite -names-golem-dataset-337 = Conichalcite -names-golem-dataset-338 = Connellite -names-golem-dataset-339 = Cooperite -names-golem-dataset-340 = Copiapite -names-golem-dataset-341 = Copper -names-golem-dataset-342 = Corderoite -names-golem-dataset-343 = Cordierite -names-golem-dataset-344 = Corkite -names-golem-dataset-345 = Cornubite -names-golem-dataset-346 = Cornwallite -names-golem-dataset-347 = Corundum -names-golem-dataset-348 = Cotunnite -names-golem-dataset-349 = Covellite -names-golem-dataset-350 = Coyoteite -names-golem-dataset-351 = Creedite -names-golem-dataset-352 = Cristobalite -names-golem-dataset-353 = Crocoite -names-golem-dataset-354 = Cronstedtite -names-golem-dataset-355 = Crookesite -names-golem-dataset-356 = Crossite -names-golem-dataset-357 = Cryolite -names-golem-dataset-358 = Cryptomelane -names-golem-dataset-359 = Cubanite -names-golem-dataset-360 = Cummingtonite -names-golem-dataset-361 = Cupalite -names-golem-dataset-362 = Cuprite -names-golem-dataset-363 = Cuprosklodowskite -names-golem-dataset-364 = Cuprospinel -names-golem-dataset-365 = Curite -names-golem-dataset-366 = Cuspidine -names-golem-dataset-367 = Cyanotrichite -names-golem-dataset-368 = Cylindrite -names-golem-dataset-369 = Cymrite -names-golem-dataset-370 = Cyrilovite -names-golem-dataset-371 = Danalite -names-golem-dataset-372 = Danburite -names-golem-dataset-373 = Datolite -names-golem-dataset-374 = Daubreeite -names-golem-dataset-375 = Daubreelite -names-golem-dataset-376 = Davidite -names-golem-dataset-377 = Dawsonite -names-golem-dataset-378 = Delafossite -names-golem-dataset-379 = Delvauxite -names-golem-dataset-380 = Demesmaekerite -names-golem-dataset-381 = Derriksite -names-golem-dataset-382 = Descloizite -names-golem-dataset-383 = Devilline -names-golem-dataset-384 = Diaboleite -names-golem-dataset-385 = Diadochite -names-golem-dataset-386 = Diamond -names-golem-dataset-387 = Diaspore -names-golem-dataset-388 = Dickite -names-golem-dataset-389 = Digenite -names-golem-dataset-390 = Dimorphite -names-golem-dataset-391 = Diopside -names-golem-dataset-392 = Dioptase -names-golem-dataset-393 = Djerfisherite -names-golem-dataset-394 = Djurleite -names-golem-dataset-395 = Dmitryivanovite -names-golem-dataset-396 = Dollaseite -names-golem-dataset-397 = Dolomite -names-golem-dataset-398 = Domeykite -names-golem-dataset-399 = Donnayite -names-golem-dataset-400 = Drysdallite -names-golem-dataset-401 = Duftite -names-golem-dataset-402 = Dumortierite -names-golem-dataset-403 = Dundasite -names-golem-dataset-404 = Dypingite -names-golem-dataset-405 = Dyscrasite -names-golem-dataset-406 = Dzhalindite -names-golem-dataset-407 = Edenite -names-golem-dataset-408 = Edingtonite -names-golem-dataset-409 = Efremovite -names-golem-dataset-410 = Ekanite -names-golem-dataset-411 = Elbaite -names-golem-dataset-412 = Emmonsite -names-golem-dataset-413 = Empressite -names-golem-dataset-414 = Enargite -names-golem-dataset-415 = Enstatite -names-golem-dataset-416 = Eosphorite -names-golem-dataset-417 = Ephesite -names-golem-dataset-418 = Epidote -names-golem-dataset-419 = Epsomite -names-golem-dataset-420 = Ericssonite -names-golem-dataset-421 = Erionite -names-golem-dataset-422 = Erythrite -names-golem-dataset-423 = Eskolaite -names-golem-dataset-424 = Esperite -names-golem-dataset-425 = Ettringite -names-golem-dataset-426 = Euchroite -names-golem-dataset-427 = Euclase -names-golem-dataset-428 = Eucryptite -names-golem-dataset-429 = Eudialyte -names-golem-dataset-430 = Euxenite -names-golem-dataset-431 = Eveite -names-golem-dataset-432 = Evenkite -names-golem-dataset-433 = Eveslogite -names-golem-dataset-434 = Fabianite -names-golem-dataset-435 = Farneseite -names-golem-dataset-436 = Faujasite -names-golem-dataset-437 = Faustite -names-golem-dataset-438 = Fayalite -names-golem-dataset-439 = Feldspar -names-golem-dataset-440 = Feldspathoid -names-golem-dataset-441 = Felsobanyaite -names-golem-dataset-442 = Ferberite -names-golem-dataset-443 = Fergusonite -names-golem-dataset-444 = Feroxyhyte -names-golem-dataset-445 = Ferrierite -names-golem-dataset-446 = Ferrihydrite -names-golem-dataset-447 = Ferrimolybdite -names-golem-dataset-448 = Ferro-actinolite -names-golem-dataset-449 = Ferrogedrite -names-golem-dataset-450 = Ferrohortonolite -names-golem-dataset-451 = Ferronigerite -names-golem-dataset-452 = Ferropericlase -names-golem-dataset-453 = Ferroselite -names-golem-dataset-454 = Fettelite -names-golem-dataset-455 = Fichtelite -names-golem-dataset-456 = Fletcherite -names-golem-dataset-457 = Fluckite -names-golem-dataset-458 = Fluellite -names-golem-dataset-459 = Fluoborite -names-golem-dataset-460 = Fluocerite -names-golem-dataset-461 = Fluorapatite -names-golem-dataset-462 = Fluorapophyllite -names-golem-dataset-463 = Fluorcaphite -names-golem-dataset-464 = Fluorellestadite -names-golem-dataset-465 = Fluorite -names-golem-dataset-466 = Fluororichterite -names-golem-dataset-467 = Fornacite -names-golem-dataset-468 = Forsterite -names-golem-dataset-469 = Fougerite -names-golem-dataset-470 = Fourmarierite -names-golem-dataset-471 = Fraipontite -names-golem-dataset-472 = Francevillite -names-golem-dataset-473 = Franckeite -names-golem-dataset-474 = Frankamenite -names-golem-dataset-475 = Frankdicksonite -names-golem-dataset-476 = Frankhawthorneite -names-golem-dataset-477 = Franklinite -names-golem-dataset-478 = Franklinphilite -names-golem-dataset-479 = Freibergite -names-golem-dataset-480 = Freieslebenite -names-golem-dataset-481 = Fukuchilite -names-golem-dataset-482 = Gabrielite -names-golem-dataset-483 = Gadolinite -names-golem-dataset-484 = Gagarinite -names-golem-dataset-485 = Gahnite -names-golem-dataset-486 = Galaxite -names-golem-dataset-487 = Galena -names-golem-dataset-488 = Galkhaite -names-golem-dataset-489 = Gananite -names-golem-dataset-490 = Garnet -names-golem-dataset-491 = Gaspeite -names-golem-dataset-492 = Gatehouseite -names-golem-dataset-493 = Gaylussite -names-golem-dataset-494 = Gedrite -names-golem-dataset-495 = Geerite -names-golem-dataset-496 = Gehlenite -names-golem-dataset-497 = Geigerite -names-golem-dataset-498 = Geikielite -names-golem-dataset-499 = Geocronite -names-golem-dataset-500 = Georgerobinsonite -names-golem-dataset-501 = Germanite -names-golem-dataset-502 = Gersdorffite -names-golem-dataset-503 = Getchellite -names-golem-dataset-504 = Gibbsite -names-golem-dataset-505 = Gilalite -names-golem-dataset-506 = Gismondine -names-golem-dataset-507 = Glauberite -names-golem-dataset-508 = Glaucochroite -names-golem-dataset-509 = Glaucodot -names-golem-dataset-510 = Glauconite -names-golem-dataset-511 = Glaucophane -names-golem-dataset-512 = Gmelinite -names-golem-dataset-513 = Godovikovite -names-golem-dataset-514 = Goethite -names-golem-dataset-515 = Gold -names-golem-dataset-516 = Goldmanite -names-golem-dataset-517 = Gonnardite -names-golem-dataset-518 = Gordaite -names-golem-dataset-519 = Gormanite -names-golem-dataset-520 = Goslarite -names-golem-dataset-521 = Graftonite -names-golem-dataset-522 = Grandidierite -names-golem-dataset-523 = Grandreefite -names-golem-dataset-524 = Graphite -names-golem-dataset-525 = Gratonite -names-golem-dataset-526 = Greenalite -names-golem-dataset-527 = Greenockite -names-golem-dataset-528 = Gregoryite -names-golem-dataset-529 = Greifensteinite -names-golem-dataset-530 = Greigite -names-golem-dataset-531 = Grossite -names-golem-dataset-532 = Grossular -names-golem-dataset-533 = Groutite -names-golem-dataset-534 = Grunerite -names-golem-dataset-535 = Guettardite -names-golem-dataset-536 = Gugiaite -names-golem-dataset-537 = Guilleminite -names-golem-dataset-538 = Gunningite -names-golem-dataset-539 = Guyanaite -names-golem-dataset-540 = Gwihabaite -names-golem-dataset-541 = Gypsum -names-golem-dataset-542 = Hafnon -names-golem-dataset-543 = Hagendorfite -names-golem-dataset-544 = Haggertyite -names-golem-dataset-545 = Haidingerite -names-golem-dataset-546 = Haiweeite -names-golem-dataset-547 = Haleniusite -names-golem-dataset-548 = Halite -names-golem-dataset-549 = Halloysite -names-golem-dataset-550 = Halotrichite -names-golem-dataset-551 = Hambergite -names-golem-dataset-552 = Hanksite -names-golem-dataset-553 = Hapkeite -names-golem-dataset-554 = Hardystonite -names-golem-dataset-555 = Harmotome -names-golem-dataset-556 = Hauerite -names-golem-dataset-557 = Hausmannite -names-golem-dataset-558 = Hauyne -names-golem-dataset-559 = Hawleyite -names-golem-dataset-560 = Haxonite -names-golem-dataset-561 = Hazenite -names-golem-dataset-562 = Heazlewoodite -names-golem-dataset-563 = Hectorite -names-golem-dataset-564 = Hedenbergite -names-golem-dataset-565 = Hellyerite -names-golem-dataset-566 = Hematite -names-golem-dataset-567 = Hemihedrite -names-golem-dataset-568 = Hemimorphite -names-golem-dataset-569 = Hemusite -names-golem-dataset-570 = Herbertsmithite -names-golem-dataset-571 = Hercynite -names-golem-dataset-572 = Herderite -names-golem-dataset-573 = Hessite -names-golem-dataset-574 = Heulandite -names-golem-dataset-575 = Hexaferrum -names-golem-dataset-576 = Hiarneite -names-golem-dataset-577 = Hibonite -names-golem-dataset-578 = Hidalgoite -names-golem-dataset-579 = Hilgardite -names-golem-dataset-580 = Hisingerite -names-golem-dataset-581 = Hodgkinsonite -names-golem-dataset-582 = Hoelite -names-golem-dataset-583 = Hollandite -names-golem-dataset-584 = Holmquistite -names-golem-dataset-585 = Homilite -names-golem-dataset-586 = Hopeite -names-golem-dataset-587 = Hornblende -names-golem-dataset-588 = Howlite -names-golem-dataset-589 = Hsianghualite -names-golem-dataset-590 = Hubeite -names-golem-dataset-591 = Hubnerite -names-golem-dataset-592 = Huemulite -names-golem-dataset-593 = Humite -names-golem-dataset-594 = Huntite -names-golem-dataset-595 = Hureaulite -names-golem-dataset-596 = Hutchinsonite -names-golem-dataset-597 = Huttonite -names-golem-dataset-598 = Hydroboracite -names-golem-dataset-599 = Hydrogrossular -names-golem-dataset-600 = Hydrohalite -names-golem-dataset-601 = Hydrokenoelsmoreite -names-golem-dataset-602 = Hydromagnesite -names-golem-dataset-603 = Hydrotalcite -names-golem-dataset-604 = Hydroxylapatite -names-golem-dataset-605 = Hydrozincite -names-golem-dataset-606 = Ianbruceite -names-golem-dataset-607 = Ice -names-golem-dataset-608 = Icosahedrite -names-golem-dataset-609 = Idrialite -names-golem-dataset-610 = Ikaite -names-golem-dataset-611 = Illite -names-golem-dataset-612 = Ilmenite -names-golem-dataset-613 = Ilvaite -names-golem-dataset-614 = Imogolite -names-golem-dataset-615 = Indite -names-golem-dataset-616 = Indium -names-golem-dataset-617 = Inyoite -names-golem-dataset-618 = Iodargyrite -names-golem-dataset-619 = Iranite -names-golem-dataset-620 = Iridium -names-golem-dataset-621 = Iron -names-golem-dataset-622 = Ixiolite -names-golem-dataset-623 = Jacobsite -names-golem-dataset-624 = Jadarite -names-golem-dataset-625 = Jadeite -names-golem-dataset-626 = Jaffeite -names-golem-dataset-627 = Jalpaite -names-golem-dataset-628 = Jamesonite -names-golem-dataset-629 = Janggunite -names-golem-dataset-630 = Jarosewichite -names-golem-dataset-631 = Jarosite -names-golem-dataset-632 = Jennite -names-golem-dataset-633 = Jeremejevite -names-golem-dataset-634 = Jerrygibbsite -names-golem-dataset-635 = Jimthompsonite -names-golem-dataset-636 = Johannite -names-golem-dataset-637 = Jolliffeite -names-golem-dataset-638 = Jonesite -names-golem-dataset-639 = Jordanite -names-golem-dataset-640 = Julgoldite -names-golem-dataset-641 = Junitoite -names-golem-dataset-642 = Jurbanite -names-golem-dataset-643 = Kaatialaite -names-golem-dataset-644 = Kadyrelite -names-golem-dataset-645 = Kaersutite -names-golem-dataset-646 = Kainite -names-golem-dataset-647 = Kainosite -names-golem-dataset-648 = Kalininite -names-golem-dataset-649 = Kalinite -names-golem-dataset-650 = Kalsilite -names-golem-dataset-651 = Kamacite -names-golem-dataset-652 = Kambaldaite -names-golem-dataset-653 = Kamiokite -names-golem-dataset-654 = Kampfite -names-golem-dataset-655 = Kankite -names-golem-dataset-656 = Kanoite -names-golem-dataset-657 = Kaolinite -names-golem-dataset-658 = Karlite -names-golem-dataset-659 = Kassite -names-golem-dataset-660 = Kegelite -names-golem-dataset-661 = Keilite -names-golem-dataset-662 = Kermesite -names-golem-dataset-663 = Kernite -names-golem-dataset-664 = Kesterite -names-golem-dataset-665 = Keyite -names-golem-dataset-666 = Khatyrkite -names-golem-dataset-667 = Kieserite -names-golem-dataset-668 = Kinoite -names-golem-dataset-669 = Knebelite -names-golem-dataset-670 = Knorringite -names-golem-dataset-671 = Kobellite -names-golem-dataset-672 = Kochite -names-golem-dataset-673 = Kogarkoite -names-golem-dataset-674 = Kolbeckite -names-golem-dataset-675 = Kornerupine -names-golem-dataset-676 = Kosmochlor -names-golem-dataset-677 = Kostovite -names-golem-dataset-678 = Kottigite -names-golem-dataset-679 = Kovdorskite -names-golem-dataset-680 = Kratochv�lite -names-golem-dataset-681 = Kremersite -names-golem-dataset-682 = Krennerite -names-golem-dataset-683 = Krieselite -names-golem-dataset-684 = Krohnkite -names-golem-dataset-685 = Krotite -names-golem-dataset-686 = Krutovite -names-golem-dataset-687 = Kukharenkoite -names-golem-dataset-688 = Kuratite -names-golem-dataset-689 = Kurnakovite -names-golem-dataset-690 = Kutnohorite -names-golem-dataset-691 = Kyanite -names-golem-dataset-692 = Labradorite -names-golem-dataset-693 = Lanarkite -names-golem-dataset-694 = Langbeinite -names-golem-dataset-695 = Langite -names-golem-dataset-696 = Lansfordite -names-golem-dataset-697 = Lanthanite -names-golem-dataset-698 = Laplandite -names-golem-dataset-699 = Larnite -names-golem-dataset-700 = Laumontite -names-golem-dataset-701 = Laurionite -names-golem-dataset-702 = Laurite -names-golem-dataset-703 = Lautite -names-golem-dataset-704 = Lavendulan -names-golem-dataset-705 = Lawsonite -names-golem-dataset-706 = Lazulite -names-golem-dataset-707 = Lazurite -names-golem-dataset-708 = Lead -names-golem-dataset-709 = Leadhillite -names-golem-dataset-710 = Legrandite -names-golem-dataset-711 = Leifite -names-golem-dataset-712 = Leightonite -names-golem-dataset-713 = Lepidocrocite -names-golem-dataset-714 = Lepidolite -names-golem-dataset-715 = Letovicite -names-golem-dataset-716 = Leucite -names-golem-dataset-717 = Leucophanite -names-golem-dataset-718 = Leucophoenicite -names-golem-dataset-719 = Levyne -names-golem-dataset-720 = Libethenite -names-golem-dataset-721 = Liebigite -names-golem-dataset-722 = Linarite -names-golem-dataset-723 = Lindgrenite -names-golem-dataset-724 = Linnaeite -names-golem-dataset-725 = Lipscombite -names-golem-dataset-726 = Liroconite -names-golem-dataset-727 = Litharge -names-golem-dataset-728 = Lithiophilite -names-golem-dataset-729 = Livingstonite -names-golem-dataset-730 = Lizardite -names-golem-dataset-731 = Loellingite -names-golem-dataset-732 = Lonsdaleite -names-golem-dataset-733 = Loparite -names-golem-dataset-734 = Lopezite -names-golem-dataset-735 = Lorandite -names-golem-dataset-736 = Lorenzenite -names-golem-dataset-737 = Loveringite -names-golem-dataset-738 = Ludlamite -names-golem-dataset-739 = Ludwigite -names-golem-dataset-740 = Lulzacite -names-golem-dataset-741 = Lyonsite -names-golem-dataset-742 = Macaulayite -names-golem-dataset-743 = Macdonaldite -names-golem-dataset-744 = Mackinawite -names-golem-dataset-745 = Madocite -names-golem-dataset-746 = Magadiite -names-golem-dataset-747 = Maghemite -names-golem-dataset-748 = Magnesioferrite -names-golem-dataset-749 = Magnesiohastingsite -names-golem-dataset-750 = Magnesiopascoite -names-golem-dataset-751 = Magnesite -names-golem-dataset-752 = Magnetite -names-golem-dataset-753 = Majorite -names-golem-dataset-754 = Malachite -names-golem-dataset-755 = Malayaite -names-golem-dataset-756 = Manganite -names-golem-dataset-757 = Manganosite -names-golem-dataset-758 = Manganvesuvianite -names-golem-dataset-759 = Marcasite -names-golem-dataset-760 = Margaritasite -names-golem-dataset-761 = Margarite -names-golem-dataset-762 = Marialite -names-golem-dataset-763 = Maricite -names-golem-dataset-764 = Marrite -names-golem-dataset-765 = Marthozite -names-golem-dataset-766 = Mascagnite -names-golem-dataset-767 = Massicot -names-golem-dataset-768 = Masuyite -names-golem-dataset-769 = Matlockite -names-golem-dataset-770 = Maucherite -names-golem-dataset-771 = Mawsonite -names-golem-dataset-772 = Mckelveyite -names-golem-dataset-773 = Meionite -names-golem-dataset-774 = Melanophlogite -names-golem-dataset-775 = Melanterite -names-golem-dataset-776 = Melilite -names-golem-dataset-777 = Mellite -names-golem-dataset-778 = Melonite -names-golem-dataset-779 = Mendipite -names-golem-dataset-780 = Mendozite -names-golem-dataset-781 = Meneghinite -names-golem-dataset-782 = Mercury -names-golem-dataset-783 = Mereheadite -names-golem-dataset-784 = Merenskyite -names-golem-dataset-785 = Meridianiite -names-golem-dataset-786 = Merrillite -names-golem-dataset-787 = Mesolite -names-golem-dataset-788 = Messelite -names-golem-dataset-789 = Metacinnabar -names-golem-dataset-790 = Metatorbernite -names-golem-dataset-791 = Metazeunerite -names-golem-dataset-792 = Meyerhofferite -names-golem-dataset-793 = Miargyrite -names-golem-dataset-794 = Mica -names-golem-dataset-795 = Microcline -names-golem-dataset-796 = Microlite -names-golem-dataset-797 = Millerite -names-golem-dataset-798 = Millosevichite -names-golem-dataset-799 = Mimetite -names-golem-dataset-800 = Minium -names-golem-dataset-801 = Minnesotaite -names-golem-dataset-802 = Minyulite -names-golem-dataset-803 = Mirabilite -names-golem-dataset-804 = Mixite -names-golem-dataset-805 = Moganite -names-golem-dataset-806 = Mohite -names-golem-dataset-807 = Mohrite -names-golem-dataset-808 = Moissanite -names-golem-dataset-809 = Molybdenite -names-golem-dataset-810 = Molybdite -names-golem-dataset-811 = Monazite -names-golem-dataset-812 = Monohydrocalcite -names-golem-dataset-813 = Monticellite -names-golem-dataset-814 = Montmorillonite -names-golem-dataset-815 = Mooihoekite -names-golem-dataset-816 = Moolooite -names-golem-dataset-817 = Mordenite -names-golem-dataset-818 = Moschellandsbergite -names-golem-dataset-819 = Mosesite -names-golem-dataset-820 = Mottramite -names-golem-dataset-821 = Motukoreaite -names-golem-dataset-822 = Mullite -names-golem-dataset-823 = Mundite -names-golem-dataset-824 = Murdochite -names-golem-dataset-825 = Muscovite -names-golem-dataset-826 = Musgravite -names-golem-dataset-827 = Nabalamprophyllite -names-golem-dataset-828 = Nabesite -names-golem-dataset-829 = Nacrite -names-golem-dataset-830 = Nadorite -names-golem-dataset-831 = Nagyagite -names-golem-dataset-832 = Nahcolite -names-golem-dataset-833 = Naldrettite -names-golem-dataset-834 = Nambulite -names-golem-dataset-835 = Narsarsukite -names-golem-dataset-836 = Native copper -names-golem-dataset-837 = Natrolite -names-golem-dataset-838 = Natron -names-golem-dataset-839 = Natrophilite -names-golem-dataset-840 = Nekrasovite -names-golem-dataset-841 = Nelenite -names-golem-dataset-842 = Nenadkevichite -names-golem-dataset-843 = Nepheline -names-golem-dataset-844 = Nepouite -names-golem-dataset-845 = Neptunite -names-golem-dataset-846 = Nichromite -names-golem-dataset-847 = Nickel -names-golem-dataset-848 = Nickeline -names-golem-dataset-849 = Niedermayrite -names-golem-dataset-850 = Niningerite -names-golem-dataset-851 = Nissonite -names-golem-dataset-852 = Niter -names-golem-dataset-853 = Nitratine -names-golem-dataset-854 = Nobleite -names-golem-dataset-855 = Nontronite -names-golem-dataset-856 = Norbergite -names-golem-dataset-857 = Normandite -names-golem-dataset-858 = Northupite -names-golem-dataset-859 = Nosean -names-golem-dataset-860 = Nsutite -names-golem-dataset-861 = Nyerereite -names-golem-dataset-862 = kenite -names-golem-dataset-863 = Oldhamite -names-golem-dataset-864 = Olgite -names-golem-dataset-865 = Olivenite -names-golem-dataset-866 = Olivine -names-golem-dataset-867 = Omphacite -names-golem-dataset-868 = Ordonezite -names-golem-dataset-869 = Oregonite -names-golem-dataset-870 = Orpiment -names-golem-dataset-871 = Orthoclase -names-golem-dataset-872 = Osarizawaite -names-golem-dataset-873 = Osmium -names-golem-dataset-874 = Osumilite -names-golem-dataset-875 = Otavite -names-golem-dataset-876 = Ottrelite -names-golem-dataset-877 = Otwayite -names-golem-dataset-878 = Paakkonenite -names-golem-dataset-879 = Pabstite -names-golem-dataset-880 = Painite -names-golem-dataset-881 = Palladium -names-golem-dataset-882 = Palygorskite -names-golem-dataset-883 = Panethite -names-golem-dataset-884 = Panguite -names-golem-dataset-885 = Papagoite -names-golem-dataset-886 = Paragonite -names-golem-dataset-887 = Paralaurionite -names-golem-dataset-888 = Paramelaconite -names-golem-dataset-889 = Pararealgar -names-golem-dataset-890 = Pargasite -names-golem-dataset-891 = Parisite -names-golem-dataset-892 = Parsonsite -names-golem-dataset-893 = Partheite -names-golem-dataset-894 = Pascoite -names-golem-dataset-895 = Patronite -names-golem-dataset-896 = Paulingite -names-golem-dataset-897 = Paulscherrerite -names-golem-dataset-898 = Pearceite -names-golem-dataset-899 = Pecoraite -names-golem-dataset-900 = Pectolite -names-golem-dataset-901 = Penikisite -names-golem-dataset-902 = Penroseite -names-golem-dataset-903 = Pentagonite -names-golem-dataset-904 = Pentlandite -names-golem-dataset-905 = Perhamite -names-golem-dataset-906 = Periclase -names-golem-dataset-907 = Perite -names-golem-dataset-908 = Perovskite -names-golem-dataset-909 = Petalite -names-golem-dataset-910 = Petzite -names-golem-dataset-911 = Pezzottaite -names-golem-dataset-912 = Pharmacolite -names-golem-dataset-913 = Pharmacosiderite -names-golem-dataset-914 = Phenakite -names-golem-dataset-915 = Phillipsite -names-golem-dataset-916 = Phlogopite -names-golem-dataset-917 = Phoenicochroite -names-golem-dataset-918 = Phosgenite -names-golem-dataset-919 = Phosphophyllite -names-golem-dataset-920 = Phosphuranylite -names-golem-dataset-921 = Pickeringite -names-golem-dataset-922 = Picropharmacolite -names-golem-dataset-923 = Piemontite -names-golem-dataset-924 = Pigeonite -names-golem-dataset-925 = Pinalite -names-golem-dataset-926 = Pinnoite -names-golem-dataset-927 = Piypite -names-golem-dataset-928 = Plagioclase -names-golem-dataset-929 = Plancheite -names-golem-dataset-930 = Platinum -names-golem-dataset-931 = Plattnerite -names-golem-dataset-932 = Playfairite -names-golem-dataset-933 = Plumbogummite -names-golem-dataset-934 = Polarite -names-golem-dataset-935 = Pollucite -names-golem-dataset-936 = Polybasite -names-golem-dataset-937 = Polycrase -names-golem-dataset-938 = Polydymite -names-golem-dataset-939 = Polyhalite -names-golem-dataset-940 = Portlandite -names-golem-dataset-941 = Posnjakite -names-golem-dataset-942 = Poudretteite -names-golem-dataset-943 = Povondraite -names-golem-dataset-944 = Powellite -names-golem-dataset-945 = Prehnite -names-golem-dataset-946 = Proustite -names-golem-dataset-947 = Pseudobrookite -names-golem-dataset-948 = Pseudomalachite -names-golem-dataset-949 = Pseudowollastonite -names-golem-dataset-950 = Pumpellyite -names-golem-dataset-951 = Purpurite -names-golem-dataset-952 = Putnisite -names-golem-dataset-953 = Pyrargyrite -names-golem-dataset-954 = Pyrite -names-golem-dataset-955 = Pyrochlore -names-golem-dataset-956 = Pyrolusite -names-golem-dataset-957 = Pyromorphite -names-golem-dataset-958 = Pyrope -names-golem-dataset-959 = Pyrophanite -names-golem-dataset-960 = Pyrophyllite -names-golem-dataset-961 = Pyroxene -names-golem-dataset-962 = Pyroxferroite -names-golem-dataset-963 = Pyroxmangite -names-golem-dataset-964 = Pyrrhotite -names-golem-dataset-965 = Rambergite -names-golem-dataset-966 = Rameauite -names-golem-dataset-967 = Rammelsbergite -names-golem-dataset-968 = Rapidcreekite -names-golem-dataset-969 = Raspite -names-golem-dataset-970 = Realgar -names-golem-dataset-971 = Reidite -names-golem-dataset-972 = Reinerite -names-golem-dataset-973 = Renierite -names-golem-dataset-974 = Rheniite -names-golem-dataset-975 = Rhodium -names-golem-dataset-976 = Rhodochrosite -names-golem-dataset-977 = Rhodonite -names-golem-dataset-978 = Rhodplumsite -names-golem-dataset-979 = Rhomboclase -names-golem-dataset-980 = Richterite -names-golem-dataset-981 = Rickardite -names-golem-dataset-982 = Riebeckite -names-golem-dataset-983 = Ringwoodite -names-golem-dataset-984 = Roaldite -names-golem-dataset-985 = Robertsite -names-golem-dataset-986 = Rodalquilarite -names-golem-dataset-987 = Romanechite -names-golem-dataset-988 = Romeite -names-golem-dataset-989 = Rosasite -names-golem-dataset-990 = Roscoelite -names-golem-dataset-991 = Roselite -names-golem-dataset-992 = Rosenbergite -names-golem-dataset-993 = Rosickyite -names-golem-dataset-994 = Routhierite -names-golem-dataset-995 = Rozenite -names-golem-dataset-996 = Rubicline -names-golem-dataset-997 = Ruizite -names-golem-dataset-998 = Russellite -names-golem-dataset-999 = Ruthenium -names-golem-dataset-1000 = Rutherfordine -names-golem-dataset-1001 = Rutile -names-golem-dataset-1002 = Rynersonite -names-golem-dataset-1003 = Sabatierite -names-golem-dataset-1004 = Sabieite -names-golem-dataset-1005 = Sabinaite -names-golem-dataset-1006 = Sacrofanite -names-golem-dataset-1007 = Safflorite -names-golem-dataset-1008 = Sal Ammoniac -names-golem-dataset-1009 = Saleeite -names-golem-dataset-1010 = Saliotite -names-golem-dataset-1011 = Salzburgite -names-golem-dataset-1012 = Samarskite -names-golem-dataset-1013 = Sampleite -names-golem-dataset-1014 = Samsonite -names-golem-dataset-1015 = Samuelsonite -names-golem-dataset-1016 = Sanbornite -names-golem-dataset-1017 = Saneroite -names-golem-dataset-1018 = Sanidine -names-golem-dataset-1019 = Santabarbaraite -names-golem-dataset-1020 = Santite -names-golem-dataset-1021 = Saponite -names-golem-dataset-1022 = Sapphirine -names-golem-dataset-1023 = Sarabauite -names-golem-dataset-1024 = Sarkinite -names-golem-dataset-1025 = Sassolite -names-golem-dataset-1026 = Satterlyite -names-golem-dataset-1027 = Sauconite -names-golem-dataset-1028 = Sborgite -names-golem-dataset-1029 = Scapolite -names-golem-dataset-1030 = Schaferite -names-golem-dataset-1031 = Scheelite -names-golem-dataset-1032 = Schmiederite -names-golem-dataset-1033 = Schoepite -names-golem-dataset-1034 = Schorl -names-golem-dataset-1035 = Schreibersite -names-golem-dataset-1036 = Schreyerite -names-golem-dataset-1037 = Schrockingerite -names-golem-dataset-1038 = Schwertmannite -names-golem-dataset-1039 = Scolecite -names-golem-dataset-1040 = Scorodite -names-golem-dataset-1041 = Scorzalite -names-golem-dataset-1042 = Scrutinyite -names-golem-dataset-1043 = Seamanite -names-golem-dataset-1044 = Searlesite -names-golem-dataset-1045 = Seeligerite -names-golem-dataset-1046 = Segelerite -names-golem-dataset-1047 = Seifertite -names-golem-dataset-1048 = Sekaninaite -names-golem-dataset-1049 = Selenium -names-golem-dataset-1050 = Seligmannite -names-golem-dataset-1051 = Sellaite -names-golem-dataset-1052 = Semseyite -names-golem-dataset-1053 = Senarmontite -names-golem-dataset-1054 = Sepiolite -names-golem-dataset-1055 = Serandite -names-golem-dataset-1056 = Serendibite -names-golem-dataset-1057 = Serpentine -names-golem-dataset-1058 = Serpierite -names-golem-dataset-1059 = Sewardite -names-golem-dataset-1060 = Shandite -names-golem-dataset-1061 = Shattuckite -names-golem-dataset-1062 = Shigaite -names-golem-dataset-1063 = Shortite -names-golem-dataset-1064 = Siderite -names-golem-dataset-1065 = Siderophyllite -names-golem-dataset-1066 = Siderotil -names-golem-dataset-1067 = Siegenite -names-golem-dataset-1068 = Silicon -names-golem-dataset-1069 = Sillimanite -names-golem-dataset-1070 = Silver -names-golem-dataset-1071 = Simonellite -names-golem-dataset-1072 = Simpsonite -names-golem-dataset-1073 = Sincosite -names-golem-dataset-1074 = Sinkankasite -names-golem-dataset-1075 = Sinoite -names-golem-dataset-1076 = Skaergaardite -names-golem-dataset-1077 = Sklodowskite -names-golem-dataset-1078 = Skutterudite -names-golem-dataset-1079 = Smaltite -names-golem-dataset-1080 = Smectite -names-golem-dataset-1081 = Smithsonite -names-golem-dataset-1082 = Sodalite -names-golem-dataset-1083 = Soddyite -names-golem-dataset-1084 = Sonolite -names-golem-dataset-1085 = Sperrylite -names-golem-dataset-1086 = Spertiniite -names-golem-dataset-1087 = Spessartine -names-golem-dataset-1088 = Sphalerite -names-golem-dataset-1089 = Spherocobaltite -names-golem-dataset-1090 = Spinel -names-golem-dataset-1091 = Spodumene -names-golem-dataset-1092 = Spurrite -names-golem-dataset-1093 = Stannite -names-golem-dataset-1094 = Stannoidite -names-golem-dataset-1095 = Staurolite -names-golem-dataset-1096 = Steacyite -names-golem-dataset-1097 = Stellerite -names-golem-dataset-1098 = Stephanite -names-golem-dataset-1099 = Stercorite -names-golem-dataset-1100 = Stibarsen -names-golem-dataset-1101 = Stibiconite -names-golem-dataset-1102 = Stibiopalladinite -names-golem-dataset-1103 = Stibnite -names-golem-dataset-1104 = Stichtite -names-golem-dataset-1105 = Stilbite -names-golem-dataset-1106 = Stilleite -names-golem-dataset-1107 = Stillwaterite -names-golem-dataset-1108 = Stillwellite -names-golem-dataset-1109 = Stilpnomelane -names-golem-dataset-1110 = Stishovite -names-golem-dataset-1111 = Stolzite -names-golem-dataset-1112 = Strashimirite -names-golem-dataset-1113 = Strengite -names-golem-dataset-1114 = Stromeyerite -names-golem-dataset-1115 = Strontianite -names-golem-dataset-1116 = Struvite -names-golem-dataset-1117 = Studenitsite -names-golem-dataset-1118 = Studtite -names-golem-dataset-1119 = Stutzite -names-golem-dataset-1120 = Suanite -names-golem-dataset-1121 = Suessite -names-golem-dataset-1122 = Sugilite -names-golem-dataset-1123 = Sulfur -names-golem-dataset-1124 = Sursassite -names-golem-dataset-1125 = Susannite -names-golem-dataset-1126 = Sussexite -names-golem-dataset-1127 = Svanbergite -names-golem-dataset-1128 = Sweetite -names-golem-dataset-1129 = Switzerite -names-golem-dataset-1130 = Sylvanite -names-golem-dataset-1131 = Sylvite -names-golem-dataset-1132 = Synchysite -names-golem-dataset-1133 = Syngenite -names-golem-dataset-1134 = Taaffeite -names-golem-dataset-1135 = Tachyhydrite -names-golem-dataset-1136 = Taenite -names-golem-dataset-1137 = Talc -names-golem-dataset-1138 = Talmessite -names-golem-dataset-1139 = Talnakhite -names-golem-dataset-1140 = Tamarugite -names-golem-dataset-1141 = Tangeite -names-golem-dataset-1142 = Tantalite -names-golem-dataset-1143 = Tantite -names-golem-dataset-1144 = Tapiolite -names-golem-dataset-1145 = Taranakite -names-golem-dataset-1146 = Tarapacaite -names-golem-dataset-1147 = Tarbuttite -names-golem-dataset-1148 = Tausonite -names-golem-dataset-1149 = Teallite -names-golem-dataset-1150 = Tellurite -names-golem-dataset-1151 = Tellurium -names-golem-dataset-1152 = Tellurobismuthite -names-golem-dataset-1153 = Temagamite -names-golem-dataset-1154 = Tennantite -names-golem-dataset-1155 = Tenorite -names-golem-dataset-1156 = Tephroite -names-golem-dataset-1157 = Terlinguaite -names-golem-dataset-1158 = Teruggite -names-golem-dataset-1159 = Tetradymite -names-golem-dataset-1160 = Tetrahedrite -names-golem-dataset-1161 = Tetrataenite -names-golem-dataset-1162 = Thaumasite -names-golem-dataset-1163 = Thenardite -names-golem-dataset-1164 = Thermonatrite -names-golem-dataset-1165 = Thiospinel -names-golem-dataset-1166 = Thomasclarkite -names-golem-dataset-1167 = Thomsenolite -names-golem-dataset-1168 = Thomsonite -names-golem-dataset-1169 = Thorianite -names-golem-dataset-1170 = Thorite -names-golem-dataset-1171 = Thortveitite -names-golem-dataset-1172 = Tiemannite -names-golem-dataset-1173 = Tienshanite -names-golem-dataset-1174 = Tin -names-golem-dataset-1175 = Tinaksite -names-golem-dataset-1176 = Tincalconite -names-golem-dataset-1177 = Titanite -names-golem-dataset-1178 = Titanium -names-golem-dataset-1179 = Titanowodginite -names-golem-dataset-1180 = Tobermorite -names-golem-dataset-1181 = Todorokite -names-golem-dataset-1182 = Tokyoite -names-golem-dataset-1183 = Tongbaite -names-golem-dataset-1184 = Topaz -names-golem-dataset-1185 = Torbernite -names-golem-dataset-1186 = Tourmaline -names-golem-dataset-1187 = Tranquillityite -names-golem-dataset-1188 = Tremolite -names-golem-dataset-1189 = Trevorite -names-golem-dataset-1190 = Tridymite -names-golem-dataset-1191 = Triphylite -names-golem-dataset-1192 = Triplite -names-golem-dataset-1193 = Triploidite -names-golem-dataset-1194 = Tripuhyite -names-golem-dataset-1195 = Troilite -names-golem-dataset-1196 = Trona -names-golem-dataset-1197 = Tschermakite -names-golem-dataset-1198 = Tschermigite -names-golem-dataset-1199 = Tsumcorite -names-golem-dataset-1200 = Tsumebite -names-golem-dataset-1201 = Tugtupite -names-golem-dataset-1202 = Tungsten -names-golem-dataset-1203 = Tungstite -names-golem-dataset-1204 = Tuperssuatsiaite -names-golem-dataset-1205 = Turquoise -names-golem-dataset-1206 = Tusionite -names-golem-dataset-1207 = Tyrolite -names-golem-dataset-1208 = Tyrrellite -names-golem-dataset-1209 = Tyuyamunite -names-golem-dataset-1210 = Uchucchacuaite -names-golem-dataset-1211 = Uklonskovite -names-golem-dataset-1212 = Ulexite -names-golem-dataset-1213 = Ullmannite -names-golem-dataset-1214 = Ulrichite -names-golem-dataset-1215 = Ulvospinel -names-golem-dataset-1216 = Umangite -names-golem-dataset-1217 = Umbite -names-golem-dataset-1218 = Upalite -names-golem-dataset-1219 = Uraninite -names-golem-dataset-1220 = Uranocircite -names-golem-dataset-1221 = Uranophane -names-golem-dataset-1222 = Uranopilite -names-golem-dataset-1223 = Urea -names-golem-dataset-1224 = Uricite -names-golem-dataset-1225 = Urusovite -names-golem-dataset-1226 = Ussingite -names-golem-dataset-1227 = Utahite -names-golem-dataset-1228 = Uvarovite -names-golem-dataset-1229 = Uytenbogaardtite -names-golem-dataset-1230 = Vaesite -names-golem-dataset-1231 = Valentinite -names-golem-dataset-1232 = Valleriite -names-golem-dataset-1233 = Vanadinite -names-golem-dataset-1234 = Vanadiocarpholite -names-golem-dataset-1235 = Vanadium -names-golem-dataset-1236 = Vantasselite -names-golem-dataset-1237 = Vanuralite -names-golem-dataset-1238 = Variscite -names-golem-dataset-1239 = Vaterite -names-golem-dataset-1240 = Vauquelinite -names-golem-dataset-1241 = Vauxite -names-golem-dataset-1242 = Veatchite -names-golem-dataset-1243 = Vermiculite -names-golem-dataset-1244 = Vesuvianite -names-golem-dataset-1245 = Villiaumite -names-golem-dataset-1246 = Violarite -names-golem-dataset-1247 = Vishnevite -names-golem-dataset-1248 = Vivianite -names-golem-dataset-1249 = Vladimirite -names-golem-dataset-1250 = Vlasovite -names-golem-dataset-1251 = Volborthite -names-golem-dataset-1252 = Vulcanite -names-golem-dataset-1253 = Wadsleyite -names-golem-dataset-1254 = Wagnerite -names-golem-dataset-1255 = Wairakite -names-golem-dataset-1256 = Wakabayashilite -names-golem-dataset-1257 = Wakefieldite -names-golem-dataset-1258 = Walfordite -names-golem-dataset-1259 = Wardite -names-golem-dataset-1260 = Warikahnite -names-golem-dataset-1261 = Warwickite -names-golem-dataset-1262 = Wassonite -names-golem-dataset-1263 = Wavellite -names-golem-dataset-1264 = Weddellite -names-golem-dataset-1265 = Weeksite -names-golem-dataset-1266 = Weilite -names-golem-dataset-1267 = Weissite -names-golem-dataset-1268 = Weloganite -names-golem-dataset-1269 = Whewellite -names-golem-dataset-1270 = Whiteite -names-golem-dataset-1271 = Whitlockite -names-golem-dataset-1272 = Willemite -names-golem-dataset-1273 = Wiluite -names-golem-dataset-1274 = Witherite -names-golem-dataset-1275 = Wodginite -names-golem-dataset-1276 = Wolframite -names-golem-dataset-1277 = Wollastonite -names-golem-dataset-1278 = Woodhouseite -names-golem-dataset-1279 = Wulfenite -names-golem-dataset-1280 = Wurtzite -names-golem-dataset-1281 = Wustite -names-golem-dataset-1282 = Wyartite -names-golem-dataset-1283 = Xanthiosite -names-golem-dataset-1284 = Xanthoconite -names-golem-dataset-1285 = Xanthoxenite -names-golem-dataset-1286 = Xenophyllite -names-golem-dataset-1287 = Xenotime -names-golem-dataset-1288 = Xiangjiangite -names-golem-dataset-1289 = Xieite -names-golem-dataset-1290 = Xifengite -names-golem-dataset-1291 = Xilingolite -names-golem-dataset-1292 = Ximengite -names-golem-dataset-1293 = Xingzhongite -names-golem-dataset-1294 = Xitieshanite -names-golem-dataset-1295 = Xocolatlite -names-golem-dataset-1296 = Xocomecatlite -names-golem-dataset-1297 = Xonotlite -names-golem-dataset-1298 = Ye'elimite -names-golem-dataset-1299 = Yttrialite -names-golem-dataset-1300 = Yttropyrochlore -names-golem-dataset-1301 = Yuksporite -names-golem-dataset-1302 = Zabuyelite -names-golem-dataset-1303 = Zaccagnaite -names-golem-dataset-1304 = Zaherite -names-golem-dataset-1305 = Zairite -names-golem-dataset-1306 = Zakharovite -names-golem-dataset-1307 = Zanazziite -names-golem-dataset-1308 = Zaratite -names-golem-dataset-1309 = Zektzerite -names-golem-dataset-1310 = Zemannite -names-golem-dataset-1311 = Zeolite -names-golem-dataset-1312 = Zeunerite -names-golem-dataset-1313 = Zhanghengite -names-golem-dataset-1314 = Zharchikhite -names-golem-dataset-1315 = Zhemchuzhnikovite -names-golem-dataset-1316 = Ziesite -names-golem-dataset-1317 = Zimbabweite -names-golem-dataset-1318 = Zincite -names-golem-dataset-1319 = Zinclipscombite -names-golem-dataset-1320 = Zincmelanterite -names-golem-dataset-1321 = Zincobotryogen -names-golem-dataset-1322 = Zincochromite -names-golem-dataset-1323 = Zincolivenite -names-golem-dataset-1324 = Zinkenite -names-golem-dataset-1325 = Zinnwaldite -names-golem-dataset-1326 = Zippeite -names-golem-dataset-1327 = Zircon -names-golem-dataset-1328 = Zirconolite -names-golem-dataset-1329 = Zircophyllite -names-golem-dataset-1330 = Zirkelite -names-golem-dataset-1331 = Znucalite -names-golem-dataset-1332 = Zoisite -names-golem-dataset-1333 = Zorite -names-golem-dataset-1334 = Zunyite -names-golem-dataset-1335 = Zussmanite -names-golem-dataset-1336 = Zykaite diff --git a/Resources/Locale/en-US/implant/implant.ftl b/Resources/Locale/en-US/implant/implant.ftl index 8cddef4c81..3f38ae443f 100644 --- a/Resources/Locale/en-US/implant/implant.ftl +++ b/Resources/Locale/en-US/implant/implant.ftl @@ -25,12 +25,3 @@ implanter-label-draw = [color=red]{$implantName}[/color] Mode: [color=white]{$modeString}[/color] implanter-contained-implant-text = [color=green]{$desc}[/color] - -## Implant Popups - -scramble-implant-activated-popup = Your appearance shifts and changes! - -## Implant Messages - -deathrattle-implant-dead-message = {$user} has died {$position}. -deathrattle-implant-critical-message = {$user} life signs critical, immediate assistance required {$position}. diff --git a/Resources/Locale/en-US/mind/role-types.ftl b/Resources/Locale/en-US/mind/role-types.ftl index 7d568fd686..4614d20a47 100644 --- a/Resources/Locale/en-US/mind/role-types.ftl +++ b/Resources/Locale/en-US/mind/role-types.ftl @@ -33,3 +33,4 @@ role-subtype-survivor = Survivor role-subtype-subverted = Subverted role-subtype-paradox-clone = Paradox role-subtype-wizard = Wizard +role-subtype-changeling = Changeling diff --git a/Resources/Locale/en-US/nutrition/components/ingestion-system.ftl b/Resources/Locale/en-US/nutrition/components/ingestion-system.ftl index 692100e61a..a82eae11e3 100644 --- a/Resources/Locale/en-US/nutrition/components/ingestion-system.ftl +++ b/Resources/Locale/en-US/nutrition/components/ingestion-system.ftl @@ -26,9 +26,12 @@ ingestion-verb-drink = Drink # Edible Component edible-nom = Nom. {$flavors} +edible-nom-other = Nom. edible-slurp = Slurp. {$flavors} +edible-slurp-other = Slurp. edible-swallow = You swallow { THE($food) } edible-gulp = Gulp. {$flavors} +edible-gulp-other = Gulp. edible-has-used-storage = You cannot {$verb} { THE($food) } with an item stored inside. diff --git a/Resources/Locale/en-US/polymorph/polymorph.ftl b/Resources/Locale/en-US/polymorph/polymorph.ftl index ac78eb6bb4..2a74b0df32 100644 --- a/Resources/Locale/en-US/polymorph/polymorph.ftl +++ b/Resources/Locale/en-US/polymorph/polymorph.ftl @@ -3,3 +3,5 @@ polymorph-self-action-description = Instantly polymorph yourself into {$target}. polymorph-popup-generic = {CAPITALIZE(THE($parent))} turned into {$child}. polymorph-revert-popup-generic = {CAPITALIZE(THE($parent))} reverted back into {$child}. + +polymorph-paused-map-name = Polymorph body storage map diff --git a/Resources/Locale/en-US/seeds/seeds.ftl b/Resources/Locale/en-US/seeds/seeds.ftl index 1ca559db30..379f25183d 100644 --- a/Resources/Locale/en-US/seeds/seeds.ftl +++ b/Resources/Locale/en-US/seeds/seeds.ftl @@ -142,3 +142,5 @@ seeds-cherry-name = cherry seeds-cherry-display-name = cherry tree seeds-anomaly-berry-name = anomaly berry seeds-anomaly-berry-display-name = anomaly berries +seeds-bloonion-name = bloonion +seeds-bloonion-display-name = bloonion bulbs diff --git a/Resources/Locale/en-US/silicons/station-ai.ftl b/Resources/Locale/en-US/silicons/station-ai.ftl index 81fee66ccb..442782f9a1 100644 --- a/Resources/Locale/en-US/silicons/station-ai.ftl +++ b/Resources/Locale/en-US/silicons/station-ai.ftl @@ -1,5 +1,5 @@ # General -ai-wire-snipped = Wire has been cut at {$coords}. +ai-wire-snipped = One of your systems' wires has been cut at {$source}. wire-name-ai-vision-light = AIV wire-name-ai-act-light = AIA station-ai-takeover = AI takeover diff --git a/Resources/Locale/en-US/triggers/rattle-on-trigger.ftl b/Resources/Locale/en-US/triggers/rattle-on-trigger.ftl new file mode 100644 index 0000000000..3d090f1ae3 --- /dev/null +++ b/Resources/Locale/en-US/triggers/rattle-on-trigger.ftl @@ -0,0 +1,2 @@ +rattle-on-trigger-dead-message = {$user} has died {$position}. +rattle-on-trigger-critical-message = {$user} life signs critical, immediate assistance required {$position}. diff --git a/Resources/Locale/en-US/triggers/scramble-on-trigger.ftl b/Resources/Locale/en-US/triggers/scramble-on-trigger.ftl new file mode 100644 index 0000000000..1e84766032 --- /dev/null +++ b/Resources/Locale/en-US/triggers/scramble-on-trigger.ftl @@ -0,0 +1 @@ +scramble-on-trigger-popup = Your appearance shifts and changes! diff --git a/Resources/Locale/en-US/weapons/ranged/turrets.ftl b/Resources/Locale/en-US/weapons/ranged/turrets.ftl index 213599d926..8a21647075 100644 --- a/Resources/Locale/en-US/weapons/ranged/turrets.ftl +++ b/Resources/Locale/en-US/weapons/ranged/turrets.ftl @@ -9,4 +9,5 @@ deployable-turret-component-is-broken = The turret is heavily damaged and must b deployable-turret-component-cannot-access-wires = You can't reach the maintenance panel while the turret is active # Turret notification for station AI -station-ai-turret-is-attacking-warning = {CAPITALIZE($source)} has engaged a hostile target. \ No newline at end of file +station-ai-turret-component-name = {$name} ({$address}) +station-ai-turret-component-is-attacking-warning = {CAPITALIZE($source)} has engaged a hostile target. \ No newline at end of file diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml index e6587ae6b8..97435c229d 100644 --- a/Resources/Prototypes/Actions/types.yml +++ b/Resources/Prototypes/Actions/types.yml @@ -121,7 +121,7 @@ state: gib - type: entity - parent: BaseAction + parent: BaseImplantAction id: ActionActivateFreedomImplant name: Break Free description: Activating your freedom implant will free you from any hand restraints @@ -135,8 +135,6 @@ icon: sprite: Actions/Implants/implants.rsi state: freedom - - type: InstantAction - event: !type:UseFreedomImplantEvent - type: entity parent: BaseAction @@ -171,7 +169,7 @@ state: icon - type: entity - parent: BaseAction + parent: BaseImplantAction id: ActionActivateScramImplant name: SCRAM! description: Randomly teleports you within a large distance. @@ -186,11 +184,9 @@ icon: sprite: Structures/Specific/anomaly.rsi state: anom4 - - type: InstantAction - event: !type:UseScramImplantEvent - type: entity - parent: BaseAction + parent: BaseImplantAction id: ActionActivateDnaScramblerImplant name: Scramble DNA description: Randomly changes your name and appearance. @@ -205,8 +201,6 @@ icon: sprite: Clothing/OuterClothing/Hardsuits/lingspacesuit.rsi state: icon - - type: InstantAction - event: !type:UseDnaScramblerImplantEvent - type: entity parent: BaseAction @@ -409,12 +403,12 @@ components: - type: Action useDelay: 8 - icon: + icon: sprite: Interface/Actions/jump.rsi state: icon - type: InstantAction event: !type:GravityJumpEvent {} - + - type: entity parent: BaseToggleAction id: ActionToggleRootable diff --git a/Resources/Prototypes/Antag/changeling.yml b/Resources/Prototypes/Antag/changeling.yml new file mode 100644 index 0000000000..b4c8b2e3dc --- /dev/null +++ b/Resources/Prototypes/Antag/changeling.yml @@ -0,0 +1,35 @@ +- type: entity + parent: MobHuman + id: MobLing + name: Urist McLing + suffix: Non-Antag + components: + - type: ChangelingDevour + - type: ChangelingIdentity + - type: ChangelingTransform + - type: ActionGrant + actions: + - ActionRetractableItemArmBlade # Temporary addition, will inevitably be a purchasable in the bio-store + +- type: entity + id: ActionChangelingDevour + name: "[color=red]Devour[/color]" + description: Consume the essence of your victims and subsume their identity and mind into your own. + components: + - type: Action + icon: { sprite : Interface/Actions/changeling.rsi, state: "devour" } + iconOn: { sprite : Interface/Actions/changeling.rsi, state: "devour_on" } + priority: 1 + - type: TargetAction + - type: EntityTargetAction + event: !type:ChangelingDevourActionEvent + +- type: entity + id: ActionChangelingTransform + name: "[color=red]Transform[/color]" + description: Transform and assume the identities of those you have devoured. + components: + - type: Action + icon: { sprite : Interface/Actions/changeling.rsi, state: "transform" } + - type: InstantAction + event: !type:ChangelingTransformActionEvent diff --git a/Resources/Prototypes/Chat/notifications.yml b/Resources/Prototypes/Chat/notifications.yml new file mode 100644 index 0000000000..c1aee755c6 --- /dev/null +++ b/Resources/Prototypes/Chat/notifications.yml @@ -0,0 +1,21 @@ +- type: chatNotification + id: IntellicardDownload + message: ai-consciousness-download-warning + color: Red + sound: /Audio/Misc/notice2.ogg + nextDelay: 8 + +- type: chatNotification + id: TurretIsAttacking + message: station-ai-turret-component-is-attacking-warning + color: Orange + sound: /Audio/Misc/notice2.ogg + nextDelay: 14 + notifyBySource: true + +- type: chatNotification + id: AiWireSnipped + message: ai-wire-snipped + color: Pink + nextDelay: 12 + notifyBySource: true diff --git a/Resources/Prototypes/Datasets/Names/golem.yml b/Resources/Prototypes/Datasets/Names/golem.yml deleted file mode 100644 index 52ecc30c9b..0000000000 --- a/Resources/Prototypes/Datasets/Names/golem.yml +++ /dev/null @@ -1,5 +0,0 @@ -- type: localizedDataset - id: NamesGolem - values: - prefix: names-golem-dataset- - count: 1336 diff --git a/Resources/Prototypes/Entities/Mobs/Player/clone.yml b/Resources/Prototypes/Entities/Mobs/Player/clone.yml index bdf741e58a..907b694db9 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/clone.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/clone.yml @@ -1,13 +1,12 @@ # Settings for cloning bodies # If you add a new trait, job specific component or a component doing visual/examination changes for humanoids # then add it here to the correct prototype. -# The datafields of the components are only shallow copied using CopyComp. +# The datafields of the components copied using CopyComp. # Subscribe to CloningEvent instead if that is not enough. -# for basic traits etc. -# used by the random clone spawner +# for basic physical traits - type: cloningSettings - id: BaseClone + id: Body components: # general - DetailExaminable @@ -15,8 +14,9 @@ - Fingerprint - NpcFactionMember # traits - # - LegsParalyzed (you get healed) - BlackAndWhiteOverlay + - Clumsy + # - LegsParalyzed (you get healed) - LightweightDrunk - Muted - Narcolepsy @@ -26,14 +26,6 @@ - PermanentBlindness - Snoring - Unrevivable - # job specific - - BibleUser - - CommandStaff - - Clumsy - - MindShield - - MimePowers - - SpaceNinja - - Thieving # accents - Accentless - BackwardsAccent @@ -62,6 +54,30 @@ - SouthernAccent - SpanishAccent - StutteringAccent + +# for job-specific traits etc. +- type: cloningSettings + id: Special + components: + - BibleUser + - CommandStaff + - MindShield + - MimePowers + - SpaceNinja + - Thieving + +# antag roles +- type: cloningSettings + id: Antag + components: + - HeadRevolutionary + - Revolutionary + - NukeOperative + +# a full clone with all traits and items, but no antag roles +- type: cloningSettings + id: BaseClone + parent: [Body, Special] blacklist: components: - AttachedClothing # helmets, which are part of the suit @@ -69,26 +85,47 @@ - Implanter # they will spawn full again, but you already get the implant. And we can't do item slot copying yet - VirtualItem -# all antagonist roles -- type: cloningSettings - id: Antag - parent: BaseClone - components: - - HeadRevolutionary - - Revolutionary - - NukeOperative - # for cloning pods - type: cloningSettings id: CloningPod - parent: Antag + parent: [BaseClone, Antag] forceCloning: false copyEquipment: null copyInternalStorage: false copyImplants: false -# spawner +# for paradox clones +- type: cloningSettings + id: ParadoxCloningSettings + parent: [BaseClone, Antag] +# changeling identity copying +- type: cloningSettings + id: ChangelingCloningSettings + parent: Body + components: + # These are already part of the base species prototype that is spawned for the clone, + # that means we only need to copy them over when switching between species. + # So these don't need to be part of the Body settings, unless someone makes a trait that adjusts these components. + - BodyEmotes + # - Fixtures TODO: A better way to clone fixtures or a fixture fix. Currently if you devour someone on the ground and transform, you lose collision with tables as they were knocked down when they were copied. + - Speech + - TypingIndicator + - ScaleVisuals # for dwarf height + eventComponents: + # these need special treatment in the event subscription + - Inventory # arachnid pockets and diona feet + - Wagging # lizard tails + - Vocal # voice sounds + - Storage # slime storage + - Rootable # diona + - Sericulture # arachnids + - MovementSpeedModifier # moths when weightless + copyEquipment: null + copyInternalStorage: false + copyImplants: false + +# spawner - type: entity id: RandomCloneSpawner name: Random Clone diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 9ecce3904d..8fcaef42b9 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -81,6 +81,7 @@ horizontalRotation: 90 - type: HumanoidAppearance species: Human + - type: TypingIndicator - type: SlowOnDamage speedModifierThresholds: 60: 0.7 diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/food_base.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/food_base.yml index fedce70e79..6ba1895548 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/food_base.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/food_base.yml @@ -2,9 +2,9 @@ # Base component for edible food-like items # - type: entity + abstract: true parent: BaseItem id: EdibleBase - abstract: true components: - type: FlavorProfile flavors: @@ -19,9 +19,9 @@ # that should be cleaned up in space # - type: entity + abstract: true parent: EdibleBase id: FoodBase - abstract: true components: - type: SpaceGarbage @@ -30,9 +30,9 @@ # But it might in future also mean drawing with a syringe, so this is a base prototype just in case. - type: entity + abstract: true parent: FoodBase id: FoodInjectableBase - abstract: true components: - type: InjectableSolution solution: food @@ -42,8 +42,8 @@ # usable by any food that can be opened # handles appearance with states "icon" and "icon-open" - type: entity - id: FoodOpenableBase abstract: true + id: FoodOpenableBase components: - type: Appearance - type: Sprite diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml index 721c2e3e38..3841dc06b0 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml @@ -2756,3 +2756,78 @@ entries: Taco: AnomalyBerry Burger: AnomalyBerryBurger + +- type: entity + name: bloonion bulb + description: A strange floating bulb.. Nothing worth crying over. + parent: BaseStructureDynamic + id: FoodBloonion + components: + - type: Appearance + - type: Sprite + drawdepth: Items + noRot: true + sprite: Objects/Specific/Hydroponics/bloonion.rsi + state: produce + - type: Item + size: Small + sprite: Objects/Specific/Hydroponics/bloonion.rsi + heldPrefix: produce + - type: Produce + seedId: bloonion + - type: Tag + tags: + - Vegetable + - type: PotencyVisuals + - type: FlavorProfile + flavors: + - onion + - strange + - type: SolutionContainerManager + solutions: + food: + maxVol: 7 + canReact: false + reagents: + - ReagentId: Potassium + Quantity: 1 + - ReagentId: Phosphorus + Quantity: 1 + - ReagentId: Sugar + Quantity: 1 + - ReagentId: Allicin + Quantity: 4 + - type: Extractable + grindableSolutionName: food + - type: ExplodeOnTrigger + - type: Explosive + explosionType: Default + maxIntensity: 0.001 + intensitySlope: 1 + totalIntensity: 0.1 + - type: Damageable + damageContainer: Biological + - type: Destructible + thresholds: + - trigger: + !type:DamageTrigger + damage: 1 + behaviors: + - !type:SpillBehavior + solution: food + - !type:TriggerBehavior + - !type:DoActsBehavior + acts: [ "Destruction" ] + - type: InteractionOutline + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeCircle + radius: 0.20 + position: 0, 0.35 + density: 80 + mask: + - MobMask + layer: + - MobLayer diff --git a/Resources/Prototypes/Entities/Objects/Misc/implanters.yml b/Resources/Prototypes/Entities/Objects/Misc/implanters.yml index 79432e6fc1..e1918ef5e6 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/implanters.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/implanters.yml @@ -13,7 +13,7 @@ - type: Implanter whitelist: components: - - Body # no chair microbomb + - MobState # no chair microbomb blacklist: components: - Guardian # no holoparasite macrobomb wombo combo diff --git a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml index a369a730cf..6a4ad24664 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml @@ -135,11 +135,14 @@ description: This implant lets the user break out of hand restraints up to three times before ceasing to function anymore. categories: [ HideSpawnMenu ] components: - - type: SubdermalImplant - implantAction: ActionActivateFreedomImplant - whitelist: - components: - - Cuffable # useless if you cant be cuffed + - type: SubdermalImplant + implantAction: ActionActivateFreedomImplant + whitelist: + components: + - Cuffable # useless if you cant be cuffed + - type: TriggerOnActivateImplant + - type: UncuffOnTrigger + targetUser: true - type: entity parent: BaseSubdermalImplant @@ -198,7 +201,8 @@ - type: SubdermalImplant implantAction: ActionActivateScramImplant - type: TriggerOnActivateImplant - - type: ScramImplant + - type: ScramOnTrigger + targetUser: true - type: entity parent: BaseSubdermalImplant @@ -207,11 +211,15 @@ description: This implant lets the user randomly change their appearance and name once. categories: [ HideSpawnMenu ] components: - - type: SubdermalImplant - implantAction: ActionActivateDnaScramblerImplant - whitelist: - components: - - HumanoidAppearance # syndies cant turn hamlet into a human + - type: SubdermalImplant + implantAction: ActionActivateDnaScramblerImplant + whitelist: + components: + - HumanoidAppearance # syndies cant turn hamlet into a human + - type: TriggerOnActivateImplant + - type: DnaScrambleOnTrigger + targetUser: true + - type: DeleteOnTrigger - type: entity categories: [ HideSpawnMenu, Spawner ] diff --git a/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/seeds.yml b/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/seeds.yml index 1777d8675c..ca6270ab64 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/seeds.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/seeds.yml @@ -762,3 +762,13 @@ seedId: anomalyBerry - type: Sprite sprite: Objects/Specific/Hydroponics/anomaly_berry.rsi + +- type: entity + parent: SeedBase + name: packet of bloonion seeds + id: BloonionSeeds + components: + - type: Seed + seedId: bloonion + - type: Sprite + sprite: Objects/Specific/Hydroponics/bloonion.rsi diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Bombs/spider.yml b/Resources/Prototypes/Entities/Objects/Weapons/Bombs/spider.yml index d00297a723..3d7991cf0f 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Bombs/spider.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Bombs/spider.yml @@ -28,7 +28,7 @@ components: - Anchorable - Item - - Body + - MobState - type: Explosive # Powerful explosion in a large radius. Will break underplating. explosionType: DemolitionCharge totalIntensity: 360 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/magic.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/magic.yml index 9e2b6d76c1..d1b50429f9 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/magic.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/magic.yml @@ -93,7 +93,7 @@ disableWhenFirstOpened: true whitelist: components: - - Body + - MobState - type: LockVisuals stateLocked: cursed_door stateUnlocked: decursed_door @@ -191,7 +191,7 @@ - type: WhitelistTriggerCondition userWhitelist: components: - - Body + - MobState - type: entity id: ProjectilePolyboltMonkey @@ -205,7 +205,7 @@ - type: WhitelistTriggerCondition userWhitelist: components: - - Body + - MobState - type: entity id: ProjectilePolyboltDoor @@ -275,7 +275,7 @@ - type: WhitelistTriggerCondition userWhitelist: components: - - Body + - MobState - type: entity id: ProjectileIcicle @@ -305,4 +305,4 @@ - type: WhitelistTriggerCondition userWhitelist: components: - - Body + - MobState diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_energy.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_energy.yml index 077d5dc5fd..40c67c898d 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_energy.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Turrets/turrets_energy.yml @@ -144,6 +144,7 @@ components: - type: AccessReader access: [["StationAi"], ["ResearchDirector"]] + - type: StationAiTurret - type: TurretTargetSettings exemptAccessLevels: - Borg diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/baseball_bat.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/baseball_bat.yml index b09dfee840..3aa5b041f8 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/baseball_bat.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/baseball_bat.yml @@ -23,6 +23,11 @@ Structural: 10 - type: Item size: Normal + shape: + - 0,0,0,3 + storedSprite: + state: storage + sprite: Objects/Weapons/Melee/baseball_bat.rsi - type: Tool qualities: - Rolling @@ -54,6 +59,11 @@ state: icon - type: Item size: Normal + shape: + - 0,0,0,3 + storedSprite: + state: storage + sprite: Objects/Weapons/Melee/incomplete_bat.rsi sprite: Objects/Weapons/Melee/incomplete_bat.rsi - type: Construction graph: WoodenBat diff --git a/Resources/Prototypes/GameRules/roundstart.yml b/Resources/Prototypes/GameRules/roundstart.yml index 08d088a08c..1748a4c5a3 100644 --- a/Resources/Prototypes/GameRules/roundstart.yml +++ b/Resources/Prototypes/GameRules/roundstart.yml @@ -225,6 +225,31 @@ mindRoles: - MindRoleTraitorReinforcement +- type: entity + id: Changeling + parent: BaseGameRule + components: + - type: GameRule + minPlayers: 25 + - type: AntagSelection + definitions: + - prefRoles: [ Changeling ] + max: 3 + playerRatio: 15 + briefing: + text: changeling-role-greeting + color: Red + sound: /Audio/Ambience/Antag/changeling_start.ogg + components: + - type: ChangelingDevour + - type: ChangelingIdentity + - type: ChangelingTransform + - type: ActionGrant + actions: + - ActionRetractableItemArmBlade # Temporary addition, will inevitably be a purchasable in the bio-store + mindRoles: + - MindRoleChangeling + - type: entity id: Revolutionary parent: BaseGameRule diff --git a/Resources/Prototypes/Hydroponics/seeds.yml b/Resources/Prototypes/Hydroponics/seeds.yml index 55cec210af..37e8a1beb3 100644 --- a/Resources/Prototypes/Hydroponics/seeds.yml +++ b/Resources/Prototypes/Hydroponics/seeds.yml @@ -832,6 +832,7 @@ - FoodOnion mutationPrototypes: - onionred + - bloonion lifespan: 25 maturation: 8 production: 6 @@ -1994,3 +1995,38 @@ Min: 1 Max: 2 PotencyDivisor: 40 + +- type: seed + id: bloonion + name: seeds-bloonion-name + noun: seeds-noun-seeds + displayName: seeds-bloonion-display-name + plantRsi: Objects/Specific/Hydroponics/bloonion.rsi + packetPrototype: BloonionSeeds + productPrototypes: + - FoodBloonion + lifespan: 25 + maturation: 15 + production: 3 + yield: 3 + potency: 10 + growthStages: 4 + waterConsumption: 0.60 + nutrientConsumption: 0.50 + chemicals: + Potassium: + Min: 1 + Max: 5 + PotencyDivisor: 20 + Phosphorus: + Min: 1 + Max: 5 + PotencyDivisor: 20 + Sugar: + Min: 1 + Max: 5 + PotencyDivisor: 20 + Allicin: + Min: 1 + Max: 10 + PotencyDivisor: 10 diff --git a/Resources/Prototypes/Magic/mindswap_spell.yml b/Resources/Prototypes/Magic/mindswap_spell.yml index 8039f12074..0a28a9683b 100644 --- a/Resources/Prototypes/Magic/mindswap_spell.yml +++ b/Resources/Prototypes/Magic/mindswap_spell.yml @@ -14,7 +14,7 @@ - type: EntityTargetAction whitelist: components: - - Body # this also allows borgs because that supercode uses Body for no reason + - MobState - PAI # intended to mindswap pAIs and AIs - StationAiCore event: !type:MindSwapSpellEvent diff --git a/Resources/Prototypes/Magic/touch_spells.yml b/Resources/Prototypes/Magic/touch_spells.yml index ba62a76421..f3b44eabb1 100644 --- a/Resources/Prototypes/Magic/touch_spells.yml +++ b/Resources/Prototypes/Magic/touch_spells.yml @@ -12,7 +12,7 @@ - type: EntityTargetAction whitelist: components: - - Body + - MobState canTargetSelf: false - type: entity diff --git a/Resources/Prototypes/Nutrition/edible.yml b/Resources/Prototypes/Nutrition/edible.yml index 1b29bbed69..3dd11521ef 100644 --- a/Resources/Prototypes/Nutrition/edible.yml +++ b/Resources/Prototypes/Nutrition/edible.yml @@ -11,6 +11,7 @@ volume: -1 collection: eating # I think this *should* grab the sound specifier... message: edible-nom + otherMessage: edible-nom-other verb: edible-verb-food noun: edible-noun-food verbName: ingestion-verb-food @@ -26,6 +27,7 @@ volume: -2 path: /Audio/Items/drink.ogg message: edible-slurp + otherMessage: edible-slurp-other verb: edible-verb-drink noun: edible-noun-drink verbName: ingestion-verb-drink @@ -41,6 +43,7 @@ volume: -1 path: /Audio/Items/pill.ogg message: edible-swallow + otherMessage: edible-gulp-other verb: edible-verb-pill noun: edible-noun-pill verbName: ingestion-verb-food diff --git a/Resources/Prototypes/Objectives/traitor.yml b/Resources/Prototypes/Objectives/traitor.yml index 98c6b9789f..411a457961 100644 --- a/Resources/Prototypes/Objectives/traitor.yml +++ b/Resources/Prototypes/Objectives/traitor.yml @@ -91,6 +91,12 @@ - type: TargetObjective title: objective-condition-maroon-person-title - type: PickRandomPerson + filters: + # Can't have multiple objectives to kill the same person. + - !type:TargetObjectiveMindFilter + blacklist: + components: + - KillPersonCondition - type: KillPersonCondition requireMaroon: true @@ -106,7 +112,17 @@ unique: true - type: TargetObjective title: objective-condition-kill-maroon-title - - type: PickRandomHead + - type: PickRandomPerson + filters: + - !type:BodyMindFilter + whitelist: + components: + - CommandStaff + # Can't have multiple objectives to kill the same person. + - !type:TargetObjectiveMindFilter + blacklist: + components: + - KillPersonCondition - type: KillPersonCondition # don't count missing evac as killing as heads are higher profile, so you really need to do the dirty work # if ce flies a shittle to centcom you better find a way onto it @@ -124,7 +140,18 @@ difficulty: 1.75 - type: TargetObjective title: objective-condition-other-traitor-alive-title - - type: RandomTraitorAlive + - type: PickRandomPerson + filters: + - !type:HasRoleMindFilter + whitelist: + components: + - TraitorRole + # Can't have multiple objectives to help/save the same person + - !type:TargetObjectiveMindFilter + blacklist: + components: + - RandomTraitorAlive + - RandomTraitorProgress - type: entity parent: [BaseTraitorSocialObjective, BaseHelpProgressObjective] @@ -135,7 +162,25 @@ difficulty: 2.5 - type: TargetObjective title: objective-condition-other-traitor-progress-title - - type: RandomTraitorProgress + - type: PickRandomPerson + filters: + - !type:HasRoleMindFilter + whitelist: + components: + - TraitorRole + # Can't help anyone who is tasked with helping: + # 1. thats boring + # 2. no cyclic progress dependencies!!! + - !type:ObjectiveMindFilter + blacklist: + components: + - HelpProgressCondition + # Can't have multiple objectives to help/save the same person + - !type:TargetObjectiveMindFilter + blacklist: + components: + - RandomTraitorAlive + - RandomTraitorProgress # steal diff --git a/Resources/Prototypes/Roles/Antags/changeling.yml b/Resources/Prototypes/Roles/Antags/changeling.yml new file mode 100644 index 0000000000..3b19c993e6 --- /dev/null +++ b/Resources/Prototypes/Roles/Antags/changeling.yml @@ -0,0 +1,6 @@ +- type: antag + id: Changeling + name: roles-antag-changeling-name + antagonist: true + setPreference: false # TODO: set this to true once Changeling exits WIP status + objective: roles-antag-changeling-objective diff --git a/Resources/Prototypes/Roles/Jobs/CentComm/official.yml b/Resources/Prototypes/Roles/Jobs/CentComm/official.yml index 68fc11b679..9c28c56002 100644 --- a/Resources/Prototypes/Roles/Jobs/CentComm/official.yml +++ b/Resources/Prototypes/Roles/Jobs/CentComm/official.yml @@ -22,7 +22,7 @@ jumpsuit: ClothingUniformJumpsuitCentcomOfficial shoes: ClothingShoesBootsCombatFilled head: ClothingHeadHatCentcom - eyes: ClothingEyesGlassesSunglasses + eyes: ClothingEyesGlassesCommand gloves: ClothingHandsGlovesColorBlack outerClothing: ClothingOuterArmorCentcommCarapace id: CentcomPDA diff --git a/Resources/Prototypes/Roles/Jobs/Command/captain.yml b/Resources/Prototypes/Roles/Jobs/Command/captain.yml index bc7aef6ab0..fa02e160a8 100644 --- a/Resources/Prototypes/Roles/Jobs/Command/captain.yml +++ b/Resources/Prototypes/Roles/Jobs/Command/captain.yml @@ -38,7 +38,7 @@ id: CaptainGear equipment: shoes: ClothingShoesBootsLaceup - eyes: ClothingEyesGlassesSunglasses + eyes: ClothingEyesGlassesCommand gloves: ClothingHandsGlovesCaptain id: CaptainPDA ears: ClothingHeadsetAltCommand diff --git a/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml b/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml index 5bdbe47e80..7bd1ff5c52 100644 --- a/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml +++ b/Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml @@ -66,6 +66,7 @@ gloves: ClothingHandsGlovesHop ears: ClothingHeadsetAltCommand belt: BoxFolderClipboard + eyes: ClothingEyesHudCommand storage: back: - Flash diff --git a/Resources/Prototypes/Roles/Jobs/Fun/visitors_startinggear.yml b/Resources/Prototypes/Roles/Jobs/Fun/visitors_startinggear.yml index d3bb481ff7..b7db27a972 100644 --- a/Resources/Prototypes/Roles/Jobs/Fun/visitors_startinggear.yml +++ b/Resources/Prototypes/Roles/Jobs/Fun/visitors_startinggear.yml @@ -7,7 +7,7 @@ equipment: jumpsuit: ClothingUniformJumpsuitCaptain shoes: ClothingShoesBootsLaceup - eyes: ClothingEyesGlassesSunglasses + eyes: ClothingEyesGlassesCommand gloves: ClothingHandsGlovesCaptain head: ClothingHeadHatCaptain neck: ClothingNeckCloakCap @@ -23,7 +23,7 @@ equipment: jumpsuit: ClothingUniformJumpsuitCapFormal shoes: ClothingShoesBootsLaceup - eyes: ClothingEyesGlassesSunglasses + eyes: ClothingEyesGlassesCommand gloves: ClothingHandsGlovesCaptain head: ClothingHeadHatCapcap neck: ClothingNeckMantleCap diff --git a/Resources/Prototypes/Roles/MindRoles/mind_roles.yml b/Resources/Prototypes/Roles/MindRoles/mind_roles.yml index 79223ed67d..95d49c1b83 100644 --- a/Resources/Prototypes/Roles/MindRoles/mind_roles.yml +++ b/Resources/Prototypes/Roles/MindRoles/mind_roles.yml @@ -307,3 +307,16 @@ exclusiveAntag: true subtype: role-subtype-zombie - type: ZombieRole + +# Changeling +- type: entity + parent: BaseMindRoleAntag + id: MindRoleChangeling + name: Changeling Role + components: + - type: MindRole + antagPrototype: Changeling + exclusiveAntag: true + roleType: SoloAntagonist + subtype: role-subtype-changeling + - type: ChangelingRole diff --git a/Resources/Prototypes/SoundCollections/changeling.yml b/Resources/Prototypes/SoundCollections/changeling.yml new file mode 100644 index 0000000000..4edb07dc16 --- /dev/null +++ b/Resources/Prototypes/SoundCollections/changeling.yml @@ -0,0 +1,14 @@ +- type: soundCollection + id: ChangelingDevourConsume + files: + - /Audio/Effects/Changeling/devour_consume.ogg + +- type: soundCollection + id: ChangelingDevourWindup + files: + - /Audio/Effects/Changeling/devour_windup.ogg + +- type: soundCollection + id: ChangelingTransformAttempt + files: + - /Audio/Effects/Changeling/changeling_transform.ogg diff --git a/Resources/Textures/Interface/Actions/changeling.rsi/devour.png b/Resources/Textures/Interface/Actions/changeling.rsi/devour.png new file mode 100644 index 0000000000..f5acc7dcee Binary files /dev/null and b/Resources/Textures/Interface/Actions/changeling.rsi/devour.png differ diff --git a/Resources/Textures/Interface/Actions/changeling.rsi/devour_on.png b/Resources/Textures/Interface/Actions/changeling.rsi/devour_on.png new file mode 100644 index 0000000000..65d63eb250 Binary files /dev/null and b/Resources/Textures/Interface/Actions/changeling.rsi/devour_on.png differ diff --git a/Resources/Textures/Interface/Actions/changeling.rsi/meta.json b/Resources/Textures/Interface/Actions/changeling.rsi/meta.json index babe776612..20c9ed1720 100644 --- a/Resources/Textures/Interface/Actions/changeling.rsi/meta.json +++ b/Resources/Textures/Interface/Actions/changeling.rsi/meta.json @@ -7,6 +7,15 @@ "y": 32 }, "states": [ + { + "name": "transform" + }, + { + "name": "devour" + }, + { + "name": "devour_on" + }, { "name": "armblade" } diff --git a/Resources/Textures/Interface/Actions/changeling.rsi/transform.png b/Resources/Textures/Interface/Actions/changeling.rsi/transform.png new file mode 100644 index 0000000000..6657790012 Binary files /dev/null and b/Resources/Textures/Interface/Actions/changeling.rsi/transform.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/dead.png b/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/dead.png new file mode 100644 index 0000000000..eb068364b5 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/dead.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/harvest.png b/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/harvest.png new file mode 100644 index 0000000000..d02cb9ce15 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/harvest.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/meta.json b/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/meta.json new file mode 100644 index 0000000000..79a691f5a6 --- /dev/null +++ b/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/meta.json @@ -0,0 +1,43 @@ +{ + "version": 1, + "license": "CC0-1.0", + "copyright": "Made by Thinbug for space station 14 :33", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "dead" + }, + { + "name": "harvest" + }, + { + "name": "produce" + }, + { + "name": "produce-inhand-left", + "directions": 4 + }, + { + "name": "produce-inhand-right", + "directions": 4 + }, + { + "name": "seed" + }, + { + "name": "stage-1" + }, + { + "name": "stage-2" + }, + { + "name": "stage-3" + }, + { + "name": "stage-4" + } + ] +} diff --git a/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/produce-inhand-left.png b/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/produce-inhand-left.png new file mode 100644 index 0000000000..44094d23e4 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/produce-inhand-left.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/produce-inhand-right.png b/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/produce-inhand-right.png new file mode 100644 index 0000000000..d5955e0196 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/produce-inhand-right.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/produce.png b/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/produce.png new file mode 100644 index 0000000000..53cd9e2f9a Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/produce.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/seed.png b/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/seed.png new file mode 100644 index 0000000000..c370b26cf5 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/seed.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/stage-1.png b/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/stage-1.png new file mode 100644 index 0000000000..283c44be21 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/stage-1.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/stage-2.png b/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/stage-2.png new file mode 100644 index 0000000000..5362406d00 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/stage-2.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/stage-3.png b/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/stage-3.png new file mode 100644 index 0000000000..c4947b625c Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/stage-3.png differ diff --git a/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/stage-4.png b/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/stage-4.png new file mode 100644 index 0000000000..9fa575c033 Binary files /dev/null and b/Resources/Textures/Objects/Specific/Hydroponics/bloonion.rsi/stage-4.png differ diff --git a/Resources/Textures/Objects/Weapons/Melee/baseball_bat.rsi/meta.json b/Resources/Textures/Objects/Weapons/Melee/baseball_bat.rsi/meta.json index 6eb88a0dff..d22ea9b629 100644 --- a/Resources/Textures/Objects/Weapons/Melee/baseball_bat.rsi/meta.json +++ b/Resources/Textures/Objects/Weapons/Melee/baseball_bat.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-NC-SA-3.0", - "copyright": "Taken from goonstation and modified by Swept at commit https://github.com/goonstation/goonstation/pull/3555/commits/b24eb6260647c0fcfe858268a26b6160bc50017a, wielded version by Easypoll", + "copyright": "Taken from goonstation and modified by Swept at commit https://github.com/goonstation/goonstation/pull/3555/commits/b24eb6260647c0fcfe858268a26b6160bc50017a, wielded version by Easypoll, vertical icon sprite by TiniestShark", "size": { "x": 32, "y": 32 @@ -10,6 +10,9 @@ { "name": "icon" }, + { + "name": "storage" + }, { "name": "inhand-left", "directions": 4 diff --git a/Resources/Textures/Objects/Weapons/Melee/baseball_bat.rsi/storage.png b/Resources/Textures/Objects/Weapons/Melee/baseball_bat.rsi/storage.png new file mode 100644 index 0000000000..eac5bb8b4d Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Melee/baseball_bat.rsi/storage.png differ diff --git a/Resources/Textures/Objects/Weapons/Melee/incomplete_bat.rsi/meta.json b/Resources/Textures/Objects/Weapons/Melee/incomplete_bat.rsi/meta.json index 5b19648740..d435ae235f 100644 --- a/Resources/Textures/Objects/Weapons/Melee/incomplete_bat.rsi/meta.json +++ b/Resources/Textures/Objects/Weapons/Melee/incomplete_bat.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-NC-SA-3.0", - "copyright": "Taken from goonstation and modified by Swept at commit https://github.com/goonstation/goonstation/pull/3555/commits/b24eb6260647c0fcfe858268a26b6160bc50017a, modified into incomplete version by Vermidia, in-hands by SeamLesss (GitHub)", + "copyright": "Taken from goonstation and modified by Swept at commit https://github.com/goonstation/goonstation/pull/3555/commits/b24eb6260647c0fcfe858268a26b6160bc50017a, modified into incomplete version by Vermidia, in-hands by SeamLesss (GitHub), vertical icon sprite by TiniestShark", "size": { "x": 32, "y": 32 @@ -10,6 +10,9 @@ { "name": "icon" }, + { + "name": "storage" + }, { "name": "inhand-left", "directions": 4 diff --git a/Resources/Textures/Objects/Weapons/Melee/incomplete_bat.rsi/storage.png b/Resources/Textures/Objects/Weapons/Melee/incomplete_bat.rsi/storage.png new file mode 100644 index 0000000000..6148f3943e Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Melee/incomplete_bat.rsi/storage.png differ