Merge remote-tracking branch 'upstream/master' into up/drinks-yml

# Conflicts:
#	Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_metamorphic.yml
This commit is contained in:
iaada
2025-08-08 15:17:10 -05:00
208 changed files with 3359 additions and 2492 deletions

View File

@@ -76,7 +76,7 @@ public sealed partial class ObjectsTab : Control
switch (selection)
{
case ObjectsTabSelection.Stations:
entities.AddRange(_entityManager.EntitySysManager.GetEntitySystem<StationSystem>().Stations);
entities.AddRange(_entityManager.EntitySysManager.GetEntitySystem<StationSystem>().GetStationNames());
break;
case ObjectsTabSelection.Grids:
{

View File

@@ -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<BodyComponent>(ent, out var body) &&
body.Prototype is not null &&
ent.Comp.SpeciesSprites.TryGetValue(body.Prototype.Value, out var speciesSprite))
if (TryComp<HumanoidAppearanceComponent>(ent, out var humanoidAppearance) &&
ent.Comp.SpeciesSprites.TryGetValue(humanoidAppearance.Species, out var speciesSprite))
{
_sprite.LayerSetSprite((ent.Owner, sprite), index, speciesSprite);
}

View File

@@ -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<BodyComponent> _bodyQuery;
private EntityQuery<MobStateComponent> _mobStateQuery;
public override void Initialize()
{
base.Initialize();
_bodyQuery = GetEntityQuery<BodyComponent>();
_mobStateQuery = GetEntityQuery<MobStateComponent>();
SubscribeNetworkEvent<PlayBoxEffectMessage>(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);

View File

@@ -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<ChangelingTransformMenu>();
_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));
}
}

View File

@@ -0,0 +1,8 @@
<ui:RadialMenu
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
CloseButtonStyleClass="RadialMenuCloseButton"
VerticalExpand="True"
HorizontalExpand="True">
<ui:RadialContainer Name="Main">
</ui:RadialContainer>
</ui:RadialMenu>

View File

@@ -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<NetEntity>? 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<MetaDataComponent>(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;

View File

@@ -0,0 +1,5 @@
using Content.Shared.Cloning;
namespace Content.Client.Cloning;
public sealed partial class CloningSystem : SharedCloningSystem;

View File

@@ -0,0 +1,5 @@
using Content.Shared.Forensics.Systems;
namespace Content.Client.Forensics.Systems;
public sealed class ForensicsSystem : SharedForensicsSystem;

View File

@@ -115,6 +115,13 @@ namespace Content.Client.Inventory
OnLinkInventorySlots?.Invoke(uid, component);
}
protected override void OnInit(Entity<InventoryComponent> ent, ref ComponentInit args)
{
base.OnInit(ent, ref args);
_clothingVisualsSystem.InitClothing(ent.Owner, ent.Comp);
}
public override void Shutdown()
{
CommandBinds.Unregister<ClientInventorySystem>();
@@ -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);
}

View File

@@ -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;

View File

@@ -2,34 +2,5 @@
namespace Content.Client.Station;
/// <summary>
/// This handles letting the client know stations are a thing. Only really used by an admin menu.
/// </summary>
public sealed partial class StationSystem : SharedStationSystem
{
private readonly List<(string Name, NetEntity Entity)> _stations = new();
/// <summary>
/// All stations that currently exist.
/// </summary>
/// <remarks>
/// I'd have this just invoke an entity query, but we're on the client and the client barely knows about stations.
/// </remarks>
// TODO: Stations have a global PVS override now, this can probably be changed into a query.
public IReadOnlyList<(string Name, NetEntity Entity)> Stations => _stations;
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeNetworkEvent<StationsUpdatedEvent>(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);
}
}
/// <inheritdoc/>
public sealed partial class StationSystem : SharedStationSystem;

View File

@@ -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();

View File

@@ -5,7 +5,7 @@
<ScrollContainer HorizontalExpand="True"
VerticalExpand="True"
SizeFlagsStretchRatio="6">
<GridContainer Name="Values"/>
<GridContainer Name="Values" HSeparationOverride="0" VSeparationOverride="15"/>
</ScrollContainer>
</BoxContainer>
</DefaultWindow>

View File

@@ -21,17 +21,20 @@ public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayS
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
private MenuButton? EmotesButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.EmotesButton;
private SimpleRadialMenu? _menu;
private static readonly Dictionary<EmoteCategory, (string Tooltip, SpriteSpecifier Sprite)> EmoteGroupingInfo
= new Dictionary<EmoteCategory, (string Tooltip, SpriteSpecifier Sprite)>
{
[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<EmoteCategory, (string Tooltip, SpriteSpecifier Sprite)> 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<GameplayS
var whitelistSystem = EntitySystemManager.GetEntitySystem<EntityWhitelistSystem>();
var player = _playerManager.LocalSession?.AttachedEntity;
Dictionary<EmoteCategory, List<RadialMenuOption>> emotesByCategory = new();
Dictionary<EmoteCategory, List<RadialMenuOption>> emotesByCategory = new();
foreach (var emote in emotePrototypes)
{
if(emote.Category == EmoteCategory.Invalid)

View File

@@ -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<IEntityManager>();
var mapManager = server.ResolveDependency<IMapManager>();
var host = server.ResolveDependency<IServerConsoleHost>();
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)}");
});

View File

@@ -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;

View File

@@ -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<StartingGearPrototype> PirateGearId = "PirateGear";
private static readonly EntProtoId DefaultChangelingRule = "Changeling";
private static readonly EntProtoId ParadoxCloneRuleId = "ParadoxCloneSpawn";
private static readonly ProtoId<StartingGearPrototype> PirateGearId = "PirateGear";
// All antag verbs have names so invokeverb works.
private void AddAntagVerbs(GetVerbsEvent<Verb> args)
@@ -58,7 +58,7 @@ public sealed partial class AdminVerbSystem
_antag.ForceMakeAntag<TraitorRuleComponent>(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<ChangelingRuleComponent>(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()
{

View File

@@ -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;

View File

@@ -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<StationDataComponent>(station, out var data) ||
_station.GetLargestGrid(data) is not { } grid)
_station.GetLargestGrid(station) is not { } grid)
{
if (xform.GridUid == null)
return;

View File

@@ -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<BrainComponent, OrganAddedToBodyEvent>((uid, _, args) => HandleMind(args.Body, uid));
SubscribeLocalEvent<BrainComponent, OrganRemovedFromBodyEvent>((uid, _, args) => HandleMind(uid, args.OldBody));
SubscribeLocalEvent<BrainComponent, PointAttemptEvent>(OnPointAttempt);
}
SubscribeLocalEvent<BrainComponent, OrganAddedToBodyEvent>((uid, _, args) => HandleMind(args.Body, uid));
SubscribeLocalEvent<BrainComponent, OrganRemovedFromBodyEvent>((uid, _, args) => HandleMind(uid, args.OldBody));
SubscribeLocalEvent<BrainComponent, PointAttemptEvent>(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<MindContainerComponent>(newEntity);
EnsureComp<MindContainerComponent>(oldEntity);
EnsureComp<MindContainerComponent>(newEntity);
EnsureComp<MindContainerComponent>(oldEntity);
var ghostOnMove = EnsureComp<GhostOnMoveComponent>(newEntity);
ghostOnMove.MustBeDead = HasComp<MobStateComponent>(newEntity); // Don't ghost living players out of their bodies.
var ghostOnMove = EnsureComp<GhostOnMoveComponent>(newEntity);
if (HasComp<BodyComponent>(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<BrainComponent> ent, ref PointAttemptEvent args)
{
args.Cancel();
}
private void OnPointAttempt(Entity<BrainComponent> ent, ref PointAttemptEvent args)
{
args.Cancel();
}
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -90,19 +90,22 @@ public sealed class PricingSystem : EntitySystem
if (args.Handled)
return;
if (!TryComp<BodyComponent>(uid, out var body) || !TryComp<MobStateComponent>(uid, out var state))
if (!TryComp<MobStateComponent>(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<BodyComponent>(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);
}

View File

@@ -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;
/// <summary>
/// This system is used to notify specific players of the occurance of predefined events.
/// </summary>
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<Source, next allowed TOA>
private readonly Dictionary<(EntityUid, ProtoId<ChatNotificationPrototype>), Dictionary<EntityUid, TimeSpan>> _chatNotificationsBySource = new();
// Local cache for rate limiting chat notifications by type
// (Recipient, ChatNotification) -> next allowed TOA
private readonly Dictionary<(EntityUid, ProtoId<ChatNotificationPrototype>), TimeSpan> _chatNotificationsByType = new();
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ActorComponent, ChatNotificationEvent>(OnChatNotification);
_sawmill = _logManager.GetSawmill("chatnotification");
}
/// <summary>
/// Triggered when the specified player recieves a chat notification event.
/// </summary>
/// <param name="ent">The player receiving the chat notification.</param>
/// <param name="args">The chat notification event</param>
public void OnChatNotification(Entity<ActorComponent> 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);
}
}

View File

@@ -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;

View File

@@ -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;
/// <summary>
/// The part of item cloning responsible for copying over important components.
/// This is used for <see cref="CopyItem"/>.
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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<StackComponent, CloningItemEvent>(OnCloneStack);
SubscribeLocalEvent<LabelComponent, CloningItemEvent>(OnCloneLabel);
SubscribeLocalEvent<PaperComponent, CloningItemEvent>(OnClonePaper);
SubscribeLocalEvent<ForensicsComponent, CloningItemEvent>(OnCloneForensics);
SubscribeLocalEvent<StoreComponent, CloningItemEvent>(OnCloneStore);
// These are used for <see cref="CopyItem"/>.
// 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<StackComponent, CloningItemEvent>(OnCloneItemStack);
SubscribeLocalEvent<LabelComponent, CloningItemEvent>(OnCloneItemLabel);
SubscribeLocalEvent<PaperComponent, CloningItemEvent>(OnCloneItemPaper);
SubscribeLocalEvent<ForensicsComponent, CloningItemEvent>(OnCloneItemForensics);
SubscribeLocalEvent<StoreComponent, CloningItemEvent>(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<VocalComponent, CloningEvent>(OnCloneVocal);
SubscribeLocalEvent<StorageComponent, CloningEvent>(OnCloneStorage);
SubscribeLocalEvent<InventoryComponent, CloningEvent>(OnCloneInventory);
SubscribeLocalEvent<MovementSpeedModifierComponent, CloningEvent>(OnCloneInventory);
}
private void OnCloneStack(Entity<StackComponent> ent, ref CloningItemEvent args)
private void OnCloneItemStack(Entity<StackComponent> ent, ref CloningItemEvent args)
{
// if the clone is a stack as well, adjust the count of the copy
if (TryComp<StackComponent>(args.CloneUid, out var cloneStackComp))
_stack.SetCount(args.CloneUid, ent.Comp.Count, cloneStackComp);
}
private void OnCloneLabel(Entity<LabelComponent> ent, ref CloningItemEvent args)
private void OnCloneItemLabel(Entity<LabelComponent> ent, ref CloningItemEvent args)
{
// copy the label
_label.Label(args.CloneUid, ent.Comp.CurrentLabel);
}
private void OnClonePaper(Entity<PaperComponent> ent, ref CloningItemEvent args)
private void OnCloneItemPaper(Entity<PaperComponent> ent, ref CloningItemEvent args)
{
// copy the text and any stamps
if (TryComp<PaperComponent>(args.CloneUid, out var clonePaperComp))
@@ -63,13 +79,13 @@ public sealed partial class CloningSystem : EntitySystem
}
}
private void OnCloneForensics(Entity<ForensicsComponent> ent, ref CloningItemEvent args)
private void OnCloneItemForensics(Entity<ForensicsComponent> ent, ref CloningItemEvent args)
{
// copy any forensics to the cloned item
_forensics.CopyForensicsFrom(ent.Comp, args.CloneUid);
}
private void OnCloneStore(Entity<StoreComponent> ent, ref CloningItemEvent args)
private void OnCloneItemStore(Entity<StoreComponent> 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<VocalComponent> 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<StorageComponent> 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<InventoryComponent> 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<MovementSpeedModifierComponent> ent, ref CloningEvent args)
{
if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name))
return;
_movementSpeedModifier.CopyComponent(ent.AsNullable(), args.CloneUid);
}
}

View File

@@ -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.
/// </summary>
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;
}
/// <summary>
/// Copy components from one entity to another based on a CloningSettingsPrototype.
/// </summary>
/// <param name="original">The orignal Entity to clone components from.</param>
/// <param name="clone">The target Entity to clone components to.</param>
/// <param name="settings">The clone settings prototype containing the list of components to clone.</param>
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);

View File

@@ -1,4 +1,4 @@
using Content.Server.Station.Components;
using Content.Shared.Station.Components;
using Robust.Shared.Console;
namespace Content.Server.Commands;

View File

@@ -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;

View File

@@ -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
/// <summary>
/// Emote sounds prototype id for body emotes.
/// </summary>
[DataField("soundsId", customTypeSerializer: typeof(PrototypeIdSerializer<EmoteSoundsPrototype>))]
public string? SoundsId;
/// <summary>
/// Loaded emote sounds prototype used for body emotes.
/// </summary>
public EmoteSoundsPrototype? Sounds;
[DataField]
public ProtoId<EmoteSoundsPrototype>? SoundsId;
}

View File

@@ -14,15 +14,8 @@ public sealed class BodyEmotesSystem : EntitySystem
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BodyEmotesComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<BodyEmotesComponent, EmoteEvent>(OnEmote);
}
private void OnStartup(EntityUid uid, BodyEmotesComponent component, ComponentStartup args)
{
if (component.SoundsId == null)
return;
_proto.TryIndex(component.SoundsId, out component.Sounds);
SubscribeLocalEvent<BodyEmotesComponent, EmoteEvent>(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);
}
}

View File

@@ -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
/// <summary>
/// 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.
/// </summary>
public void RandomizeDNA(Entity<DnaComponent?> ent)
public override void RandomizeDNA(Entity<DnaComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return;
@@ -334,11 +330,7 @@ namespace Content.Server.Forensics
RaiseLocalEvent(ent.Owner, ref ev);
}
/// <summary>
/// Give the entity a new, random fingerprint string.
/// Does nothing if it does not have the FingerprintComponent.
/// </summary>
public void RandomizeFingerprint(Entity<FingerprintComponent?> ent)
public override void RandomizeFingerprint(Entity<FingerprintComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return;

View File

@@ -0,0 +1,23 @@
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Roles;
using Content.Shared.Changeling;
namespace Content.Server.GameTicking.Rules;
/// <summary>
/// Game rule system for Changelings
/// </summary>
public sealed class ChangelingRuleSystem : GameRuleSystem<ChangelingRuleComponent>
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ChangelingRoleComponent, GetBriefingEvent>(OnGetBriefing);
}
private void OnGetBriefing(Entity<ChangelingRoleComponent> ent, ref GetBriefingEvent args)
{
args.Append(Loc.GetString("changeling-briefing"));
}
}

View File

@@ -0,0 +1,7 @@
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
/// Gamerule component for handling a changeling antagonist.
/// </summary>
[RegisterComponent]
public sealed partial class ChangelingRuleComponent : Component;

View File

@@ -14,7 +14,7 @@ public sealed partial class ParadoxCloneRuleComponent : Component
/// Cloning settings to be used.
/// </summary>
[DataField]
public ProtoId<CloningSettingsPrototype> Settings = "Antag";
public ProtoId<CloningSettingsPrototype> Settings = "ParadoxCloningSettings";
/// <summary>
/// Visual effect spawned when gibbing at round end.

View File

@@ -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<DragonRuleComponent>
var dragonXform = Transform(dragon);
var station = _station.GetStationInMap(dragonXform.MapID);
EntityUid? stationGrid = null;
if (TryComp<StationDataComponent>(station, out var stationData))
stationGrid = _station.GetLargestGrid(stationData);
if (_station.GetStationInMap(dragonXform.MapID) is { } station)
stationGrid = _station.GetLargestGrid(station);
if (stationGrid is not null)
{

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -58,7 +58,7 @@ public abstract class BaseEntityReplaceVariationPassSystem<TEntComp, TGameRuleCo
Transform(newEnt).LocalRotation = rot;
}
Log.Debug($"Entity replacement took {stopwatch.Elapsed} with {Stations.GetTileCount(args.Station)} tiles");
Log.Debug($"Entity replacement took {stopwatch.Elapsed} with {Stations.GetTileCount(args.Station.AsNullable())} tiles");
}
private void QueueReplace(Entity<TransformComponent> ent, List<EntitySpawnEntry> replacements)

View File

@@ -9,7 +9,7 @@ public sealed class EntitySpawnVariationPassSystem : VariationPassSystem<EntityS
{
protected override void ApplyVariation(Entity<EntitySpawnVariationPassComponent> 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);

View File

@@ -15,7 +15,7 @@ public sealed class PuddleMessVariationPassSystem : VariationPassSystem<PuddleMe
protected override void ApplyVariation(Entity<PuddleMessVariationPassComponent> 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;

View File

@@ -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<ZombieRuleComponent>
{
foreach (var station in _station.GetStationsSet())
{
if (TryComp<StationDataComponent>(station, out var data) && _station.GetLargestGrid(data) is { } grid)
if (_station.GetLargestGrid(station) is { } grid)
stationGrids.Add(grid);
}
}

View File

@@ -1,20 +0,0 @@
using Content.Server.Implants;
using Robust.Shared.Audio;
namespace Content.Server.Implants.Components;
/// <summary>
/// Randomly teleports entity when triggered.
/// </summary>
[RegisterComponent]
public sealed partial class ScramImplantComponent : Component
{
/// <summary>
/// Up to how far to teleport the user
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float TeleportRadius = 100f;
[DataField, ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier TeleportSound = new SoundPathSpecifier("/Audio/Effects/teleport_arrival.ogg");
}

View File

@@ -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<PhysicsComponent> _physicsQuery;
private HashSet<Entity<MapGridComponent>> _targetGrids = [];
public override void Initialize()
{
base.Initialize();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
SubscribeLocalEvent<SubdermalImplantComponent, UseFreedomImplantEvent>(OnFreedomImplant);
SubscribeLocalEvent<StoreComponent, ImplantRelayEvent<AfterInteractUsingEvent>>(OnStoreRelay);
SubscribeLocalEvent<SubdermalImplantComponent, ActivateImplantEvent>(OnActivateImplantEvent);
SubscribeLocalEvent<SubdermalImplantComponent, UseScramImplantEvent>(OnScramImplant);
SubscribeLocalEvent<SubdermalImplantComponent, UseDnaScramblerImplantEvent>(OnDnaScramblerImplant);
}
private void OnStoreRelay(EntityUid uid, StoreComponent store, ImplantRelayEvent<AfterInteractUsingEvent> 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<CuffableComponent>(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<ScramImplantComponent>(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<PullableComponent>(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<PullerComponent>(ent, out var puller) && TryComp<PullableComponent>(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<MapGridComponent>? 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<MapGridComponent>(userXform.GridUid, out var gridComp))
{
var userGrid = new Entity<MapGridComponent>(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<Vector2i>();
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<HumanoidAppearanceComponent>(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<DetailExaminableComponent>(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);
}
}

View File

@@ -117,19 +117,18 @@ public sealed class SharpSystem : EntitySystem
popupEnt = Spawn(proto, coords.Offset(_robustRandom.NextVector2(0.25f)));
}
var hasBody = TryComp<BodyComponent>(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<MobStateComponent>(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;

View File

@@ -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;

View File

@@ -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;
/// <summary>
/// A mind filter that removes minds if you have an objective targeting them matching a blacklist.
/// </summary>
/// <remarks>
/// Used to prevent assigning multiple kill objectives for the same person.
/// </remarks>
public sealed partial class TargetObjectiveMindFilter : MindFilter
{
/// <summary>
/// A blacklist to check objectives against, for removing a mind.
/// If null then any objective targeting it will remove minds.
/// </summary>
[DataField]
public EntityWhitelist? Blacklist;
protected override bool ShouldRemove(Entity<MindComponent> mind, EntityUid? excluded, IEntityManager entMan, SharedMindSystem mindSys)
{
// ignore this filter if there is no user to check
if (!entMan.TryGetComponent<MindComponent>(excluded, out var excludedMind))
return false;
var whitelistSys = entMan.System<EntityWhitelistSystem>();
foreach (var objective in excludedMind.Objectives)
{
// if the player has an objective targeting this mind
if (entMan.TryGetComponent<TargetObjectiveComponent>(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;
}
}

View File

@@ -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<BodyComponent>(ent))
if (!hasMob && HasComp<MobStateComponent>(ent))
hasMob = true;
if (HasComp<ActorComponent>(ent))

View File

@@ -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;

View File

@@ -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;

View File

@@ -1,8 +0,0 @@
namespace Content.Server.Objectives.Components;
/// <summary>
/// Sets the target for <see cref="TargetObjectiveComponent"/> to a random head.
/// If there are no heads it will fallback to any person.
/// </summary>
[RegisterComponent]
public sealed partial class PickRandomHeadComponent : Component;

View File

@@ -1,7 +1,26 @@
using Content.Server.Objectives.Systems;
using Content.Shared.Mind.Filters;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Sets the target for <see cref="TargetObjectiveComponent"/> to a random person.
/// Sets the target for <see cref="TargetObjectiveComponent"/> to a random person from a pool and filters.
/// </summary>
[RegisterComponent]
public sealed partial class PickRandomPersonComponent : Component;
/// <remarks>
/// 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.
/// </remarks>
[RegisterComponent, Access(typeof(PickObjectiveTargetSystem))]
public sealed partial class PickRandomPersonComponent : Component
{
/// <summary>
/// A pool to pick potential targets from.
/// </summary>
[DataField]
public IMindPool Pool = new AliveHumansPool();
/// <summary>
/// Filters to apply to <see cref="Pool"/>.
/// </summary>
[DataField]
public List<MindFilter> Filters = new();
}

View File

@@ -1,7 +0,0 @@
namespace Content.Server.Objectives.Components;
/// <summary>
/// Sets the target for <see cref="KeepAliveConditionComponent"/> to a random traitor.
/// </summary>
[RegisterComponent]
public sealed partial class RandomTraitorAliveComponent : Component;

View File

@@ -1,7 +0,0 @@
namespace Content.Server.Objectives.Components;
/// <summary>
/// Sets the target for <see cref="HelpProgressConditionComponent"/> to a random traitor.
/// </summary>
[RegisterComponent]
public sealed partial class RandomTraitorProgressComponent : Component;

View File

@@ -25,10 +25,6 @@ public sealed class PickObjectiveTargetSystem : EntitySystem
SubscribeLocalEvent<PickSpecificPersonComponent, ObjectiveAssignedEvent>(OnSpecificPersonAssigned);
SubscribeLocalEvent<PickRandomPersonComponent, ObjectiveAssignedEvent>(OnRandomPersonAssigned);
SubscribeLocalEvent<PickRandomHeadComponent, ObjectiveAssignedEvent>(OnRandomHeadAssigned);
SubscribeLocalEvent<RandomTraitorProgressComponent, ObjectiveAssignedEvent>(OnRandomTraitorProgressAssigned);
SubscribeLocalEvent<RandomTraitorAliveComponent, ObjectiveAssignedEvent>(OnRandomTraitorAliveAssigned);
}
private void OnSpecificPersonAssigned(Entity<PickSpecificPersonComponent> ent, ref ObjectiveAssignedEvent args)
@@ -63,7 +59,7 @@ public sealed class PickObjectiveTargetSystem : EntitySystem
private void OnRandomPersonAssigned(Entity<PickRandomPersonComponent> ent, ref ObjectiveAssignedEvent args)
{
// invalid objective prototype
if (!TryComp<TargetObjectiveComponent>(ent.Owner, out var target))
if (!TryComp<TargetObjectiveComponent>(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<KillPersonConditionComponent>(objective) && TryComp<TargetObjectiveComponent>(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<PickRandomHeadComponent> ent, ref ObjectiveAssignedEvent args)
{
// invalid prototype
if (!TryComp<TargetObjectiveComponent>(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<Entity<MindComponent>>();
foreach (var person in allHumans)
{
if (TryComp<MindComponent>(person, out var mind) && mind.OwnedEntity is { } owned && HasComp<CommandStaffComponent>(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<RandomTraitorProgressComponent> ent, ref ObjectiveAssignedEvent args)
{
// invalid prototype
if (!TryComp<TargetObjectiveComponent>(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<ObjectivesComponent>(traitor) or something when objectives are moved out of mind
if (!TryComp<MindComponent>(traitor.Id, out var mind))
continue;
foreach (var objective in mind.Objectives)
{
if (HasComp<HelpProgressConditionComponent>(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<RandomTraitorAliveComponent>(objective) || HasComp<RandomTraitorProgressComponent>(objective))
{
if (TryComp<TargetObjectiveComponent>(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<RandomTraitorAliveComponent> ent, ref ObjectiveAssignedEvent args)
{
// invalid prototype
if (!TryComp<TargetObjectiveComponent>(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<RandomTraitorAliveComponent>(objective) || HasComp<RandomTraitorProgressComponent>(objective))
{
if (TryComp<TargetObjectiveComponent>(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);
}
}

View File

@@ -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;
}
}

View File

@@ -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
/// </summary>
public EntityUid? GetStation()
{
AllEntityQuery<StationEmergencyShuttleComponent, StationDataComponent>().MoveNext(out _, out _, out var data);
AllEntityQuery<StationEmergencyShuttleComponent, StationDataComponent>().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;
}

View File

@@ -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;

View File

@@ -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<StationDataComponent>(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<StationDataComponent>(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);

View File

@@ -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<StationDataComponent>(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<StationDataComponent>(stationUid));
var targetGrid = _station.GetLargestGrid(stationUid);
// UHH GOOD LUCK
if (targetGrid == null)

View File

@@ -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<StationDataComponent>(uid, out var data))
{
return;
}
var targetGrid = _station.GetLargestGrid(data);
var targetGrid = _station.GetLargestGrid(uid);
if (targetGrid == null)
return;

View File

@@ -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<Entity<StationAiCoreComponent>> _ais = new();
private readonly HashSet<Entity<StationAiCoreComponent>> _stationAiCores = new();
private readonly ProtoId<ChatNotificationPrototype> _turretIsAttackingChatNotificationPrototype = "TurretIsAttacking";
private readonly ProtoId<ChatNotificationPrototype> _aiWireSnippedChatNotificationPrototype = "AiWireSnipped";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ExpandICChatRecipientsEvent>(OnExpandICChatRecipients);
SubscribeLocalEvent<StationAiTurretComponent, AmmoShotEvent>(OnAmmoShot);
}
private void OnExpandICChatRecipients(ExpandICChatRecipientsEvent ev)
@@ -61,15 +60,33 @@ public sealed class StationAiSystem : SharedStationAiSystem
}
}
private void OnAmmoShot(Entity<StationAiTurretComponent> 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<DeviceNetworkComponent>(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<StationAiVisionComponent> 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<ActorComponent>(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<EntityUid> GetStationAIs(EntityUid gridUid)
{
_stationAiCores.Clear();
_lookup.GetChildEntities(gridUid, _stationAiCores);
var hashSet = new HashSet<EntityUid>();
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;
}
}

View File

@@ -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;

View File

@@ -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<VocalComponent, ScreamActionEvent>(OnScreamAction);
}
/// <summary>
/// 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.
/// <summary>
public void CopyComponent(Entity<VocalComponent?> source, EntityUid target)
{
if (!Resolve(source, ref source.Comp))
return;
var targetComp = EnsureComp<VocalComponent>(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

View File

@@ -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<StationSystem>();
return _station.GetLargestGrid(Comp<StationDataComponent>(input));
return _station.GetLargestGrid(input);
}
[CommandImplementation("largestgrid")]

View File

@@ -1,4 +1,4 @@
using Content.Server.Station.Components;
using Content.Shared.Station.Components;
namespace Content.Server.Station.Events;

View File

@@ -20,12 +20,10 @@ public sealed partial class StationBiomeSystem : EntitySystem
private void OnStationPostInit(Entity<StationBiomeComponent> 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);

View File

@@ -1,4 +1,5 @@
using Content.Server.Station.Components;
using Content.Shared.Station.Components;
namespace Content.Server.Station.Systems;

View File

@@ -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<MapGridComponent> _gridQuery;
private EntityQuery<TransformComponent> _xformQuery;
private ValueList<MapId> _mapIds = new();
private ValueList<(Box2Rotated Bounds, MapId MapId)> _gridBounds = new();
private ValueList<MapId> _mapIds;
private ValueList<(Box2Rotated Bounds, MapId MapId)> _gridBounds;
/// <inheritdoc/>
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
/// <summary>
/// Gets the largest member grid from a station.
/// </summary>
public EntityUid? GetLargestGrid(StationDataComponent component)
{
EntityUid? largestGrid = null;
Box2 largestBounds = new Box2();
foreach (var gridUid in component.Grids)
{
if (!TryComp<MapGridComponent>(gridUid, out var grid) ||
grid.LocalAABB.Size.LengthSquared() < largestBounds.Size.LengthSquared())
continue;
largestBounds = grid.LocalAABB;
largestGrid = gridUid;
}
return largestGrid;
}
/// <summary>
/// Returns the total number of tiles contained in the station's grids.
/// </summary>
public int GetTileCount(StationDataComponent component)
{
var count = 0;
foreach (var gridUid in component.Grids)
{
if (!TryComp<MapGridComponent>(gridUid, out var grid))
continue;
count += _map.GetAllTiles(gridUid, grid).Count();
}
return count;
}
/// <summary>
/// Tries to retrieve a filter for everything in the station the source is on.
/// </summary>
@@ -384,6 +342,7 @@ public sealed partial class StationSystem : SharedStationSystem
var stationMember = EnsureComp<StationMemberComponent>(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<StationMemberComponent>(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);
}
/// <summary>
/// Gets the station that "owns" the given entity (essentially, the station the grid it's on is attached to)
/// </summary>
/// <param name="entity">Entity to find the owner of.</param>
/// <param name="xform">Resolve pattern, transform of the entity.</param>
/// <returns>The owning station, if any.</returns>
/// <remarks>
/// This does not remember what station an entity started on, it simply checks where it is currently located.
/// </remarks>
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<StationDataComponent>(entity, out _))
{
// We are the station, just return ourselves.
return entity;
}
if (TryComp<MapGridComponent>(entity, out _))
{
// We are the station, just check ourselves.
return CompOrNull<StationMemberComponent>(entity)?.Station;
}
if (xform.GridUid == EntityUid.Invalid)
{
Log.Debug("Unable to get owning station - GridUid invalid.");
return null;
}
return CompOrNull<StationMemberComponent>(xform.GridUid)?.Station;
}
public List<EntityUid> GetStations()
{
var stations = new List<EntityUid>();
var query = EntityQueryEnumerator<StationDataComponent>();
while (query.MoveNext(out var uid, out _))
{
stations.Add(uid);
}
return stations;
}
public HashSet<EntityUid> GetStationsSet()
{
var stations = new HashSet<EntityUid>();
var query = EntityQueryEnumerator<StationDataComponent>();
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;
}
/// <summary>
/// Returns the first station that has a grid in a certain map.
/// If the map has no stations, null is returned instead.
/// </summary>
/// <remarks>
/// If there are multiple stations on a map it is probably arbitrary which one is returned.
/// </remarks>
public EntityUid? GetStationInMap(MapId map)
{
var query = EntityQueryEnumerator<StationDataComponent>();
while (query.MoveNext(out var uid, out var data))
{
foreach (var gridUid in data.Grids)
{
if (Transform(gridUid).MapID == map)
{
return uid;
}
}
}
return null;
}
}
/// <summary>

View File

@@ -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<AnomalySpawnRuleCompon
if (!TryComp<StationDataComponent>(chosenStation, out var stationData))
return;
var grid = StationSystem.GetLargestGrid(stationData);
var grid = StationSystem.GetLargestGrid((chosenStation.Value, stationData));
if (grid is null)
return;

View File

@@ -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;

View File

@@ -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<MeteorSwarmComponent>
return;
var station = RobustRandom.Pick(_station.GetStations());
if (_station.GetLargestGrid(Comp<StationDataComponent>(station)) is not { } grid)
if (_station.GetLargestGrid(station) is not { } grid)
return;
var mapId = Transform(grid).MapID;

View File

@@ -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<SpaceSpawnRuleComponent>
return;
}
var stationData = Comp<StationDataComponent>(station.Value);
// find a station grid
var gridUid = StationSystem.GetLargestGrid(stationData);
var gridUid = StationSystem.GetLargestGrid(station.Value);
if (gridUid == null || !TryComp<MapGridComponent>(gridUid, out var grid))
{
Sawmill.Warning("Chosen station has no grids, cannot pick location for {ToPrettyString(uid):rule}");

View File

@@ -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<DamageTypePrototype> StructuralDamageType = "Structural";
private StatValuesEuiMessage GetMelee()
{
var values = new List<string[]>();
var meleeName = _entManager.ComponentFactory.GetComponentName<MeleeWeaponComponent>();
var increaseDamageName = _entManager.ComponentFactory.GetComponentName<IncreaseDamageOnWieldComponent>();
foreach (var proto in _proto.EnumeratePrototypes<EntityPrototype>())
{
@@ -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<string>()
Title = Loc.GetString("stat-melee-values"),
Headers = new List<string>
{
"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,
};

View File

@@ -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<WaggingComponent, ComponentShutdown>(OnWaggingShutdown);
SubscribeLocalEvent<WaggingComponent, ToggleActionEvent>(OnWaggingToggle);
SubscribeLocalEvent<WaggingComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<WaggingComponent, CloningEvent>(OnCloning);
}
private void OnCloning(Entity<WaggingComponent> ent, ref CloningEvent args)
{
if (!args.Settings.EventComponents.Contains(Factory.GetRegistration(ent.Comp.GetType()).Name))
return;
EnsureComp<WaggingComponent>(args.CloneUid);
}
private void OnWaggingMapInit(EntityUid uid, WaggingComponent component, MapInitEvent args)

View File

@@ -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
/// </summary>
[DataField, AutoNetworkedField]
public Dictionary<ProtoId<BodyPrototype>, SpriteSpecifier> SpeciesSprites = new();
public Dictionary<ProtoId<SpeciesPrototype>, SpriteSpecifier> SpeciesSprites = new();
/// <summary>
/// The key of the entity layer into which the sprite will be inserted

View File

@@ -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<TransformComponent?> entity)

View File

@@ -0,0 +1,35 @@
using Content.Shared.Cloning;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Changeling;
/// <summary>
/// The storage component for Changelings, it handles the link between a changeling and its consumed identities
/// that exist on a paused map.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ChangelingIdentityComponent : Component
{
/// <summary>
/// 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.
/// </summary>
// 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<EntityUid> ConsumedIdentities = new();
/// <summary>
/// The currently assumed identity.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? CurrentIdentity;
/// <summary>
/// The cloning settings passed to the CloningSystem, contains a list of all components to copy or have handled by their
/// respective systems.
/// </summary>
public ProtoId<CloningSettingsPrototype> IdentityCloningSettings = "ChangelingCloningSettings";
public override bool SendOnlyToOwner => true;
}

View File

@@ -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<ChangelingIdentityComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ChangelingIdentityComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<ChangelingIdentityComponent, MindAddedMessage>(OnMindAdded);
SubscribeLocalEvent<ChangelingIdentityComponent, MindRemovedMessage>(OnMindRemoved);
SubscribeLocalEvent<ChangelingStoredIdentityComponent, ComponentRemove>(OnStoredRemove);
}
private void OnMindAdded(Entity<ChangelingIdentityComponent> ent, ref MindAddedMessage args)
{
if (!TryComp<ActorComponent>(args.Container.Owner, out var actor))
return;
HandOverPvsOverride(actor.PlayerSession, ent.Comp);
}
private void OnMindRemoved(Entity<ChangelingIdentityComponent> ent, ref MindRemovedMessage args)
{
CleanupPvsOverride(ent, args.Container.Owner);
}
private void OnMapInit(Entity<ChangelingIdentityComponent> 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<ChangelingIdentityComponent> ent, ref ComponentShutdown args)
{
CleanupPvsOverride(ent, ent.Owner);
CleanupChangelingNullspaceIdentities(ent);
}
private void OnStoredRemove(Entity<ChangelingStoredIdentityComponent> ent, ref ComponentRemove args)
{
// The last stored identity is being deleted, we can clean up the map.
if (_net.IsServer && PausedMapId != null && Count<ChangelingStoredIdentityComponent>() <= 1)
_map.QueueDeleteMap(PausedMapId.Value);
}
/// <summary>
/// Cleanup all nullspaced Identities when the changeling no longer exists
/// </summary>
/// <param name="ent">the changeling</param>
public void CleanupChangelingNullspaceIdentities(Entity<ChangelingIdentityComponent> ent)
{
if (_net.IsClient)
return;
foreach (var consumedIdentity in ent.Comp.ConsumedIdentities)
{
QueueDel(consumedIdentity);
}
}
/// <summary>
/// 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
/// </summary>
/// <param name="ent">the Changeling</param>
/// <param name="target">the targets uid</param>
public EntityUid? CloneToPausedMap(Entity<ChangelingIdentityComponent> ent, EntityUid target)
{
// Don't create client side duplicate clones or a clientside map.
if (_net.IsClient)
return null;
if (!TryComp<HumanoidAppearanceComponent>(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<ChangelingStoredIdentityComponent>(mob);
storedIdentity.OriginalEntity = target; // TODO: network this once we have WeakEntityReference or the autonetworking source gen is fixed
if (TryComp<ActorComponent>(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;
}
/// <summary>
/// Simple helper to add a PVS override to a Nullspace Identity
/// </summary>
/// <param name="uid"></param>
/// <param name="target"></param>
private void HandlePvsOverride(EntityUid uid, EntityUid target)
{
if (!TryComp<ActorComponent>(uid, out var actor))
return;
_pvsOverrideSystem.AddSessionOverride(target, actor.PlayerSession);
}
/// <summary>
/// Cleanup all Pvs Overrides for the owner of the ChangelingIdentity
/// </summary>
/// <param name="ent">the Changeling itself</param>
/// <param name="entityUid">Who specifically to cleanup from, usually just the same owner, but in the case of a mindswap we want to clean up the victim</param>
private void CleanupPvsOverride(Entity<ChangelingIdentityComponent> ent, EntityUid entityUid)
{
if (!TryComp<ActorComponent>(entityUid, out var actor))
return;
foreach (var identity in ent.Comp.ConsumedIdentities)
{
_pvsOverrideSystem.RemoveSessionOverride(identity, actor.PlayerSession);
}
}
/// <summary>
/// Inform another Session of the entities stored for Transformation
/// </summary>
/// <param name="session">The Session you wish to inform</param>
/// <param name="comp">The Target storage of identities</param>
public void HandOverPvsOverride(ICommonSession session, ChangelingIdentityComponent comp)
{
foreach (var entity in comp.ConsumedIdentities)
{
_pvsOverrideSystem.AddSessionOverride(entity, session);
}
}
/// <summary>
/// Create a paused map for storing devoured identities as a clone of the player.
/// </summary>
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);
}
}

View File

@@ -0,0 +1,9 @@
using Content.Shared.Roles;
namespace Content.Shared.Changeling;
/// <summary>
/// The Mindrole for Changeling Antags
/// </summary>
[RegisterComponent]
public sealed partial class ChangelingRoleComponent : BaseMindRoleComponent;

View File

@@ -0,0 +1,29 @@
using Robust.Shared.GameStates;
using Robust.Shared.Player;
namespace Content.Shared.Changeling;
/// <summary>
/// Marker component for cloned identities devoured by a changeling.
/// These are stored on a paused map so that the changeling can transform into them.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ChangelingStoredIdentityComponent : Component
{
/// <summary>
/// The original entity the identity was cloned from.
/// </summary>
/// <remarks>
/// TODO: Not networked at the moment because it will create PVS errors when the original is somehow deleted.
/// Use WeakEntityReference once it's merged.
/// </remarks>
[DataField]
public EntityUid? OriginalEntity;
/// <summary>
/// The player session of the original entity, if any.
/// Used for admin logging purposes.
/// </summary>
[ViewVariables]
public ICommonSession? OriginalSession;
}

View File

@@ -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;
/// <summary>
/// Component responsible for Changelings Devour attack. Including the amount of damage
/// and how long it takes to devour someone
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
[Access(typeof(ChangelingDevourSystem))]
public sealed partial class ChangelingDevourComponent : Component
{
/// <summary>
/// The Action for devouring
/// </summary>
[DataField]
public EntProtoId? ChangelingDevourAction = "ActionChangelingDevour";
/// <summary>
/// The action entity associated with devouring
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? ChangelingDevourActionEntity;
/// <summary>
/// The whitelist of targets for devouring
/// </summary>
[DataField, AutoNetworkedField]
public EntityWhitelist? Whitelist = new()
{
Components =
[
"MobState",
"HumanoidAppearance",
],
};
/// <summary>
/// The Sound to use during consumption of a victim
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[DataField, AutoNetworkedField]
public SoundSpecifier? ConsumeNoise = new SoundCollectionSpecifier("ChangelingDevourConsume", AudioParams.Default.WithMaxDistance(6));
/// <summary>
/// The Sound to use during the windup before consuming a victim
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[DataField, AutoNetworkedField]
public SoundSpecifier? DevourWindupNoise = new SoundCollectionSpecifier("ChangelingDevourWindup", AudioParams.Default.WithMaxDistance(6));
/// <summary>
/// The time between damage ticks
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan DamageTimeBetweenTicks = TimeSpan.FromSeconds(1);
/// <summary>
/// The windup time before the changeling begins to engage in devouring the identity of a target
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan DevourWindupTime = TimeSpan.FromSeconds(2);
/// <summary>
/// The time it takes to FULLY consume someones identity.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan DevourConsumeTime = TimeSpan.FromSeconds(10);
/// <summary>
/// Damage cap that a target is allowed to be caused due to IdentityConsumption
/// </summary>
[DataField, AutoNetworkedField]
public float DevourConsumeDamageCap = 350f;
/// <summary>
/// The Currently active devour sound in the world
/// </summary>
[DataField]
public EntityUid? CurrentDevourSound;
/// <summary>
/// The damage profile for a single tick of devour damage
/// </summary>
[DataField, AutoNetworkedField]
public DamageSpecifier DamagePerTick = new()
{
DamageDict = new Dictionary<string, FixedPoint2>
{
{ "Slash", 10},
{ "Piercing", 10 },
{ "Blunt", 5 },
},
};
/// <summary>
/// The list of protective damage types capable of preventing a devour if over the threshold
/// </summary>
[DataField, AutoNetworkedField]
public List<ProtoId<DamageTypePrototype>> ProtectiveDamageTypes = new()
{
"Slash",
"Piercing",
"Blunt",
};
/// <summary>
/// The next Tick to deal damage on (utilized during the consumption "do-during" (a do after with an attempt event))
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoNetworkedField, AutoPausedField]
public TimeSpan NextTick = TimeSpan.Zero;
/// <summary>
/// The percentage of ANY brute damage resistance that will prevent devouring
/// </summary>
[DataField, AutoNetworkedField]
public float DevourPreventionPercentageThreshold = 0.1f;
public override bool SendOnlyToOwner => true;
}

View File

@@ -0,0 +1,22 @@
using Content.Shared.Actions;
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;
namespace Content.Shared.Changeling.Devour;
/// <summary>
/// Action event for Devour, someone has initiated a devour on someone, begin to windup.
/// </summary>
public sealed partial class ChangelingDevourActionEvent : EntityTargetActionEvent;
/// <summary>
/// A windup has either successfully been completed or has been canceled. If successful start the devouring DoAfter.
/// </summary>
[Serializable, NetSerializable]
public sealed partial class ChangelingDevourWindupDoAfterEvent : SimpleDoAfterEvent;
/// <summary>
/// The Consumption DoAfter has either successfully been completed or was canceled.
/// </summary>
[Serializable, NetSerializable]
public sealed partial class ChangelingDevourConsumeDoAfterEvent : SimpleDoAfterEvent;

View File

@@ -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<ChangelingDevourComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ChangelingDevourComponent, ChangelingDevourActionEvent>(OnDevourAction);
SubscribeLocalEvent<ChangelingDevourComponent, ChangelingDevourWindupDoAfterEvent>(OnDevourWindup);
SubscribeLocalEvent<ChangelingDevourComponent, ChangelingDevourConsumeDoAfterEvent>(OnDevourConsume);
SubscribeLocalEvent<ChangelingDevourComponent, DoAfterAttemptEvent<ChangelingDevourConsumeDoAfterEvent>>(OnConsumeAttemptTick);
SubscribeLocalEvent<ChangelingDevourComponent, ComponentShutdown>(OnShutdown);
}
private void OnMapInit(Entity<ChangelingDevourComponent> ent, ref MapInitEvent args)
{
_actionsSystem.AddAction(ent, ref ent.Comp.ChangelingDevourActionEntity, ent.Comp.ChangelingDevourAction);
}
private void OnShutdown(Entity<ChangelingDevourComponent> 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<ChangelingDevourComponent> ent,
ref DoAfterAttemptEvent<ChangelingDevourConsumeDoAfterEvent> 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<DamageableComponent>(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);
}
/// <summary>
/// Checkes if the targets outerclothing is beyond a DamageCoefficientThreshold to protect them from being devoured.
/// </summary>
/// <param name="target">The Targeted entity</param>
/// <param name="ent">Changelings Devour Component</param>
/// <returns>Is the target Protected from the attack</returns>
private bool IsTargetProtected(EntityUid target, Entity<ChangelingDevourComponent> 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<ChangelingDevourComponent> ent, ref ChangelingDevourActionEvent args)
{
if (args.Handled || _whitelistSystem.IsWhitelistFailOrNull(ent.Comp.Whitelist, args.Target)
|| !HasComp<ChangelingIdentityComponent>(ent))
return;
args.Handled = true;
var target = args.Target;
if (target == ent.Owner)
return; // don't eat yourself
if (HasComp<RottingComponent>(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<ChangelingDevourComponent> 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<ChangelingDevourComponent> 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<BodyComponent>(target, out var body)
&& HasComp<HumanoidAppearanceComponent>(target)
&& TryComp<ChangelingIdentityComponent>(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<ButcherableComponent>(item, out var butcherable))
RipClothing(target.Value, (item.Value, butcherable));
}
Dirty(ent);
}
private void RipClothing(EntityUid victim, Entity<ButcherableComponent> 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);
}
}

View File

@@ -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;
/// <summary>
/// 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
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(ChangelingTransformSystem))]
public sealed partial class ChangelingTransformComponent : Component
{
/// <summary>
/// The action Prototype for Transforming
/// </summary>
[DataField]
public EntProtoId? ChangelingTransformAction = "ActionChangelingTransform";
/// <summary>
/// The Action Entity for transforming associated with this Component
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? ChangelingTransformActionEntity;
/// <summary>
/// Time it takes to Transform
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan TransformWindup = TimeSpan.FromSeconds(5);
/// <summary>
/// The noise used when attempting to transform
/// </summary>
[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.
/// <summary>
/// The currently active transform in the world
/// </summary>
[DataField]
public EntityUid? CurrentTransformSound;
/// <summary>
/// The cloning settings passed to the CloningSystem, contains a list of all components to copy or have handled by their
/// respective systems.
/// </summary>
public ProtoId<CloningSettingsPrototype> TransformCloningSettings = "ChangelingCloningSettings";
public override bool SendOnlyToOwner => true;
}

View File

@@ -0,0 +1,16 @@
using Content.Shared.Actions;
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;
namespace Content.Shared.Changeling.Transform;
/// <summary>
/// Action event for opening the changeling transformation radial menu.
/// </summary>
public sealed partial class ChangelingTransformActionEvent : InstantActionEvent;
/// <summary>
/// DoAfterevent used to transform a changeling into one of their stored identities.
/// </summary>
[Serializable, NetSerializable]
public sealed partial class ChangelingTransformDoAfterEvent : SimpleDoAfterEvent;

View File

@@ -0,0 +1,33 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Changeling.Transform;
/// <summary>
/// Send when a player selects an intentity to transform into in the radial menu.
/// </summary>
[Serializable, NetSerializable]
public sealed class ChangelingTransformIdentitySelectMessage(NetEntity targetIdentity) : BoundUserInterfaceMessage
{
/// <summary>
/// The uid of the cloned identity.
/// </summary>
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<NetEntity> identities) : BoundUserInterfaceState
{
/// <summary>
/// The uids of the cloned identities.
/// </summary>
public readonly List<NetEntity> Identites = identities;
}
[Serializable, NetSerializable]
public enum TransformUI : byte
{
Key,
}

View File

@@ -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<ChangelingTransformComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<ChangelingTransformComponent, ChangelingTransformActionEvent>(OnTransformAction);
SubscribeLocalEvent<ChangelingTransformComponent, ChangelingTransformDoAfterEvent>(OnSuccessfulTransform);
SubscribeLocalEvent<ChangelingTransformComponent, ChangelingTransformIdentitySelectMessage>(OnTransformSelected);
SubscribeLocalEvent<ChangelingTransformComponent, ComponentShutdown>(OnShutdown);
}
private void OnMapInit(Entity<ChangelingTransformComponent> ent, ref MapInitEvent init)
{
_actionsSystem.AddAction(ent, ref ent.Comp.ChangelingTransformActionEntity, ent.Comp.ChangelingTransformAction);
var userInterfaceComp = EnsureComp<UserInterfaceComponent>(ent);
_uiSystem.SetUi((ent, userInterfaceComp), TransformUI.Key, new InterfaceData(ChangelingBuiXmlGeneratedName));
}
private void OnShutdown(Entity<ChangelingTransformComponent> ent, ref ComponentShutdown args)
{
if (ent.Comp.ChangelingTransformActionEntity != null)
{
_actionsSystem.RemoveAction(ent.Owner, ent.Comp.ChangelingTransformActionEntity);
}
}
private void OnTransformAction(Entity<ChangelingTransformComponent> ent,
ref ChangelingTransformActionEvent args)
{
if (!TryComp<UserInterfaceComponent>(ent, out var userInterfaceComp))
return;
if (!TryComp<ChangelingIdentityComponent>(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<NetEntity>();
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.
}
/// <summary>
/// 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.
/// </summary>
public void TransformInto(Entity<ChangelingTransformComponent?> 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<ChangelingStoredIdentityComponent>(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<ChangelingTransformComponent> ent,
ref ChangelingTransformIdentitySelectMessage args)
{
_uiSystem.CloseUi(ent.Owner, TransformUI.Key, ent);
if (!TryGetEntity(args.TargetIdentity, out var targetIdentity))
return;
if (!TryComp<ChangelingIdentityComponent>(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<ChangelingTransformComponent> 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<ChangelingStoredIdentityComponent>(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<ChangelingIdentityComponent>(ent, out var identity)) // in case we ever get changelings that don't store identities
{
identity.CurrentIdentity = targetIdentity;
Dirty(ent.Owner, identity);
}
}
}

View File

@@ -0,0 +1,74 @@
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Shared.Chat.Prototypes;
/// <summary>
/// A predefined notification used to warn a player of specific events.
/// </summary>
[Prototype("chatNotification")]
public sealed partial class ChatNotificationPrototype : IPrototype
{
[ViewVariables]
[IdDataField]
public string ID { get; private set; } = default!;
/// <summary>
/// The notification that the player receives.
/// </summary>
/// <remarks>
/// Use '{$source}', '{user}', and '{target}' in the fluent message
/// to insert the source, user, and target names respectively.
/// </remarks>
[DataField(required: true)]
public LocId Message = string.Empty;
/// <summary>
/// Font color for the notification.
/// </summary>
[DataField]
public Color Color = Color.White;
/// <summary>
/// Sound played upon receiving the notification.
/// </summary>
[DataField]
public SoundSpecifier? Sound;
/// <summary>
/// The period during which duplicate chat notifications are blocked after a player receives one.
/// Blocked notifications will never be delivered to the player.
/// </summary>
[DataField]
public TimeSpan NextDelay = TimeSpan.FromSeconds(10.0);
/// <summary>
/// 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).
/// </summary>
[DataField]
public bool NotifyBySource = false;
}
/// <summary>
/// Raised when an specific player should be notified via a chat message of a predefined event occuring.
/// </summary>
/// <param name="ChatNotification">The prototype used to define the chat notification.</param>
/// <param name="Source">The entity that the triggered the notification.</param>
/// <param name="User">The entity that ultimately responsible for triggering the notification.</param>
[ByRefEvent]
public record ChatNotificationEvent(ProtoId<ChatNotificationPrototype> ChatNotification, EntityUid Source, EntityUid? User = null)
{
/// <summary>
/// Set this variable if you want to change the name of the notification source
/// (if the name is included in the chat notification).
/// </summary>
public string? SourceNameOverride;
/// <summary>
/// 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).
/// </summary>
public string? UserNameOverride;
}

View File

@@ -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.
/// </summary>
[RegisterComponent, NetworkedComponent]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedTypingIndicatorSystem))]
public sealed partial class TypingIndicatorComponent : Component
{
/// <summary>
/// Prototype id that store all visual info about typing indicator.
/// </summary>
[DataField("proto")]
[DataField("proto"), AutoNetworkedField]
public ProtoId<TypingIndicatorPrototype> TypingIndicatorPrototype = "default";
}

View File

@@ -0,0 +1,14 @@
namespace Content.Shared.Cloning;
public abstract partial class SharedCloningSystem : EntitySystem
{
/// <summary>
/// Copy components from one entity to another based on a CloningSettingsPrototype.
/// </summary>
/// <param name="original">The orignal Entity to clone components from.</param>
/// <param name="clone">The target Entity to clone components to.</param>
/// <param name="settings">The clone settings prototype containing the list of components to clone.</param>
public virtual void CloneComponents(EntityUid original, EntityUid clone, CloningSettingsPrototype settings)
{
}
}

View File

@@ -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<ItemComponent>(entity);
if (!storable && !HasComp<BodyComponent>(entity))
if (!storable && !HasComp<MobStateComponent>(entity))
return false;
if (_whitelistSystem.IsBlacklistPass(component.Blacklist, entity) ||

View File

@@ -0,0 +1,18 @@
using Content.Shared.Forensics.Components;
namespace Content.Shared.Forensics.Systems;
public abstract class SharedForensicsSystem : EntitySystem
{
/// <summary>
/// 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.
/// </summary>
public virtual void RandomizeDNA(Entity<DnaComponent?> ent) { }
/// <summary>
/// Give the entity a new, random fingerprint string.
/// Does nothing if it does not have the FingerprintComponent.
/// </summary>
public virtual void RandomizeFingerprint(Entity<FingerprintComponent?> ent) { }
}

View File

@@ -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<GrammarComponent>(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);
}
/// <summary>
/// Sets the gender in the entity's HumanoidAppearanceComponent and GrammarComponent.
/// </summary>
public void SetGender(Entity<HumanoidAppearanceComponent?> ent, Gender gender)
{
if (!Resolve(ent, ref ent.Comp))
return;
ent.Comp.Gender = gender;
Dirty(ent);
if (TryComp<GrammarComponent>(ent, out var grammar))
_grammarSystem.SetGender((ent, grammar), gender);
_identity.QueueIdentityUpdate(ent);
}
/// <summary>
/// Sets the skin color of this humanoid mob. Will only affect base layers that are not custom,
/// custom base layers should use <see cref="SetBaseLayerColor"/> instead.

View File

@@ -49,7 +49,7 @@ public sealed partial class SubdermalImplantComponent : Component
/// </summary>
[DataField]
public EntityWhitelist? Blacklist;
/// <summary>
/// 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
{
}
/// <summary>
/// Used for triggering trigger events on the implant via action
/// </summary>
@@ -86,13 +81,3 @@ public sealed partial class OpenUplinkImplantEvent : InstantActionEvent
{
}
public sealed partial class UseScramImplantEvent : InstantActionEvent
{
}
public sealed partial class UseDnaScramblerImplantEvent : InstantActionEvent
{
}

View File

@@ -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)

View File

@@ -20,20 +20,4 @@ public sealed partial class IntellicardComponent : Component
/// </summary>
[DataField, AutoNetworkedField]
public int UploadTime = 3;
/// <summary>
/// The sound that plays for the AI
/// when they are being downloaded
/// </summary>
[DataField, AutoNetworkedField]
public SoundSpecifier? WarningSound = new SoundPathSpecifier("/Audio/Misc/notice2.ogg");
/// <summary>
/// The delay before allowing the warning to play again in seconds.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan WarningDelay = TimeSpan.FromSeconds(8);
[ViewVariables]
public TimeSpan NextWarningAllowed = TimeSpan.Zero;
}

View File

@@ -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;

Some files were not shown because too many files have changed in this diff Show More