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:
@@ -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:
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
5
Content.Client/Cloning/CloningSystem.cs
Normal file
5
Content.Client/Cloning/CloningSystem.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.Cloning;
|
||||
|
||||
namespace Content.Client.Cloning;
|
||||
|
||||
public sealed partial class CloningSystem : SharedCloningSystem;
|
||||
5
Content.Client/Forensics/Systems/ForensicsSystem.cs
Normal file
5
Content.Client/Forensics/Systems/ForensicsSystem.cs
Normal file
@@ -0,0 +1,5 @@
|
||||
using Content.Shared.Forensics.Systems;
|
||||
|
||||
namespace Content.Client.Forensics.Systems;
|
||||
|
||||
public sealed class ForensicsSystem : SharedForensicsSystem;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<ScrollContainer HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
SizeFlagsStretchRatio="6">
|
||||
<GridContainer Name="Values"/>
|
||||
<GridContainer Name="Values" HSeparationOverride="0" VSeparationOverride="15"/>
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)}");
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
105
Content.Server/Chat/Systems/ChatNotificationSystem.cs
Normal file
105
Content.Server/Chat/Systems/ChatNotificationSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Shared.Station.Components;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server.Commands;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
23
Content.Server/GameTicking/Rules/ChangelingRuleSystem.cs
Normal file
23
Content.Server/GameTicking/Rules/ChangelingRuleSystem.cs
Normal 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"));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
43
Content.Server/Mind/Filters/TargetObjectiveMindFilter.cs
Normal file
43
Content.Server/Mind/Filters/TargetObjectiveMindFilter.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Shared.Station.Components;
|
||||
|
||||
namespace Content.Server.Station.Events;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Server.Station.Components;
|
||||
using Content.Shared.Station.Components;
|
||||
|
||||
namespace Content.Server.Station.Systems;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
35
Content.Shared/Changeling/ChangelingIdentityComponent.cs
Normal file
35
Content.Shared/Changeling/ChangelingIdentityComponent.cs
Normal 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;
|
||||
}
|
||||
185
Content.Shared/Changeling/ChangelingIdentitySystem.cs
Normal file
185
Content.Shared/Changeling/ChangelingIdentitySystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
9
Content.Shared/Changeling/ChangelingRoleComponent.cs
Normal file
9
Content.Shared/Changeling/ChangelingRoleComponent.cs
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
133
Content.Shared/Changeling/Devour/ChangelingDevourComponent.cs
Normal file
133
Content.Shared/Changeling/Devour/ChangelingDevourComponent.cs
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
276
Content.Shared/Changeling/Devour/ChangelingDevourSystem.cs
Normal file
276
Content.Shared/Changeling/Devour/ChangelingDevourSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
}
|
||||
180
Content.Shared/Changeling/Transform/ChangelingTransformSystem.cs
Normal file
180
Content.Shared/Changeling/Transform/ChangelingTransformSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
74
Content.Shared/Chat/Prototypes/ChatNotificationPrototype.cs
Normal file
74
Content.Shared/Chat/Prototypes/ChatNotificationPrototype.cs
Normal 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;
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
14
Content.Shared/Cloning/SharedCloningSystem.cs
Normal file
14
Content.Shared/Cloning/SharedCloningSystem.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -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) ||
|
||||
|
||||
18
Content.Shared/Forensics/Systems/SharedForensicsSystem.cs
Normal file
18
Content.Shared/Forensics/Systems/SharedForensicsSystem.cs
Normal 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) { }
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user