Merge remote-tracking branch 'refs/remotes/upstream/master' into upstream-sync

# Conflicts:
#	Content.Server/Chat/Managers/ChatManager.cs
#	Resources/Prototypes/Catalog/VendingMachines/Inventories/detdrobe.yml
#	Resources/Prototypes/Entities/Mobs/Player/humanoid.yml
#	Resources/Prototypes/Loadouts/role_loadouts.yml
#	Resources/Prototypes/Roles/Jobs/Wildcards/psychologist.yml
#	Resources/Textures/Clothing/Head/Hats/brownfedora.rsi/equipped-HELMET.png
#	Resources/Textures/Clothing/Head/Hats/brownfedora.rsi/icon.png
#	Resources/Textures/Clothing/Head/Hats/brownfedora.rsi/meta.json
#	Resources/Textures/Clothing/Head/Hats/greyfedora.rsi/equipped-HELMET.png
#	Resources/Textures/Clothing/Head/Hats/greyfedora.rsi/icon.png
#	Resources/Textures/Clothing/Head/Hats/greyfedora.rsi/meta.json
#	Resources/Textures/Clothing/Head/Hats/hoshat.rsi/equipped-HELMET-hamster.png
#	Resources/Textures/Clothing/Head/Hats/hoshat.rsi/equipped-HELMET.png
#	Resources/Textures/Clothing/Head/Hats/hoshat.rsi/icon.png
#	Resources/Textures/Clothing/Head/Hats/hoshat.rsi/inhand-left.png
#	Resources/Textures/Clothing/Head/Hats/hoshat.rsi/inhand-right.png
#	Resources/Textures/Clothing/Head/Hats/hoshat.rsi/meta.json
#	Resources/Textures/Clothing/Head/Helmets/light_riot.rsi/equipped-HELMET.png
#	Resources/Textures/Clothing/Head/Helmets/light_riot.rsi/meta.json
#	Resources/Textures/Clothing/Head/Helmets/security.rsi/equipped-HELMET.png
#	Resources/Textures/Clothing/Head/Helmets/security.rsi/inhand-left.png
#	Resources/Textures/Clothing/Head/Helmets/security.rsi/inhand-right.png
#	Resources/Textures/Clothing/Head/Helmets/security.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Coats/detective.rsi/equipped-OUTERCLOTHING.png
#	Resources/Textures/Clothing/OuterClothing/Coats/detective.rsi/icon.png
#	Resources/Textures/Clothing/OuterClothing/Coats/detective.rsi/inhand-left.png
#	Resources/Textures/Clothing/OuterClothing/Coats/detective.rsi/inhand-right.png
#	Resources/Textures/Clothing/OuterClothing/Coats/detective.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Coats/hos_trenchcoat.rsi/equipped-OUTERCLOTHING.png
#	Resources/Textures/Clothing/OuterClothing/Coats/hos_trenchcoat.rsi/icon.png
#	Resources/Textures/Clothing/OuterClothing/Coats/hos_trenchcoat.rsi/inhand-left.png
#	Resources/Textures/Clothing/OuterClothing/Coats/hos_trenchcoat.rsi/inhand-right.png
#	Resources/Textures/Clothing/OuterClothing/Coats/hos_trenchcoat.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Coats/warden.rsi/equipped-OUTERCLOTHING.png
#	Resources/Textures/Clothing/OuterClothing/Coats/warden.rsi/icon.png
#	Resources/Textures/Clothing/OuterClothing/Coats/warden.rsi/meta.json
#	Resources/Textures/Clothing/OuterClothing/Vests/detvest.rsi/equipped-OUTERCLOTHING.png
#	Resources/Textures/Clothing/OuterClothing/Vests/detvest.rsi/icon.png
#	Resources/Textures/Clothing/OuterClothing/Vests/detvest.rsi/meta.json
#	Resources/Textures/Clothing/Uniforms/Jumpskirt/hos.rsi/equipped-INNERCLOTHING.png
#	Resources/Textures/Clothing/Uniforms/Jumpskirt/hos.rsi/icon.png
#	Resources/Textures/Clothing/Uniforms/Jumpskirt/security.rsi/equipped-INNERCLOTHING.png
#	Resources/Textures/Clothing/Uniforms/Jumpskirt/security.rsi/icon.png
#	Resources/Textures/Clothing/Uniforms/Jumpskirt/warden.rsi/equipped-INNERCLOTHING.png
#	Resources/Textures/Clothing/Uniforms/Jumpskirt/warden.rsi/icon.png
#	Resources/Textures/Clothing/Uniforms/Jumpskirt/warden.rsi/meta.json
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/hos.rsi/equipped-INNERCLOTHING-monkey.png
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/hos.rsi/equipped-INNERCLOTHING.png
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/hos.rsi/icon.png
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/hos.rsi/meta.json
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/security.rsi/equipped-INNERCLOTHING-monkey.png
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/security.rsi/equipped-INNERCLOTHING.png
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/security.rsi/icon.png
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/security.rsi/meta.json
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/warden.rsi/equipped-INNERCLOTHING-monkey.png
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/warden.rsi/equipped-INNERCLOTHING.png
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/warden.rsi/icon.png
#	Resources/Textures/Clothing/Uniforms/Jumpsuit/warden.rsi/meta.json
#	Resources/Textures/Objects/Storage/boxes.rsi/meta.json
#	Resources/Textures/Structures/Piping/Atmospherics/Portable/portable_scrubber.rsi/unlit-full.png
#	Resources/Textures/Structures/Piping/Atmospherics/Portable/portable_scrubber.rsi/unlit.png
This commit is contained in:
Morb0
2024-06-21 16:32:00 +03:00
381 changed files with 4446 additions and 2483 deletions

View File

@@ -15,9 +15,12 @@ jobs:
- name: Get changed files
id: files
uses: Ana06/get-changed-files@v1.2
uses: Ana06/get-changed-files@v2.3.0
with:
format: 'space-delimited'
filter: |
**.rsi
**.png
- name: Diff changed RSIs
id: diff

View File

@@ -3,6 +3,7 @@ using Content.Shared.Buckle;
using Content.Shared.Buckle.Components;
using Content.Shared.Rotation;
using Robust.Client.GameObjects;
using Robust.Shared.GameStates;
namespace Content.Client.Buckle;
@@ -14,40 +15,63 @@ internal sealed class BuckleSystem : SharedBuckleSystem
{
base.Initialize();
SubscribeLocalEvent<BuckleComponent, AfterAutoHandleStateEvent>(OnBuckleAfterAutoHandleState);
SubscribeLocalEvent<BuckleComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<BuckleComponent, AppearanceChangeEvent>(OnAppearanceChange);
SubscribeLocalEvent<StrapComponent, MoveEvent>(OnStrapMoveEvent);
}
private void OnBuckleAfterAutoHandleState(EntityUid uid, BuckleComponent component, ref AfterAutoHandleStateEvent args)
private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveEvent args)
{
ActionBlocker.UpdateCanMove(uid);
// I'm moving this to the client-side system, but for the sake of posterity let's keep this comment:
// > This is mega cursed. Please somebody save me from Mr Buckle's wild ride
if (!TryComp<SpriteComponent>(uid, out var ownerSprite))
// The nice thing is its still true, this is quite cursed, though maybe not omega cursed anymore.
// This code is garbage, it doesn't work with rotated viewports. I need to finally get around to reworking
// sprite rendering for entity layers & direction dependent sorting.
if (args.NewRotation == args.OldRotation)
return;
// Adjust draw depth when the chair faces north so that the seat back is drawn over the player.
// Reset the draw depth when rotated in any other direction.
// TODO when ECSing, make this a visualizer
// This code was written before rotatable viewports were introduced, so hard-coding Direction.North
// and comparing it against LocalRotation now breaks this in other rotations. This is a FIXME, but
// better to get it working for most people before we look at a more permanent solution.
if (component is { Buckled: true, LastEntityBuckledTo: { } } &&
Transform(component.LastEntityBuckledTo.Value).LocalRotation.GetCardinalDir() == Direction.North &&
TryComp<SpriteComponent>(component.LastEntityBuckledTo, out var buckledSprite))
{
component.OriginalDrawDepth ??= ownerSprite.DrawDepth;
ownerSprite.DrawDepth = buckledSprite.DrawDepth - 1;
if (!TryComp<SpriteComponent>(uid, out var strapSprite))
return;
}
// If here, we're not turning north and should restore the saved draw depth.
if (component.OriginalDrawDepth.HasValue)
var isNorth = Transform(uid).LocalRotation.GetCardinalDir() == Direction.North;
foreach (var buckledEntity in component.BuckledEntities)
{
ownerSprite.DrawDepth = component.OriginalDrawDepth.Value;
component.OriginalDrawDepth = null;
if (!TryComp<BuckleComponent>(buckledEntity, out var buckle))
continue;
if (!TryComp<SpriteComponent>(buckledEntity, out var buckledSprite))
continue;
if (isNorth)
{
buckle.OriginalDrawDepth ??= buckledSprite.DrawDepth;
buckledSprite.DrawDepth = strapSprite.DrawDepth - 1;
}
else if (buckle.OriginalDrawDepth.HasValue)
{
buckledSprite.DrawDepth = buckle.OriginalDrawDepth.Value;
buckle.OriginalDrawDepth = null;
}
}
}
private void OnHandleState(Entity<BuckleComponent> ent, ref ComponentHandleState args)
{
if (args.Current is not BuckleState state)
return;
ent.Comp.DontCollide = state.DontCollide;
ent.Comp.BuckleTime = state.BuckleTime;
var strapUid = EnsureEntity<BuckleComponent>(state.BuckledTo, ent);
SetBuckledTo(ent, strapUid == null ? null : new (strapUid.Value, null));
var (uid, component) = ent;
}
private void OnAppearanceChange(EntityUid uid, BuckleComponent component, ref AppearanceChangeEvent args)
{
if (!TryComp<RotationVisualsComponent>(uid, out var rotVisuals))

View File

@@ -0,0 +1,19 @@
using Content.Shared.Clothing.Components;
using Robust.Client.Physics;
namespace Content.Client.Clothing.Systems;
public sealed partial class PilotedByClothingSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PilotedByClothingComponent, UpdateIsPredictedEvent>(OnUpdatePredicted);
}
private void OnUpdatePredicted(Entity<PilotedByClothingComponent> entity, ref UpdateIsPredictedEvent args)
{
args.BlockPrediction = true;
}
}

View File

@@ -0,0 +1,6 @@
using Content.Shared.DeviceNetwork.Systems;
namespace Content.Client.DeviceNetwork.Systems;
/// <inheritdoc/>
public sealed class DeviceNetworkJammerSystem : SharedDeviceNetworkJammerSystem;

View File

@@ -2,8 +2,10 @@ using Content.Shared.Effects;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Shared.Animations;
using Robust.Shared.Collections;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.Effects;
@@ -11,13 +13,13 @@ public sealed class ColorFlashEffectSystem : SharedColorFlashEffectSystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly AnimationPlayerSystem _animation = default!;
[Dependency] private readonly IComponentFactory _factory = default!;
/// <summary>
/// It's a little on the long side but given we use multiple colours denoting what happened it makes it easier to register.
/// </summary>
private const float AnimationLength = 0.30f;
private const string AnimationKey = "color-flash-effect";
private ValueList<EntityUid> _toRemove = new();
public override void Initialize()
{
@@ -44,8 +46,28 @@ public sealed class ColorFlashEffectSystem : SharedColorFlashEffectSystem
{
sprite.Color = component.Color;
}
}
RemCompDeferred<ColorFlashEffectComponent>(uid);
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = AllEntityQuery<ColorFlashEffectComponent>();
_toRemove.Clear();
// Can't use deferred removal on animation completion or it will cause issues.
while (query.MoveNext(out var uid, out _))
{
if (_animation.HasRunningAnimation(uid, AnimationKey))
continue;
_toRemove.Add(uid);
}
foreach (var ent in _toRemove)
{
RemComp<ColorFlashEffectComponent>(ent);
}
}
private Animation? GetDamageAnimation(EntityUid uid, Color color, SpriteComponent? sprite = null)
@@ -82,51 +104,31 @@ public sealed class ColorFlashEffectSystem : SharedColorFlashEffectSystem
{
var ent = GetEntity(nent);
if (Deleted(ent))
if (Deleted(ent) || !TryComp(ent, out SpriteComponent? sprite))
{
continue;
}
if (!TryComp(ent, out AnimationPlayerComponent? player))
{
player = (AnimationPlayerComponent) _factory.GetComponent(typeof(AnimationPlayerComponent));
player.Owner = ent;
player.NetSyncEnabled = false;
AddComp(ent, player);
}
// Need to stop the existing animation first to ensure the sprite color is fixed.
// Otherwise we might lerp to a red colour instead.
if (_animation.HasRunningAnimation(ent, player, AnimationKey))
{
_animation.Stop(ent, player, AnimationKey);
}
if (!TryComp<SpriteComponent>(ent, out var sprite))
{
continue;
}
if (TryComp<ColorFlashEffectComponent>(ent, out var effect))
{
sprite.Color = effect.Color;
}
var animation = GetDamageAnimation(ent, color, sprite);
if (animation == null)
continue;
if (!TryComp(ent, out ColorFlashEffectComponent? comp))
{
comp = (ColorFlashEffectComponent) _factory.GetComponent(typeof(ColorFlashEffectComponent));
comp.Owner = ent;
comp.NetSyncEnabled = false;
AddComp(ent, comp);
#if DEBUG
DebugTools.Assert(!_animation.HasRunningAnimation(ent, AnimationKey));
#endif
}
_animation.Stop(ent, AnimationKey);
var animation = GetDamageAnimation(ent, color, sprite);
if (animation == null)
{
continue;
}
EnsureComp<ColorFlashEffectComponent>(ent, out comp);
comp.NetSyncEnabled = false;
comp.Color = sprite.Color;
_animation.Play((ent, player), animation, AnimationKey);
_animation.Play(ent, animation, AnimationKey);
}
}
}

View File

@@ -57,6 +57,6 @@ public sealed class FloatingVisualizerSystem : SharedFloatingVisualizerSystem
if (args.Key != component.AnimationKey)
return;
FloatAnimation(uid, component.Offset, component.AnimationKey, component.AnimationTime, !component.CanFloat);
FloatAnimation(uid, component.Offset, component.AnimationKey, component.AnimationTime, stop: !component.CanFloat);
}
}

View File

@@ -495,7 +495,7 @@ public sealed class DragDropSystem : SharedDragDropSystem
// CanInteract() doesn't support checking a second "target" entity.
// Doing so manually:
var ev = new GettingInteractedWithAttemptEvent(user, dragged);
RaiseLocalEvent(dragged, ev, true);
RaiseLocalEvent(dragged, ref ev);
if (ev.Cancelled)
return false;

View File

@@ -100,12 +100,11 @@
Margin="5 0 0 0"
Text="{Loc 'lathe-menu-fabricating-message'}">
</Label>
<TextureRect
Name="Icon"
HorizontalExpand="True"
SizeFlagsStretchRatio="2"
Margin="100 0 0 0">
</TextureRect>
<EntityPrototypeView
Name="FabricatingEntityProto"
HorizontalAlignment="Left"
Margin="100 0 0 0"
/>
<Label
Name="NameLabel"
RectClipContent="True"
@@ -114,12 +113,15 @@
</Label>
</PanelContainer>
</BoxContainer>
<ItemList
Name="QueueList"
VerticalExpand="True"
SizeFlagsStretchRatio="3"
SelectMode="None">
</ItemList>
<ScrollContainer VerticalExpand="True" HScrollEnabled="False">
<BoxContainer
Name="QueueList"
Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True"
RectClipContent="True">
</BoxContainer>
</ScrollContainer>
</BoxContainer>
<BoxContainer
VerticalExpand="True"

View File

@@ -78,9 +78,6 @@ public sealed partial class LatheMenu : DefaultWindow
/// </summary>
public void PopulateRecipes()
{
if (!_entityManager.TryGetComponent<LatheComponent>(_owner, out var component))
return;
var recipesToShow = new List<LatheRecipePrototype>();
foreach (var recipe in Recipes)
{
@@ -108,21 +105,13 @@ public sealed partial class LatheMenu : DefaultWindow
RecipeList.Children.Clear();
foreach (var prototype in sortedRecipesToShow)
{
List<Texture> textures;
EntityPrototype? recipeProto = null;
if (_prototypeManager.TryIndex(prototype.Result, out EntityPrototype? entityProto) && entityProto != null)
{
textures = SpriteComponent.GetPrototypeTextures(entityProto, _resources).Select(o => o.Default).ToList();
}
else
{
textures = prototype.Icon == null
? new List<Texture> { _spriteSystem.GetPrototypeIcon(prototype.Result).Default }
: new List<Texture> { _spriteSystem.Frame0(prototype.Icon) };
}
recipeProto = entityProto;
var canProduce = _lathe.CanProduce(_owner, prototype, quantity);
var control = new RecipeControl(prototype, () => GenerateTooltipText(prototype), canProduce, textures);
var control = new RecipeControl(prototype, () => GenerateTooltipText(prototype), canProduce, recipeProto);
control.OnButtonPressed += s =>
{
if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount <= 0)
@@ -219,14 +208,23 @@ public sealed partial class LatheMenu : DefaultWindow
/// <param name="queue"></param>
public void PopulateQueueList(List<LatheRecipePrototype> queue)
{
QueueList.Clear();
QueueList.DisposeAllChildren();
var idx = 1;
foreach (var recipe in queue)
{
var icon = recipe.Icon == null
? _spriteSystem.GetPrototypeIcon(recipe.Result).Default
: _spriteSystem.Frame0(recipe.Icon);
QueueList.AddItem($"{idx}. {recipe.Name}", icon);
var queuedRecipeBox = new BoxContainer();
queuedRecipeBox.Orientation = BoxContainer.LayoutOrientation.Horizontal;
var queuedRecipeProto = new EntityPrototypeView();
if (_prototypeManager.TryIndex(recipe.Result, out EntityPrototype? entityProto) && entityProto != null)
queuedRecipeProto.SetPrototype(entityProto);
var queuedRecipeLabel = new Label();
queuedRecipeLabel.Text = $"{idx}. {recipe.Name}";
queuedRecipeBox.AddChild(queuedRecipeProto);
queuedRecipeBox.AddChild(queuedRecipeLabel);
QueueList.AddChild(queuedRecipeBox);
idx++;
}
}
@@ -236,9 +234,10 @@ public sealed partial class LatheMenu : DefaultWindow
FabricatingContainer.Visible = recipe != null;
if (recipe == null)
return;
Icon.Texture = recipe.Icon == null
? _spriteSystem.GetPrototypeIcon(recipe.Result).Default
: _spriteSystem.Frame0(recipe.Icon);
if (_prototypeManager.TryIndex(recipe.Result, out EntityPrototype? entityProto) && entityProto != null)
FabricatingEntityProto.SetPrototype(entityProto);
NameLabel.Text = $"{recipe.Name}";
}

View File

@@ -5,14 +5,12 @@
Margin="0"
StyleClasses="ButtonSquare">
<BoxContainer Orientation="Horizontal">
<LayeredTextureRect
Name="RecipeTextures"
<EntityPrototypeView
Name="RecipePrototype"
Margin="0 0 4 0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Stretch="KeepAspectCentered"
MinSize="32 32"
CanShrink="true"
/>
<Label Name="RecipeName" HorizontalExpand="True" />
</BoxContainer>

View File

@@ -4,6 +4,7 @@ using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
namespace Content.Client.Lathe.UI;
@@ -13,12 +14,13 @@ public sealed partial class RecipeControl : Control
public Action<string>? OnButtonPressed;
public Func<string> TooltipTextSupplier;
public RecipeControl(LatheRecipePrototype recipe, Func<string> tooltipTextSupplier, bool canProduce, List<Texture> textures)
public RecipeControl(LatheRecipePrototype recipe, Func<string> tooltipTextSupplier, bool canProduce, EntityPrototype? entityPrototype = null)
{
RobustXamlLoader.Load(this);
RecipeName.Text = recipe.Name;
RecipeTextures.Textures = textures;
if (entityPrototype != null)
RecipePrototype.SetPrototype(entityPrototype);
Button.Disabled = !canProduce;
TooltipTextSupplier = tooltipTextSupplier;
Button.TooltipSupplier = SupplyTooltip;

View File

@@ -29,6 +29,9 @@ public sealed partial class LoadoutWindow : FancyWindow
if (!protoManager.TryIndex(group, out var groupProto))
continue;
if (groupProto.Hidden)
continue;
var container = new LoadoutGroupContainer(profile, loadout, protoManager.Index(group), session, collection);
LoadoutGroupsContainer.AddTab(container, Loc.GetString(groupProto.Name));
_groups.Add(container);

View File

@@ -38,7 +38,7 @@ public sealed partial class StencilOverlay
foreach (var tile in grid.Comp.GetTilesIntersecting(worldAABB))
{
// Ignored tiles for stencil
if (_weather.CanWeatherAffect(grid, tile))
if (_weather.CanWeatherAffect(grid.Owner, grid, tile))
{
continue;
}

View File

@@ -17,7 +17,7 @@ public sealed partial class ReplaySpectatorSystem
SubscribeLocalEvent<ReplaySpectatorComponent, UseAttemptEvent>(OnAttempt);
SubscribeLocalEvent<ReplaySpectatorComponent, PickupAttemptEvent>(OnAttempt);
SubscribeLocalEvent<ReplaySpectatorComponent, ThrowAttemptEvent>(OnAttempt);
SubscribeLocalEvent<ReplaySpectatorComponent, InteractionAttemptEvent>(OnAttempt);
SubscribeLocalEvent<ReplaySpectatorComponent, InteractionAttemptEvent>(OnInteractAttempt);
SubscribeLocalEvent<ReplaySpectatorComponent, AttackAttemptEvent>(OnAttempt);
SubscribeLocalEvent<ReplaySpectatorComponent, DropAttemptEvent>(OnAttempt);
SubscribeLocalEvent<ReplaySpectatorComponent, IsEquippingAttemptEvent>(OnAttempt);
@@ -27,6 +27,11 @@ public sealed partial class ReplaySpectatorSystem
SubscribeLocalEvent<ReplaySpectatorComponent, PullAttemptEvent>(OnPullAttempt);
}
private void OnInteractAttempt(Entity<ReplaySpectatorComponent> ent, ref InteractionAttemptEvent args)
{
args.Cancelled = true;
}
private void OnAttempt(EntityUid uid, ReplaySpectatorComponent component, CancellableEntityEventArgs args)
{
args.Cancel();

View File

@@ -5,7 +5,7 @@
MinSize="700 700">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<!-- First Informational panel -->
<Label Text="{Loc 'thief-backpack-window-description'}" Margin="5 5"/>
<Label Name="Description" Margin="5 5"/>
<controls:HLine Color="#404040" Thickness="2" Margin="0 5"/>
<Label Name="SelectedSets" Text="{Loc 'thief-backpack-window-selected'}" Margin="5 5"/>

View File

@@ -50,6 +50,7 @@ public sealed partial class ThiefBackpackMenu : FancyWindow
selectedNumber++;
}
Description.Text = Loc.GetString("thief-backpack-window-description", ("maxCount", state.MaxSelectedSets));
SelectedSets.Text = Loc.GetString("thief-backpack-window-selected", ("selectedCount", selectedNumber), ("maxCount", state.MaxSelectedSets));
ApproveButton.Disabled = selectedNumber == state.MaxSelectedSets ? false : true;
}

View File

@@ -2,16 +2,11 @@ using System.Numerics;
using Content.Shared.Weather;
using Robust.Client.Audio;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using AudioComponent = Robust.Shared.Audio.Components.AudioComponent;
@@ -62,7 +57,7 @@ public sealed class WeatherSystem : SharedWeatherSystem
if (TryComp<MapGridComponent>(entXform.GridUid, out var grid))
{
var gridId = entXform.GridUid.Value;
// Floodfill to the nearest tile and use that for audio.
// FloodFill to the nearest tile and use that for audio.
var seed = _mapSystem.GetTileRef(gridId, grid, entXform.Coordinates);
var frontier = new Queue<TileRef>();
frontier.Enqueue(seed);
@@ -75,7 +70,7 @@ public sealed class WeatherSystem : SharedWeatherSystem
if (!visited.Add(node.GridIndices))
continue;
if (!CanWeatherAffect(grid, node))
if (!CanWeatherAffect(entXform.GridUid.Value, grid, node))
{
// Add neighbors
// TODO: Ideally we pick some deterministically random direction and use that
@@ -107,7 +102,7 @@ public sealed class WeatherSystem : SharedWeatherSystem
if (nearestNode != null)
{
var entPos = _transform.GetMapCoordinates(entXform);
var nodePosition = nearestNode.Value.ToMap(EntityManager, _transform).Position;
var nodePosition = _transform.ToMapCoordinates(nearestNode.Value).Position;
var delta = nodePosition - entPos.Position;
var distance = delta.Length();
occlusion = _audio.GetOcclusion(entPos, delta, distance);

View File

@@ -0,0 +1,56 @@
using Content.IntegrationTests.Tests.Interaction;
using Content.Shared.Buckle;
using Content.Shared.Buckle.Components;
using Content.Shared.Input;
using Content.Shared.Movement.Pulling.Components;
namespace Content.IntegrationTests.Tests.Buckle;
public sealed class BuckleDragTest : InteractionTest
{
// Check that dragging a buckled player unbuckles them.
[Test]
public async Task BucklePullTest()
{
var urist = await SpawnTarget("MobHuman");
var sUrist = ToServer(urist);
await SpawnTarget("Chair");
var buckle = Comp<BuckleComponent>(urist);
var strap = Comp<StrapComponent>(Target);
var puller = Comp<PullerComponent>(Player);
var pullable = Comp<PullableComponent>(urist);
#pragma warning disable RA0002
buckle.Delay = TimeSpan.Zero;
#pragma warning restore RA0002
// Initially not buckled to the chair and not pulling anything
Assert.That(buckle.Buckled, Is.False);
Assert.That(buckle.BuckledTo, Is.Null);
Assert.That(strap.BuckledEntities, Is.Empty);
Assert.That(puller.Pulling, Is.Null);
Assert.That(pullable.Puller, Is.Null);
Assert.That(pullable.BeingPulled, Is.False);
// Strap the human to the chair
Assert.That(Server.System<SharedBuckleSystem>().TryBuckle(sUrist, SPlayer, STarget.Value));
await RunTicks(5);
Assert.That(buckle.Buckled, Is.True);
Assert.That(buckle.BuckledTo, Is.EqualTo(STarget));
Assert.That(strap.BuckledEntities, Is.EquivalentTo(new[]{sUrist}));
Assert.That(puller.Pulling, Is.Null);
Assert.That(pullable.Puller, Is.Null);
Assert.That(pullable.BeingPulled, Is.False);
// Start pulling, and thus unbuckle them
await PressKey(ContentKeyFunctions.TryPullObject, cursorEntity:urist);
await RunTicks(5);
Assert.That(buckle.Buckled, Is.False);
Assert.That(buckle.BuckledTo, Is.Null);
Assert.That(strap.BuckledEntities, Is.Empty);
Assert.That(puller.Pulling, Is.EqualTo(sUrist));
Assert.That(pullable.Puller, Is.EqualTo(SPlayer));
Assert.That(pullable.BeingPulled, Is.True);
}
}

View File

@@ -91,7 +91,6 @@ namespace Content.IntegrationTests.Tests.Buckle
{
Assert.That(strap, Is.Not.Null);
Assert.That(strap.BuckledEntities, Is.Empty);
Assert.That(strap.OccupiedSize, Is.Zero);
});
// Side effects of buckling
@@ -111,8 +110,6 @@ namespace Content.IntegrationTests.Tests.Buckle
// Side effects of buckling for the strap
Assert.That(strap.BuckledEntities, Does.Contain(human));
Assert.That(strap.OccupiedSize, Is.EqualTo(buckle.Size));
Assert.That(strap.OccupiedSize, Is.Positive);
});
#pragma warning disable NUnit2045 // Interdependent asserts.
@@ -122,7 +119,7 @@ namespace Content.IntegrationTests.Tests.Buckle
// Trying to unbuckle too quickly fails
Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False);
Assert.That(buckle.Buckled);
Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False);
Assert.That(buckleSystem.TryUnbuckle(human, human), Is.False);
Assert.That(buckle.Buckled);
#pragma warning restore NUnit2045
});
@@ -149,7 +146,6 @@ namespace Content.IntegrationTests.Tests.Buckle
// Unbuckle, strap
Assert.That(strap.BuckledEntities, Is.Empty);
Assert.That(strap.OccupiedSize, Is.Zero);
});
#pragma warning disable NUnit2045 // Interdependent asserts.
@@ -160,9 +156,9 @@ namespace Content.IntegrationTests.Tests.Buckle
// On cooldown
Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False);
Assert.That(buckle.Buckled);
Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False);
Assert.That(buckleSystem.TryUnbuckle(human, human), Is.False);
Assert.That(buckle.Buckled);
Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False);
Assert.That(buckleSystem.TryUnbuckle(human, human), Is.False);
Assert.That(buckle.Buckled);
#pragma warning restore NUnit2045
});
@@ -189,7 +185,6 @@ namespace Content.IntegrationTests.Tests.Buckle
#pragma warning disable NUnit2045 // Interdependent asserts.
Assert.That(buckleSystem.TryBuckle(human, human, chair, buckleComp: buckle), Is.False);
Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False);
Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False);
#pragma warning restore NUnit2045
// Move near the chair
@@ -202,12 +197,10 @@ namespace Content.IntegrationTests.Tests.Buckle
Assert.That(buckle.Buckled);
Assert.That(buckleSystem.TryUnbuckle(human, human, buckleComp: buckle), Is.False);
Assert.That(buckle.Buckled);
Assert.That(buckleSystem.ToggleBuckle(human, human, chair, buckle: buckle), Is.False);
Assert.That(buckle.Buckled);
#pragma warning restore NUnit2045
// Force unbuckle
Assert.That(buckleSystem.TryUnbuckle(human, human, true, buckleComp: buckle));
buckleSystem.Unbuckle(human, human);
Assert.Multiple(() =>
{
Assert.That(buckle.Buckled, Is.False);
@@ -311,7 +304,7 @@ namespace Content.IntegrationTests.Tests.Buckle
// Break our guy's kneecaps
foreach (var leg in legs)
{
xformSystem.DetachParentToNull(leg.Id, entityManager.GetComponent<TransformComponent>(leg.Id));
entityManager.DeleteEntity(leg.Id);
}
});
@@ -328,7 +321,8 @@ namespace Content.IntegrationTests.Tests.Buckle
Assert.That(hand.HeldEntity, Is.Null);
}
buckleSystem.TryUnbuckle(human, human, true, buckleComp: buckle);
buckleSystem.Unbuckle(human, human);
Assert.That(buckle.Buckled, Is.False);
});
await pair.CleanReturnAsync();

View File

@@ -1,5 +1,6 @@
#nullable enable
using Content.IntegrationTests.Tests.Interaction;
using Content.IntegrationTests.Tests.Movement;
using Robust.Shared.Maths;
using ClimbingComponent = Content.Shared.Climbing.Components.ClimbingComponent;
using ClimbSystem = Content.Shared.Climbing.Systems.ClimbSystem;

View File

@@ -59,11 +59,6 @@ public sealed class CraftingTests : InteractionTest
await AssertEntityLookup((Rod, 2), (Cable, 7), (ShardGlass, 2), (Spear, 1));
}
// The following is wrapped in an if DEBUG. This is because of cursed state handling bugs. Tests don't (de)serialize
// net messages and just copy objects by reference. This means that the server will directly modify cached server
// states on the client's end. Crude fix at the moment is to used modified state handling while in debug mode
// Otherwise, this test cannot work.
#if DEBUG
/// <summary>
/// Cancel crafting a complex recipe.
/// </summary>
@@ -93,28 +88,22 @@ public sealed class CraftingTests : InteractionTest
await RunTicks(1);
// DoAfter is in progress. Entity not spawned, stacks have been split and someingredients are in a container.
Assert.Multiple(async () =>
{
Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1));
Assert.That(sys.IsEntityInContainer(shard), Is.True);
Assert.That(sys.IsEntityInContainer(rods), Is.False);
Assert.That(sys.IsEntityInContainer(wires), Is.False);
Assert.That(rodStack, Has.Count.EqualTo(8));
Assert.That(wireStack, Has.Count.EqualTo(7));
Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1));
Assert.That(sys.IsEntityInContainer(shard), Is.True);
Assert.That(sys.IsEntityInContainer(rods), Is.False);
Assert.That(sys.IsEntityInContainer(wires), Is.False);
Assert.That(rodStack, Has.Count.EqualTo(8));
Assert.That(wireStack, Has.Count.EqualTo(7));
await FindEntity(Spear, shouldSucceed: false);
});
await FindEntity(Spear, shouldSucceed: false);
// Cancel the DoAfter. Should drop ingredients to the floor.
await CancelDoAfters();
Assert.Multiple(async () =>
{
Assert.That(sys.IsEntityInContainer(rods), Is.False);
Assert.That(sys.IsEntityInContainer(wires), Is.False);
Assert.That(sys.IsEntityInContainer(shard), Is.False);
await FindEntity(Spear, shouldSucceed: false);
await AssertEntityLookup((Rod, 10), (Cable, 10), (ShardGlass, 1));
});
Assert.That(sys.IsEntityInContainer(rods), Is.False);
Assert.That(sys.IsEntityInContainer(wires), Is.False);
Assert.That(sys.IsEntityInContainer(shard), Is.False);
await FindEntity(Spear, shouldSucceed: false);
await AssertEntityLookup((Rod, 10), (Cable, 10), (ShardGlass, 1));
// Re-attempt the do-after
#pragma warning disable CS4014 // Legacy construction code uses DoAfterAwait. See above.
@@ -123,24 +112,17 @@ public sealed class CraftingTests : InteractionTest
await RunTicks(1);
// DoAfter is in progress. Entity not spawned, ingredients are in a container.
Assert.Multiple(async () =>
{
Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1));
Assert.That(sys.IsEntityInContainer(shard), Is.True);
await FindEntity(Spear, shouldSucceed: false);
});
Assert.That(ActiveDoAfters.Count(), Is.EqualTo(1));
Assert.That(sys.IsEntityInContainer(shard), Is.True);
await FindEntity(Spear, shouldSucceed: false);
// Finish the DoAfter
await AwaitDoAfters();
// Spear has been crafted. Rods and wires are no longer contained. Glass has been consumed.
Assert.Multiple(async () =>
{
await FindEntity(Spear);
Assert.That(sys.IsEntityInContainer(rods), Is.False);
Assert.That(sys.IsEntityInContainer(wires), Is.False);
Assert.That(SEntMan.Deleted(shard));
});
await FindEntity(Spear);
Assert.That(sys.IsEntityInContainer(rods), Is.False);
Assert.That(sys.IsEntityInContainer(wires), Is.False);
Assert.That(SEntMan.Deleted(shard));
}
#endif
}

View File

@@ -84,8 +84,9 @@ public abstract partial class InteractionTest
/// <summary>
/// Spawn an entity entity and set it as the target.
/// </summary>
[MemberNotNull(nameof(Target))]
protected async Task SpawnTarget(string prototype)
[MemberNotNull(nameof(Target), nameof(STarget), nameof(CTarget))]
#pragma warning disable CS8774 // Member must have a non-null value when exiting.
protected async Task<NetEntity> SpawnTarget(string prototype)
{
Target = NetEntity.Invalid;
await Server.WaitPost(() =>
@@ -95,7 +96,9 @@ public abstract partial class InteractionTest
await RunTicks(5);
AssertPrototype(prototype);
return Target!.Value;
}
#pragma warning restore CS8774 // Member must have a non-null value when exiting.
/// <summary>
/// Spawn an entity in preparation for deconstruction
@@ -1170,14 +1173,17 @@ public abstract partial class InteractionTest
#region Inputs
/// <summary>
/// Make the client press and then release a key. This assumes the key is currently released.
/// This will default to using the <see cref="Target"/> entity and <see cref="TargetCoords"/> coordinates.
/// </summary>
protected async Task PressKey(
BoundKeyFunction key,
int ticks = 1,
NetCoordinates? coordinates = null,
NetEntity cursorEntity = default)
NetEntity? cursorEntity = null)
{
await SetKey(key, BoundKeyState.Down, coordinates, cursorEntity);
await RunTicks(ticks);
@@ -1186,15 +1192,17 @@ public abstract partial class InteractionTest
}
/// <summary>
/// Make the client press or release a key
/// Make the client press or release a key.
/// This will default to using the <see cref="Target"/> entity and <see cref="TargetCoords"/> coordinates.
/// </summary>
protected async Task SetKey(
BoundKeyFunction key,
BoundKeyState state,
NetCoordinates? coordinates = null,
NetEntity cursorEntity = default)
NetEntity? cursorEntity = null)
{
var coords = coordinates ?? TargetCoords;
var target = cursorEntity ?? Target ?? default;
ScreenCoordinates screen = default;
var funcId = InputManager.NetworkBindMap.KeyFunctionID(key);
@@ -1203,7 +1211,7 @@ public abstract partial class InteractionTest
State = state,
Coordinates = CEntMan.GetCoordinates(coords),
ScreenCoordinates = screen,
Uid = CEntMan.GetEntity(cursorEntity),
Uid = CEntMan.GetEntity(target),
};
await Client.WaitPost(() => InputSystem.HandleInputCommand(ClientSession, key, message));

View File

@@ -84,6 +84,7 @@ public abstract partial class InteractionTest
protected NetEntity? Target;
protected EntityUid? STarget => ToServer(Target);
protected EntityUid? CTarget => ToClient(Target);
/// <summary>
@@ -128,7 +129,6 @@ public abstract partial class InteractionTest
public float TickPeriod => (float) STiming.TickPeriod.TotalSeconds;
// Simple mob that has one hand and can perform misc interactions.
[TestPrototypes]
private const string TestPrototypes = @"
@@ -142,6 +142,8 @@ public abstract partial class InteractionTest
- type: ComplexInteraction
- type: MindContainer
- type: Stripping
- type: Puller
- type: Physics
- type: Tag
tags:
- CanPilot
@@ -207,8 +209,8 @@ public abstract partial class InteractionTest
SEntMan.System<SharedMindSystem>().WipeMind(ServerSession.ContentData()?.Mind);
old = cPlayerMan.LocalEntity;
Player = SEntMan.GetNetEntity(SEntMan.SpawnEntity(PlayerPrototype, SEntMan.GetCoordinates(PlayerCoords)));
SPlayer = SEntMan.GetEntity(Player);
SPlayer = SEntMan.SpawnEntity(PlayerPrototype, SEntMan.GetCoordinates(PlayerCoords));
Player = SEntMan.GetNetEntity(SPlayer);
Server.PlayerMan.SetAttachedEntity(ServerSession, SPlayer);
Hands = SEntMan.GetComponent<HandsComponent>(SPlayer);
DoAfters = SEntMan.GetComponent<DoAfterComponent>(SPlayer);

View File

@@ -0,0 +1,63 @@
using Content.Shared.Alert;
using Content.Shared.Buckle.Components;
using Robust.Shared.Maths;
namespace Content.IntegrationTests.Tests.Movement;
public sealed class BuckleMovementTest : MovementTest
{
// Check that interacting with a chair straps you to it and prevents movement.
[Test]
public async Task ChairTest()
{
await SpawnTarget("Chair");
var cAlert = Client.System<AlertsSystem>();
var sAlert = Server.System<AlertsSystem>();
var buckle = Comp<BuckleComponent>(Player);
var strap = Comp<StrapComponent>(Target);
#pragma warning disable RA0002
buckle.Delay = TimeSpan.Zero;
#pragma warning restore RA0002
// Initially not buckled to the chair, and standing off to the side
Assert.That(Delta(), Is.InRange(0.9f, 1.1f));
Assert.That(buckle.Buckled, Is.False);
Assert.That(buckle.BuckledTo, Is.Null);
Assert.That(strap.BuckledEntities, Is.Empty);
Assert.That(cAlert.IsShowingAlert(CPlayer, strap.BuckledAlertType), Is.False);
Assert.That(sAlert.IsShowingAlert(SPlayer, strap.BuckledAlertType), Is.False);
// Interact results in being buckled to the chair
await Interact();
Assert.That(Delta(), Is.InRange(-0.01f, 0.01f));
Assert.That(buckle.Buckled, Is.True);
Assert.That(buckle.BuckledTo, Is.EqualTo(STarget));
Assert.That(strap.BuckledEntities, Is.EquivalentTo(new[]{SPlayer}));
Assert.That(cAlert.IsShowingAlert(CPlayer, strap.BuckledAlertType), Is.True);
Assert.That(sAlert.IsShowingAlert(SPlayer, strap.BuckledAlertType), Is.True);
// Attempting to walk away does nothing
await Move(DirectionFlag.East, 1);
Assert.That(Delta(), Is.InRange(-0.01f, 0.01f));
Assert.That(buckle.Buckled, Is.True);
Assert.That(buckle.BuckledTo, Is.EqualTo(STarget));
Assert.That(strap.BuckledEntities, Is.EquivalentTo(new[]{SPlayer}));
Assert.That(cAlert.IsShowingAlert(CPlayer, strap.BuckledAlertType), Is.True);
Assert.That(sAlert.IsShowingAlert(SPlayer, strap.BuckledAlertType), Is.True);
// Interacting again will unbuckle the player
await Interact();
Assert.That(Delta(), Is.InRange(-0.5f, 0.5f));
Assert.That(buckle.Buckled, Is.False);
Assert.That(buckle.BuckledTo, Is.Null);
Assert.That(strap.BuckledEntities, Is.Empty);
Assert.That(cAlert.IsShowingAlert(CPlayer, strap.BuckledAlertType), Is.False);
Assert.That(sAlert.IsShowingAlert(SPlayer, strap.BuckledAlertType), Is.False);
// And now they can move away
await Move(DirectionFlag.SouthEast, 1);
Assert.That(Delta(), Is.LessThan(-1));
}
}

View File

@@ -1,8 +1,9 @@
#nullable enable
using System.Numerics;
using Content.IntegrationTests.Tests.Interaction;
using Robust.Shared.GameObjects;
namespace Content.IntegrationTests.Tests.Interaction;
namespace Content.IntegrationTests.Tests.Movement;
/// <summary>
/// This is a variation of <see cref="InteractionTest"/> that sets up the player with a normal human entity and a simple

View File

@@ -0,0 +1,73 @@
#nullable enable
using Content.Shared.Alert;
using Content.Shared.Input;
using Content.Shared.Movement.Pulling.Components;
using Robust.Shared.Maths;
namespace Content.IntegrationTests.Tests.Movement;
public sealed class PullingTest : MovementTest
{
protected override int Tiles => 4;
[Test]
public async Task PullTest()
{
var cAlert = Client.System<AlertsSystem>();
var sAlert = Server.System<AlertsSystem>();
await SpawnTarget("MobHuman");
var puller = Comp<PullerComponent>(Player);
var pullable = Comp<PullableComponent>(Target);
// Player is initially to the left of the target and not pulling anything
Assert.That(Delta(), Is.InRange(0.9f, 1.1f));
Assert.That(puller.Pulling, Is.Null);
Assert.That(pullable.Puller, Is.Null);
Assert.That(pullable.BeingPulled, Is.False);
Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.False);
Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.False);
// Start pulling
await PressKey(ContentKeyFunctions.TryPullObject);
await RunTicks(5);
Assert.That(puller.Pulling, Is.EqualTo(STarget));
Assert.That(pullable.Puller, Is.EqualTo(SPlayer));
Assert.That(pullable.BeingPulled, Is.True);
Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.True);
Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.True);
// Move to the left and check that the target moves with the player and is still being pulled.
await Move(DirectionFlag.West, 1);
Assert.That(Delta(), Is.InRange(0.9f, 1.3f));
Assert.That(puller.Pulling, Is.EqualTo(STarget));
Assert.That(pullable.Puller, Is.EqualTo(SPlayer));
Assert.That(pullable.BeingPulled, Is.True);
Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.True);
Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.True);
// Move in the other direction
await Move(DirectionFlag.East, 2);
Assert.That(Delta(), Is.InRange(-1.3f, -0.9f));
Assert.That(puller.Pulling, Is.EqualTo(STarget));
Assert.That(pullable.Puller, Is.EqualTo(SPlayer));
Assert.That(pullable.BeingPulled, Is.True);
Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.True);
Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.True);
// Stop pulling
await PressKey(ContentKeyFunctions.ReleasePulledObject);
await RunTicks(5);
Assert.That(Delta(), Is.InRange(-1.3f, -0.9f));
Assert.That(puller.Pulling, Is.Null);
Assert.That(pullable.Puller, Is.Null);
Assert.That(pullable.BeingPulled, Is.False);
Assert.That(cAlert.IsShowingAlert(CPlayer, puller.PullingAlert), Is.False);
Assert.That(sAlert.IsShowingAlert(SPlayer, puller.PullingAlert), Is.False);
// Move back to the left and ensure the target is no longer following us.
await Move(DirectionFlag.West, 2);
Assert.That(Delta(), Is.GreaterThan(2f));
}
}

View File

@@ -8,7 +8,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.Input;
using Robust.Shared.Maths;
namespace Content.IntegrationTests.Tests.Slipping;
namespace Content.IntegrationTests.Tests.Movement;
public sealed class SlippingTest : MovementTest
{
@@ -36,18 +36,14 @@ public sealed class SlippingTest : MovementTest
Assert.That(modifier, Is.EqualTo(1), "Player is not moving at full speed.");
// Player is to the left of the banana peel and has not slipped.
#pragma warning disable NUnit2045
Assert.That(Delta(), Is.GreaterThan(0.5f));
Assert.That(sys.Slipped, Does.Not.Contain(SEntMan.GetEntity(Player)));
#pragma warning restore NUnit2045
// Walking over the banana slowly does not trigger a slip.
await SetKey(EngineKeyFunctions.Walk, BoundKeyState.Down);
await Move(DirectionFlag.East, 1f);
#pragma warning disable NUnit2045
Assert.That(Delta(), Is.LessThan(0.5f));
Assert.That(sys.Slipped, Does.Not.Contain(SEntMan.GetEntity(Player)));
#pragma warning restore NUnit2045
AssertComp<KnockedDownComponent>(false, Player);
// Moving at normal speeds does trigger a slip.

View File

@@ -1,15 +1,52 @@
using System.Collections.Generic;
using Content.Server.Station.Systems;
using Content.Shared.Inventory;
using Content.Shared.Preferences;
using Content.Shared.Preferences.Loadouts;
using Content.Shared.Roles.Jobs;
using Robust.Shared.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Preferences;
[TestFixture]
[Ignore("HumanoidAppearance crashes upon loading default profiles.")]
public sealed class LoadoutTests
{
[TestPrototypes]
private const string Prototypes = @"
- type: playTimeTracker
id: PlayTimeLoadoutTester
- type: loadout
id: TestJumpsuit
equipment: TestJumpsuit
- type: startingGear
id: TestJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitColorGrey
- type: loadoutGroup
id: LoadoutTesterJumpsuit
name: generic-unknown
loadouts:
- TestJumpsuit
- type: roleLoadout
id: JobLoadoutTester
groups:
- LoadoutTesterJumpsuit
- type: job
id: LoadoutTester
playTimeTracker: PlayTimeLoadoutTester
";
private readonly Dictionary<string, EntProtoId> _expectedEquipment = new()
{
["jumpsuit"] = "ClothingUniformJumpsuitColorGrey"
};
/// <summary>
/// Checks that an empty loadout still spawns with default gear and not naked.
/// </summary>
@@ -26,18 +63,38 @@ public sealed class LoadoutTests
// Check that an empty role loadout spawns gear
var stationSystem = entManager.System<StationSpawningSystem>();
var inventorySystem = entManager.System<InventorySystem>();
var testMap = await pair.CreateTestMap();
// That's right I can't even spawn a dummy profile without station spawning / humanoidappearance code crashing.
var profile = new HumanoidCharacterProfile();
profile.SetLoadout(new RoleLoadout("TestRoleLoadout"));
stationSystem.SpawnPlayerMob(testMap.GridCoords, job: new JobComponent()
await server.WaitAssertion(() =>
{
// Sue me, there's so much involved in setting up jobs
Prototype = "CargoTechnician"
}, profile, station: null);
var profile = new HumanoidCharacterProfile();
profile.SetLoadout(new RoleLoadout("LoadoutTester"));
var tester = stationSystem.SpawnPlayerMob(testMap.GridCoords, job: new JobComponent()
{
Prototype = "LoadoutTester"
}, profile, station: null);
var slotQuery = inventorySystem.GetSlotEnumerator(tester);
var checkedCount = 0;
while (slotQuery.NextItem(out var item, out var slot))
{
// Make sure the slot is valid
Assert.That(_expectedEquipment.TryGetValue(slot.Name, out var expectedItem), $"Spawned item in unexpected slot: {slot.Name}");
// Make sure that the item is the right one
var meta = entManager.GetComponent<MetaDataComponent>(item);
Assert.That(meta.EntityPrototype.ID, Is.EqualTo(expectedItem.Id), $"Spawned wrong item in slot {slot.Name}!");
checkedCount++;
}
// Make sure the number of items is the same
Assert.That(checkedCount, Is.EqualTo(_expectedEquipment.Count), "Number of items does not match expected!");
entManager.DeleteEntity(tester);
});
await pair.CleanReturnAsync();
}

View File

@@ -694,6 +694,14 @@ namespace Content.Server.Database
/// Intended use is for users with shared connections. This should not be used as an alternative to <see cref="Datacenter"/>.
/// </remarks>
IP = 1 << 1,
/// <summary>
/// Ban is an IP range that is only applied for first time joins.
/// </summary>
/// <remarks>
/// Intended for use with residential IP ranges that are often used maliciously.
/// </remarks>
BlacklistedRange = 1 << 2,
// @formatter:on
}

View File

@@ -21,6 +21,9 @@ public sealed partial class AdminVerbSystem
[ValidatePrototypeId<EntityPrototype>]
private const string DefaultTraitorRule = "Traitor";
[ValidatePrototypeId<EntityPrototype>]
private const string DefaultInitialInfectedRule = "Zombie";
[ValidatePrototypeId<EntityPrototype>]
private const string DefaultNukeOpRule = "LoneOpsSpawn";
@@ -63,6 +66,20 @@ public sealed partial class AdminVerbSystem
};
args.Verbs.Add(traitor);
Verb initialInfected = new()
{
Text = Loc.GetString("admin-verb-text-make-initial-infected"),
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "InitialInfected"),
Act = () =>
{
_antag.ForceMakeAntag<ZombieRuleComponent>(targetPlayer, DefaultInitialInfectedRule);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-initial-infected"),
};
args.Verbs.Add(initialInfected);
Verb zombie = new()
{
Text = Loc.GetString("admin-verb-text-make-zombie"),

View File

@@ -9,6 +9,7 @@ using Content.Server.Administration.Managers;
using Content.Server.Afk;
using Content.Server.Discord;
using Content.Server.GameTicking;
using Content.Server.Players.RateLimiting;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Mind;
@@ -27,6 +28,8 @@ namespace Content.Server.Administration.Systems
[UsedImplicitly]
public sealed partial class BwoinkSystem : SharedBwoinkSystem
{
private const string RateLimitKey = "AdminHelp";
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IAdminManager _adminManager = default!;
[Dependency] private readonly IConfigurationManager _config = default!;
@@ -35,6 +38,7 @@ namespace Content.Server.Administration.Systems
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly SharedMindSystem _minds = default!;
[Dependency] private readonly IAfkManager _afkManager = default!;
[Dependency] private readonly PlayerRateLimitManager _rateLimit = default!;
[GeneratedRegex(@"^https://discord\.com/api/webhooks/(\d+)/((?!.*/).*)$")]
private static partial Regex DiscordRegex();
@@ -80,6 +84,22 @@ namespace Content.Server.Administration.Systems
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnGameRunLevelChanged);
SubscribeNetworkEvent<BwoinkClientTypingUpdated>(OnClientTypingUpdated);
_rateLimit.Register(
RateLimitKey,
new RateLimitRegistration
{
CVarLimitPeriodLength = CCVars.AhelpRateLimitPeriod,
CVarLimitCount = CCVars.AhelpRateLimitCount,
PlayerLimitedAction = PlayerRateLimitedAction
});
}
private void PlayerRateLimitedAction(ICommonSession obj)
{
RaiseNetworkEvent(
new BwoinkTextMessage(obj.UserId, default, Loc.GetString("bwoink-system-rate-limited"), playSound: false),
obj.Channel);
}
private void OnOverrideChanged(string obj)
@@ -395,6 +415,9 @@ namespace Content.Server.Administration.Systems
return;
}
if (_rateLimit.CountAction(eventArgs.SenderSession, RateLimitKey) != RateLimitStatus.Allowed)
return;
var escapedText = FormattedMessage.EscapeText(message.Text);
string bwoinkText;

View File

@@ -0,0 +1,7 @@
namespace Content.Server.Antag.Components;
[RegisterComponent]
public partial class AntagImmuneComponent : Component
{
}

View File

@@ -11,6 +11,7 @@ using Content.Shared.Tag;
using Robust.Server.Audio;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.Atmos.Monitor.Systems;
@@ -86,7 +87,7 @@ public sealed class AtmosAlarmableSystem : EntitySystem
return;
if (!args.Data.TryGetValue(DeviceNetworkConstants.Command, out string? cmd)
|| !args.Data.TryGetValue(AlertSource, out HashSet<string>? sourceTags))
|| !args.Data.TryGetValue(AlertSource, out HashSet<ProtoId<TagPrototype>>? sourceTags))
{
return;
}

View File

@@ -11,6 +11,7 @@ using Content.Shared.Damage;
using Content.Shared.Emag.Systems;
using Content.Shared.Mobs.Systems;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Bed
{
@@ -26,25 +27,29 @@ namespace Content.Server.Bed
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HealOnBuckleComponent, BuckleChangeEvent>(ManageUpdateList);
SubscribeLocalEvent<StasisBedComponent, BuckleChangeEvent>(OnBuckleChange);
SubscribeLocalEvent<HealOnBuckleComponent, StrappedEvent>(OnStrapped);
SubscribeLocalEvent<HealOnBuckleComponent, UnstrappedEvent>(OnUnstrapped);
SubscribeLocalEvent<StasisBedComponent, StrappedEvent>(OnStasisStrapped);
SubscribeLocalEvent<StasisBedComponent, UnstrappedEvent>(OnStasisUnstrapped);
SubscribeLocalEvent<StasisBedComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<StasisBedComponent, GotEmaggedEvent>(OnEmagged);
}
private void ManageUpdateList(EntityUid uid, HealOnBuckleComponent component, ref BuckleChangeEvent args)
private void OnStrapped(Entity<HealOnBuckleComponent> bed, ref StrappedEvent args)
{
if (args.Buckling)
{
AddComp<HealOnBuckleHealingComponent>(uid);
component.NextHealTime = _timing.CurTime + TimeSpan.FromSeconds(component.HealTime);
_actionsSystem.AddAction(args.BuckledEntity, ref component.SleepAction, SleepingSystem.SleepActionId, uid);
return;
}
EnsureComp<HealOnBuckleHealingComponent>(bed);
bed.Comp.NextHealTime = _timing.CurTime + TimeSpan.FromSeconds(bed.Comp.HealTime);
_actionsSystem.AddAction(args.Buckle, ref bed.Comp.SleepAction, SleepingSystem.SleepActionId, bed);
_actionsSystem.RemoveAction(args.BuckledEntity, component.SleepAction);
_sleepingSystem.TryWaking(args.BuckledEntity);
RemComp<HealOnBuckleHealingComponent>(uid);
// Single action entity, cannot strap multiple entities to the same bed.
DebugTools.AssertEqual(args.Strap.Comp.BuckledEntities.Count, 1);
}
private void OnUnstrapped(Entity<HealOnBuckleComponent> bed, ref UnstrappedEvent args)
{
_actionsSystem.RemoveAction(args.Buckle, bed.Comp.SleepAction);
_sleepingSystem.TryWaking(args.Buckle.Owner);
RemComp<HealOnBuckleHealingComponent>(bed);
}
public override void Update(float frameTime)
@@ -82,18 +87,22 @@ namespace Content.Server.Bed
_appearance.SetData(uid, StasisBedVisuals.IsOn, isOn);
}
private void OnBuckleChange(EntityUid uid, StasisBedComponent component, ref BuckleChangeEvent args)
private void OnStasisStrapped(Entity<StasisBedComponent> bed, ref StrappedEvent args)
{
// In testing this also received an unbuckle event when the bed is destroyed
// So don't worry about that
if (!HasComp<BodyComponent>(args.BuckledEntity))
if (!HasComp<BodyComponent>(args.Buckle) || !this.IsPowered(bed, EntityManager))
return;
if (!this.IsPowered(uid, EntityManager))
var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.Buckle, bed.Comp.Multiplier, true);
RaiseLocalEvent(args.Buckle, ref metabolicEvent);
}
private void OnStasisUnstrapped(Entity<StasisBedComponent> bed, ref UnstrappedEvent args)
{
if (!HasComp<BodyComponent>(args.Buckle) || !this.IsPowered(bed, EntityManager))
return;
var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.BuckledEntity, component.Multiplier, args.Buckling);
RaiseLocalEvent(args.BuckledEntity, ref metabolicEvent);
var metabolicEvent = new ApplyMetabolicMultiplierEvent(args.Buckle, bed.Comp.Multiplier, false);
RaiseLocalEvent(args.Buckle, ref metabolicEvent);
}
private void OnPowerChanged(EntityUid uid, StasisBedComponent component, ref PowerChangedEvent args)

View File

@@ -5,19 +5,26 @@ namespace Content.Server.Bed.Components
[RegisterComponent]
public sealed partial class HealOnBuckleComponent : Component
{
[DataField("damage", required: true)]
[ViewVariables(VVAccess.ReadWrite)]
/// <summary>
/// Damage to apply to entities that are strapped to this entity.
/// </summary>
[DataField(required: true)]
public DamageSpecifier Damage = default!;
[DataField("healTime", required: false)]
[ViewVariables(VVAccess.ReadWrite)]
public float HealTime = 1f; // How often the bed applies the damage
/// <summary>
/// How frequently the damage should be applied, in seconds.
/// </summary>
[DataField(required: false)]
public float HealTime = 1f;
[DataField("sleepMultiplier")]
/// <summary>
/// Damage multiplier that gets applied if the entity is sleeping.
/// </summary>
[DataField]
public float SleepMultiplier = 3f;
public TimeSpan NextHealTime = TimeSpan.Zero; //Next heal
[DataField("sleepAction")] public EntityUid? SleepAction;
[DataField] public EntityUid? SleepAction;
}
}

View File

@@ -1,5 +1,6 @@
namespace Content.Server.Bed.Components
{
// TODO rename this component
[RegisterComponent]
public sealed partial class HealOnBuckleHealingComponent : Component
{}

View File

@@ -6,7 +6,8 @@ namespace Content.Server.Bed.Components
/// <summary>
/// What the metabolic update rate will be multiplied by (higher = slower metabolism)
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[ViewVariables(VVAccess.ReadOnly)] // Writing is is not supported. ApplyMetabolicMultiplierEvent needs to be refactored first
[DataField]
public float Multiplier = 10f;
}
}

View File

@@ -268,6 +268,9 @@ public sealed class BloodstreamSystem : EntitySystem
Entity<BloodstreamComponent> ent,
ref ApplyMetabolicMultiplierEvent args)
{
// TODO REFACTOR THIS
// This will slowly drift over time due to floating point errors.
// Instead, raise an event with the base rates and allow modifiers to get applied to it.
if (args.Apply)
{
ent.Comp.UpdateInterval *= args.Multiplier;

View File

@@ -67,6 +67,9 @@ namespace Content.Server.Body.Systems
Entity<MetabolizerComponent> ent,
ref ApplyMetabolicMultiplierEvent args)
{
// TODO REFACTOR THIS
// This will slowly drift over time due to floating point errors.
// Instead, raise an event with the base rates and allow modifiers to get applied to it.
if (args.Apply)
{
ent.Comp.UpdateInterval *= args.Multiplier;
@@ -229,6 +232,9 @@ namespace Content.Server.Body.Systems
}
}
// TODO REFACTOR THIS
// This will cause rates to slowly drift over time due to floating point errors.
// Instead, the system that raised this should trigger an update and subscribe to get-modifier events.
[ByRefEvent]
public readonly record struct ApplyMetabolicMultiplierEvent(
EntityUid Uid,

View File

@@ -326,6 +326,9 @@ public sealed class RespiratorSystem : EntitySystem
Entity<RespiratorComponent> ent,
ref ApplyMetabolicMultiplierEvent args)
{
// TODO REFACTOR THIS
// This will slowly drift over time due to floating point errors.
// Instead, raise an event with the base rates and allow modifiers to get applied to it.
if (args.Apply)
{
ent.Comp.UpdateInterval *= args.Multiplier;

View File

@@ -1,4 +1,4 @@
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.PDA;
@@ -164,6 +164,15 @@ public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem
if (!Resolve(loaderUid, ref loader))
return false;
if (!TryComp(cartridgeUid, out CartridgeComponent? loadedCartridge))
return false;
foreach (var program in GetInstalled(loaderUid))
{
if (TryComp(program, out CartridgeComponent? installedCartridge) && installedCartridge.ProgramName == loadedCartridge.ProgramName)
return false;
}
//This will eventually be replaced by serializing and deserializing the cartridge to copy it when something needs
//the data on the cartridge to carry over when installing
@@ -191,7 +200,6 @@ public sealed class CartridgeLoaderSystem : SharedCartridgeLoaderSystem
if (container.Count >= loader.DiskSpace)
return false;
// TODO cancel duplicate program installations
var ev = new ProgramInstallationAttempt(loaderUid, prototype);
RaiseLocalEvent(ref ev);

View File

@@ -1,84 +1,41 @@
using System.Runtime.InteropServices;
using Content.Server.Players.RateLimiting;
using Content.Shared.CCVar;
using Content.Shared.Database;
using Robust.Shared.Enums;
using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Server.Chat.Managers;
internal sealed partial class ChatManager
{
private readonly Dictionary<ICommonSession, RateLimitDatum> _rateLimitData = new();
private const string RateLimitKey = "Chat";
public bool HandleRateLimit(ICommonSession player)
private void RegisterRateLimits()
{
ref var datum = ref CollectionsMarshal.GetValueRefOrAddDefault(_rateLimitData, player, out _);
var time = _gameTiming.RealTime;
if (datum.CountExpires < time)
{
// Period expired, reset it.
var periodLength = _configurationManager.GetCVar(CCVars.ChatRateLimitPeriod);
datum.CountExpires = time + TimeSpan.FromSeconds(periodLength);
datum.Count = 0;
datum.Announced = false;
}
var maxCount = _configurationManager.GetCVar(CCVars.ChatRateLimitCount);
datum.Count += 1;
if (datum.Count <= maxCount)
return true;
// Breached rate limits, inform admins if configured.
if (_configurationManager.GetCVar(CCVars.ChatRateLimitAnnounceAdmins))
{
if (datum.NextAdminAnnounce < time)
_rateLimitManager.Register(RateLimitKey,
new RateLimitRegistration
{
SendAdminAlert(Loc.GetString("chat-manager-rate-limit-admin-announcement", ("player", player.Name)));
var delay = _configurationManager.GetCVar(CCVars.ChatRateLimitAnnounceAdminsDelay);
datum.NextAdminAnnounce = time + TimeSpan.FromSeconds(delay);
}
}
if (!datum.Announced)
{
DispatchServerMessage(player, Loc.GetString("chat-manager-rate-limited"), suppressLog: true);
_adminLogger.Add(LogType.ChatRateLimited, LogImpact.Medium, $"Player {player} breached chat rate limits");
datum.Announced = true;
}
return false;
CVarLimitPeriodLength = CCVars.ChatRateLimitPeriod,
CVarLimitCount = CCVars.ChatRateLimitCount,
CVarAdminAnnounceDelay = CCVars.ChatRateLimitAnnounceAdminsDelay,
PlayerLimitedAction = RateLimitPlayerLimited,
AdminAnnounceAction = RateLimitAlertAdmins,
AdminLogType = LogType.ChatRateLimited,
});
}
private void PlayerStatusChanged(object? sender, SessionStatusEventArgs e)
private void RateLimitPlayerLimited(ICommonSession player)
{
if (e.NewStatus == SessionStatus.Disconnected)
_rateLimitData.Remove(e.Session);
DispatchServerMessage(player, Loc.GetString("chat-manager-rate-limited"), suppressLog: true);
}
private struct RateLimitDatum
private void RateLimitAlertAdmins(ICommonSession player)
{
/// <summary>
/// Time stamp (relative to <see cref="IGameTiming.RealTime"/>) this rate limit period will expire at.
/// </summary>
public TimeSpan CountExpires;
if (_configurationManager.GetCVar(CCVars.ChatRateLimitAnnounceAdmins))
SendAdminAlert(Loc.GetString("chat-manager-rate-limit-admin-announcement", ("player", player.Name)));
}
/// <summary>
/// How many messages have been sent in the current rate limit period.
/// </summary>
public int Count;
/// <summary>
/// Have we announced to the player that they've been blocked in this rate limit period?
/// </summary>
public bool Announced;
/// <summary>
/// Time stamp (relative to <see cref="IGameTiming.RealTime"/>) of the
/// next time we can send an announcement to admins about rate limit breach.
/// </summary>
public TimeSpan NextAdminAnnounce;
public RateLimitStatus HandleRateLimit(ICommonSession player)
{
return _rateLimitManager.CountAction(player, RateLimitKey);
}
}

View File

@@ -6,18 +6,17 @@ using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Server.Administration.Systems;
using Content.Server.MoMMI;
using Content.Server.Players.RateLimiting;
using Content.Server.Preferences.Managers;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Mind;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Replays;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Chat.Managers
@@ -44,8 +43,7 @@ namespace Content.Server.Chat.Managers
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly INetConfigurationManager _netConfigManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly PlayerRateLimitManager _rateLimitManager = default!;
private ISharedSponsorsManager? _sponsorsManager; // Corvax-Sponsors
/// <summary>
@@ -67,7 +65,7 @@ namespace Content.Server.Chat.Managers
_configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true);
_configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true);
_playerManager.PlayerStatusChanged += PlayerStatusChanged;
RegisterRateLimits();
}
private void OnOocEnabledChanged(bool val)
@@ -209,7 +207,7 @@ namespace Content.Server.Chat.Managers
/// <param name="type">The type of message.</param>
public void TrySendOOCMessage(ICommonSession player, string message, OOCChatType type)
{
if (!HandleRateLimit(player))
if (HandleRateLimit(player) != RateLimitStatus.Allowed)
return;
// Check if message exceeds the character limit

View File

@@ -1,4 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Players;
using Content.Server.Players.RateLimiting;
using Content.Shared.Administration;
using Content.Shared.Chat;
using Robust.Shared.Network;
@@ -50,6 +52,6 @@ namespace Content.Server.Chat.Managers
/// </summary>
/// <param name="player">The player sending a chat message.</param>
/// <returns>False if the player has violated rate limits and should be blocked from sending further messages.</returns>
bool HandleRateLimit(ICommonSession player);
RateLimitStatus HandleRateLimit(ICommonSession player);
}
}

View File

@@ -6,6 +6,7 @@ using Content.Server.Administration.Managers;
using Content.Server.Chat.Managers;
using Content.Server.Examine;
using Content.Server.GameTicking;
using Content.Server.Players.RateLimiting;
using Content.Server.Speech.Components;
using Content.Server.Speech.EntitySystems;
using Content.Server.Station.Components;
@@ -186,7 +187,7 @@ public sealed partial class ChatSystem : SharedChatSystem
return;
}
if (player != null && !_chatManager.HandleRateLimit(player))
if (player != null && _chatManager.HandleRateLimit(player) != RateLimitStatus.Allowed)
return;
// Sus
@@ -275,7 +276,7 @@ public sealed partial class ChatSystem : SharedChatSystem
if (!CanSendInGame(message, shell, player))
return;
if (player != null && !_chatManager.HandleRateLimit(player))
if (player != null && _chatManager.HandleRateLimit(player) != RateLimitStatus.Allowed)
return;
// It doesn't make any sense for a non-player to send in-game OOC messages, whereas non-players may be sending

View File

@@ -633,6 +633,11 @@ namespace Content.Server.Database
return record == null ? null : MakePlayerRecord(record);
}
protected async Task<bool> PlayerRecordExists(DbGuard db, NetUserId userId)
{
return await db.DbContext.Player.AnyAsync(p => p.UserId == userId);
}
[return: NotNullIfNotNull(nameof(player))]
protected PlayerRecord? MakePlayerRecord(Player? player)
{

View File

@@ -78,7 +78,8 @@ namespace Content.Server.Database
await using var db = await GetDbImpl();
var exempt = await GetBanExemptionCore(db, userId);
var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned: false, exempt)
var newPlayer = userId == null || !await PlayerRecordExists(db, userId.Value);
var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned: false, exempt, newPlayer)
.OrderByDescending(b => b.BanTime);
var ban = await query.FirstOrDefaultAsync();
@@ -98,7 +99,8 @@ namespace Content.Server.Database
await using var db = await GetDbImpl();
var exempt = await GetBanExemptionCore(db, userId);
var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned, exempt);
var newPlayer = !await db.PgDbContext.Player.AnyAsync(p => p.UserId == userId);
var query = MakeBanLookupQuery(address, userId, hwId, db, includeUnbanned, exempt, newPlayer);
var queryBans = await query.ToArrayAsync();
var bans = new List<ServerBanDef>(queryBans.Length);
@@ -122,7 +124,8 @@ namespace Content.Server.Database
ImmutableArray<byte>? hwId,
DbGuardImpl db,
bool includeUnbanned,
ServerBanExemptFlags? exemptFlags)
ServerBanExemptFlags? exemptFlags,
bool newPlayer)
{
DebugTools.Assert(!(address == null && userId == null && hwId == null));
@@ -141,7 +144,9 @@ namespace Content.Server.Database
{
var newQ = db.PgDbContext.Ban
.Include(p => p.Unban)
.Where(b => b.Address != null && EF.Functions.ContainsOrEqual(b.Address.Value, address));
.Where(b => b.Address != null
&& EF.Functions.ContainsOrEqual(b.Address.Value, address)
&& !(b.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) && !newPlayer));
query = query == null ? newQ : query.Union(newQ);
}
@@ -167,6 +172,9 @@ namespace Content.Server.Database
if (exemptFlags is { } exempt)
{
if (exempt != ServerBanExemptFlags.None)
exempt |= ServerBanExemptFlags.BlacklistedRange; // Any kind of exemption should bypass BlacklistedRange
query = query.Where(b => (b.ExemptFlags & exempt) == 0);
}

View File

@@ -86,11 +86,13 @@ namespace Content.Server.Database
var exempt = await GetBanExemptionCore(db, userId);
var newPlayer = userId == null || !await PlayerRecordExists(db, userId.Value);
// SQLite can't do the net masking stuff we need to match IP address ranges.
// So just pull down the whole list into memory.
var bans = await GetAllBans(db.SqliteDbContext, includeUnbanned: false, exempt);
return bans.FirstOrDefault(b => BanMatches(b, address, userId, hwId, exempt)) is { } foundBan
return bans.FirstOrDefault(b => BanMatches(b, address, userId, hwId, exempt, newPlayer)) is { } foundBan
? ConvertBan(foundBan)
: null;
}
@@ -103,12 +105,14 @@ namespace Content.Server.Database
var exempt = await GetBanExemptionCore(db, userId);
var newPlayer = !await db.SqliteDbContext.Player.AnyAsync(p => p.UserId == userId);
// SQLite can't do the net masking stuff we need to match IP address ranges.
// So just pull down the whole list into memory.
var queryBans = await GetAllBans(db.SqliteDbContext, includeUnbanned, exempt);
return queryBans
.Where(b => BanMatches(b, address, userId, hwId, exempt))
.Where(b => BanMatches(b, address, userId, hwId, exempt, newPlayer))
.Select(ConvertBan)
.ToList()!;
}
@@ -137,10 +141,18 @@ namespace Content.Server.Database
IPAddress? address,
NetUserId? userId,
ImmutableArray<byte>? hwId,
ServerBanExemptFlags? exemptFlags)
ServerBanExemptFlags? exemptFlags,
bool newPlayer)
{
// Any flag to bypass BlacklistedRange bans.
var exemptFromBlacklistedRange = exemptFlags != null && exemptFlags.Value != ServerBanExemptFlags.None;
if (!exemptFlags.GetValueOrDefault(ServerBanExemptFlags.None).HasFlag(ServerBanExemptFlags.IP)
&& address != null && ban.Address is not null && address.IsInSubnet(ban.Address.ToTuple().Value))
&& address != null
&& ban.Address is not null
&& address.IsInSubnet(ban.Address.ToTuple().Value)
&& (!ban.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) ||
newPlayer && !exemptFromBlacklistedRange))
{
return true;
}

View File

@@ -1,12 +1,15 @@
using Content.Server.DeviceNetwork.Components;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.DeviceNetwork.Systems;
using Robust.Server.GameObjects;
namespace Content.Server.DeviceNetwork.Systems;
public sealed class DeviceNetworkJammerSystem : EntitySystem
/// <inheritdoc/>
public sealed class DeviceNetworkJammerSystem : SharedDeviceNetworkJammerSystem
{
[Dependency] private TransformSystem _transform = default!;
[Dependency] private readonly TransformSystem _transform = default!;
[Dependency] private readonly SharedDeviceNetworkJammerSystem _jammer = default!;
public override void Initialize()
{
base.Initialize();
@@ -14,20 +17,20 @@ public sealed class DeviceNetworkJammerSystem : EntitySystem
SubscribeLocalEvent<TransformComponent, BeforePacketSentEvent>(BeforePacketSent);
}
private void BeforePacketSent(EntityUid uid, TransformComponent xform, BeforePacketSentEvent ev)
private void BeforePacketSent(Entity<TransformComponent> xform, ref BeforePacketSentEvent ev)
{
if (ev.Cancelled)
return;
var query = EntityQueryEnumerator<DeviceNetworkJammerComponent, TransformComponent>();
while (query.MoveNext(out _, out var jammerComp, out var jammerXform))
while (query.MoveNext(out var uid, out var jammerComp, out var jammerXform))
{
if (!jammerComp.JammableNetworks.Contains(ev.NetworkId))
if (!_jammer.GetJammableNetworks((uid, jammerComp)).Contains(ev.NetworkId))
continue;
if (jammerXform.Coordinates.InRange(EntityManager, _transform, ev.SenderTransform.Coordinates, jammerComp.Range)
|| jammerXform.Coordinates.InRange(EntityManager, _transform, xform.Coordinates, jammerComp.Range))
if (_transform.InRange(jammerXform.Coordinates, ev.SenderTransform.Coordinates, jammerComp.Range)
|| _transform.InRange(jammerXform.Coordinates, xform.Comp.Coordinates, jammerComp.Range))
{
ev.Cancel();
return;

View File

@@ -16,8 +16,10 @@ using Content.Server.Info;
using Content.Server.IoC;
using Content.Server.Maps;
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.Players;
using Content.Server.Players.JobWhitelist;
using Content.Server.Players.PlayTimeTracking;
using Content.Server.Players.RateLimiting;
using Content.Server.Preferences.Managers;
using Content.Server.ServerInfo;
using Content.Server.ServerUpdates;
@@ -111,6 +113,7 @@ namespace Content.Server.Entry
_updateManager.Initialize();
_playTimeTracking.Initialize();
IoCManager.Resolve<JobWhitelistManager>().Initialize();
IoCManager.Resolve<PlayerRateLimitManager>().Initialize();
}
}

View File

@@ -27,8 +27,6 @@ public sealed partial class PuddleSystem
SubscribeLocalEvent<SpillableComponent, LandEvent>(SpillOnLand);
// Openable handles the event if it's closed
SubscribeLocalEvent<SpillableComponent, MeleeHitEvent>(SplashOnMeleeHit, after: [typeof(OpenableSystem)]);
SubscribeLocalEvent<SpillableComponent, ClothingGotEquippedEvent>(OnGotEquipped);
SubscribeLocalEvent<SpillableComponent, ClothingGotUnequippedEvent>(OnGotUnequipped);
SubscribeLocalEvent<SpillableComponent, SolutionContainerOverflowEvent>(OnOverflow);
SubscribeLocalEvent<SpillableComponent, SpillDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<SpillableComponent, AttemptPacifiedThrowEvent>(OnAttemptPacifiedThrow);
@@ -97,33 +95,6 @@ public sealed partial class PuddleSystem
}
}
private void OnGotEquipped(Entity<SpillableComponent> entity, ref ClothingGotEquippedEvent args)
{
if (!entity.Comp.SpillWorn)
return;
if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln, out var solution))
return;
// block access to the solution while worn
AddComp<BlockSolutionAccessComponent>(entity);
if (solution.Volume == 0)
return;
// spill all solution on the player
var drainedSolution = _solutionContainerSystem.Drain(entity.Owner, soln.Value, solution.Volume);
TrySplashSpillAt(entity.Owner, Transform(args.Wearer).Coordinates, drainedSolution, out _);
}
private void OnGotUnequipped(Entity<SpillableComponent> entity, ref ClothingGotUnequippedEvent args)
{
if (!entity.Comp.SpillWorn)
return;
RemCompDeferred<BlockSolutionAccessComponent>(entity);
}
private void SpillOnLand(Entity<SpillableComponent> entity, ref LandEvent args)
{
if (!_solutionContainerSystem.TryGetSolution(entity.Owner, entity.Comp.SolutionName, out var soln, out var solution))

View File

@@ -160,6 +160,12 @@ namespace Content.Server.GameTicking
// whereas the command can also be used on an existing map.
var loadOpts = loadOptions ?? new MapLoadOptions();
if (map.MaxRandomOffset != 0f)
loadOpts.Offset = _robustRandom.NextVector2(map.MaxRandomOffset);
if (map.RandomRotation)
loadOpts.Rotation = _robustRandom.NextAngle();
var ev = new PreGameMapLoad(targetMapId, map, loadOpts);
RaiseLocalEvent(ev);
@@ -359,6 +365,7 @@ namespace Content.Server.GameTicking
var listOfPlayerInfo = new List<RoundEndMessageEvent.RoundEndPlayerInfo>();
// Grab the great big book of all the Minds, we'll need them for this.
var allMinds = EntityQueryEnumerator<MindComponent>();
var pvsOverride = _configurationManager.GetCVar(CCVars.RoundEndPVSOverrides);
while (allMinds.MoveNext(out var mindId, out var mind))
{
// TODO don't list redundant observer roles?
@@ -389,7 +396,7 @@ namespace Content.Server.GameTicking
else if (mind.CurrentEntity != null && TryName(mind.CurrentEntity.Value, out var icName))
playerIcName = icName;
if (TryGetEntity(mind.OriginalOwnedEntity, out var entity))
if (TryGetEntity(mind.OriginalOwnedEntity, out var entity) && pvsOverride)
{
_pvsOverride.AddGlobalOverride(GetNetEntity(entity.Value), recursive: true);
}

View File

@@ -2,6 +2,7 @@ using Content.Server.Antag;
using Content.Server.Chat.Systems;
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;
@@ -35,9 +36,27 @@ public sealed class ZombieRuleSystem : GameRuleSystem<ZombieRuleComponent>
{
base.Initialize();
SubscribeLocalEvent<InitialInfectedRoleComponent, GetBriefingEvent>(OnGetBriefing);
SubscribeLocalEvent<ZombieRoleComponent, GetBriefingEvent>(OnGetBriefing);
SubscribeLocalEvent<IncurableZombieComponent, ZombifySelfActionEvent>(OnZombifySelf);
}
private void OnGetBriefing(EntityUid uid, InitialInfectedRoleComponent component, ref GetBriefingEvent args)
{
if (!TryComp<MindComponent>(uid, out var mind) || mind.OwnedEntity == null)
return;
if (HasComp<ZombieRoleComponent>(uid)) // don't show both briefings
return;
args.Append(Loc.GetString("zombie-patientzero-role-greeting"));
}
private void OnGetBriefing(EntityUid uid, ZombieRoleComponent component, ref GetBriefingEvent args)
{
if (!TryComp<MindComponent>(uid, out var mind) || mind.OwnedEntity == null)
return;
args.Append(Loc.GetString("zombie-infection-greeting"));
}
protected override void AppendRoundEndText(EntityUid uid, ZombieRuleComponent component, GameRuleComponent gameRule,
ref RoundEndTextAppendEvent args)
{

View File

@@ -24,6 +24,9 @@ public sealed partial class ToggleableGhostRoleComponent : Component
[DataField("roleDescription")]
public string RoleDescription = string.Empty;
[DataField("roleRules")]
public string RoleRules = string.Empty;
[DataField("wipeVerbText")]
public string WipeVerbText = string.Empty;

View File

@@ -53,13 +53,14 @@ public sealed class ToggleableGhostRoleSystem : EntitySystem
EnsureComp<GhostTakeoverAvailableComponent>(uid);
ghostRole.RoleName = Loc.GetString(component.RoleName);
ghostRole.RoleDescription = Loc.GetString(component.RoleDescription);
ghostRole.RoleRules = Loc.GetString(component.RoleRules);
}
private void OnExamined(EntityUid uid, ToggleableGhostRoleComponent component, ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
if (TryComp<MindContainerComponent>(uid, out var mind) && mind.HasMind)
{
args.PushMarkup(Loc.GetString(component.ExamineTextMindPresent));

View File

@@ -14,8 +14,10 @@ using Content.Server.Info;
using Content.Server.Maps;
using Content.Server.MoMMI;
using Content.Server.NodeContainer.NodeGroups;
using Content.Server.Players;
using Content.Server.Players.JobWhitelist;
using Content.Server.Players.PlayTimeTracking;
using Content.Server.Players.RateLimiting;
using Content.Server.Preferences.Managers;
using Content.Server.ServerInfo;
using Content.Server.ServerUpdates;
@@ -65,6 +67,7 @@ namespace Content.Server.IoC
IoCManager.Register<ISharedPlaytimeManager, PlayTimeTrackingManager>();
IoCManager.Register<ServerApi>();
IoCManager.Register<JobWhitelistManager>();
IoCManager.Register<PlayerRateLimitManager>();
}
}
}

View File

@@ -3,6 +3,7 @@ using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using System.Diagnostics;
using System.Numerics;
namespace Content.Server.Maps;
@@ -21,16 +22,22 @@ public sealed partial class GameMapPrototype : IPrototype
[IdDataField]
public string ID { get; private set; } = default!;
[DataField]
public float MaxRandomOffset = 1000f;
[DataField]
public bool RandomRotation = true;
/// <summary>
/// Name of the map to use in generic messages, like the map vote.
/// </summary>
[DataField("mapName", required: true)]
[DataField(required: true)]
public string MapName { get; private set; } = default!;
/// <summary>
/// Relative directory path to the given map, i.e. `/Maps/saltern.yml`
/// </summary>
[DataField("mapPath", required: true)]
[DataField(required: true)]
public ResPath MapPath { get; private set; } = default!;
[DataField("stations", required: true)]

View File

@@ -127,10 +127,6 @@ public sealed class PullController : VirtualController
if (_container.IsEntityInContainer(player))
return false;
// Cooldown buddy
if (_timing.CurTime < pullerComp.NextThrow)
return false;
pullerComp.NextThrow = _timing.CurTime + pullerComp.ThrowCooldown;
// Cap the distance

View File

@@ -1,11 +1,9 @@
using Content.Server.Buckle.Systems;
using Content.Shared.Buckle.Components;
namespace Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat;
public sealed partial class UnbuckleOperator : HTNOperator
{
[Dependency] private readonly IEntityManager _entManager = default!;
private BuckleSystem _buckle = default!;
[DataField("shutdownState")]
@@ -21,10 +19,7 @@ public sealed partial class UnbuckleOperator : HTNOperator
{
base.Startup(blackboard);
var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
if (!_entManager.TryGetComponent<BuckleComponent>(owner, out var buckle) || !buckle.Buckled)
return;
_buckle.TryUnbuckle(owner, owner, true, buckle);
_buckle.Unbuckle(owner, null);
}
public override HTNOperatorStatus Update(NPCBlackboard blackboard, float frameTime)

View File

@@ -55,8 +55,6 @@ public sealed class ConveyorController : SharedConveyorController
if (MetaData(uid).EntityLifeStage >= EntityLifeStage.Terminating)
return;
RemComp<ActiveConveyorComponent>(uid);
if (!TryComp<PhysicsComponent>(uid, out var physics))
return;

View File

@@ -0,0 +1,254 @@
using System.Runtime.InteropServices;
using Content.Server.Administration.Logs;
using Content.Shared.Database;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Server.Players.RateLimiting;
/// <summary>
/// General-purpose system to rate limit actions taken by clients, such as chat messages.
/// </summary>
/// <remarks>
/// <para>
/// Different categories of rate limits must be registered ahead of time by calling <see cref="Register"/>.
/// Once registered, you can simply call <see cref="CountAction"/> to count a rate-limited action for a player.
/// </para>
/// <para>
/// This system is intended for rate limiting player actions over short periods,
/// to ward against spam that can cause technical issues such as admin client load.
/// It should not be used for in-game actions or similar.
/// </para>
/// <para>
/// Rate limits are reset when a client reconnects.
/// This should not be an issue for the reasonably short rate limit periods this system is intended for.
/// </para>
/// </remarks>
/// <seealso cref="RateLimitRegistration"/>
public sealed class PlayerRateLimitManager
{
[Dependency] private readonly IAdminLogManager _adminLog = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
private readonly Dictionary<string, RegistrationData> _registrations = new();
private readonly Dictionary<ICommonSession, Dictionary<string, RateLimitDatum>> _rateLimitData = new();
/// <summary>
/// Count and validate an action performed by a player against rate limits.
/// </summary>
/// <param name="player">The player performing the action.</param>
/// <param name="key">The key string that was previously used to register a rate limit category.</param>
/// <returns>Whether the action counted should be blocked due to surpassing rate limits or not.</returns>
/// <exception cref="ArgumentException">
/// <paramref name="player"/> is not a connected player
/// OR <paramref name="key"/> is not a registered rate limit category.
/// </exception>
/// <seealso cref="Register"/>
public RateLimitStatus CountAction(ICommonSession player, string key)
{
if (player.Status == SessionStatus.Disconnected)
throw new ArgumentException("Player is not connected");
if (!_registrations.TryGetValue(key, out var registration))
throw new ArgumentException($"Unregistered key: {key}");
var playerData = _rateLimitData.GetOrNew(player);
ref var datum = ref CollectionsMarshal.GetValueRefOrAddDefault(playerData, key, out _);
var time = _gameTiming.RealTime;
if (datum.CountExpires < time)
{
// Period expired, reset it.
datum.CountExpires = time + registration.LimitPeriod;
datum.Count = 0;
datum.Announced = false;
}
datum.Count += 1;
if (datum.Count <= registration.LimitCount)
return RateLimitStatus.Allowed;
// Breached rate limits, inform admins if configured.
if (registration.AdminAnnounceDelay is { } cvarAnnounceDelay)
{
if (datum.NextAdminAnnounce < time)
{
registration.Registration.AdminAnnounceAction!(player);
datum.NextAdminAnnounce = time + cvarAnnounceDelay;
}
}
if (!datum.Announced)
{
registration.Registration.PlayerLimitedAction(player);
_adminLog.Add(
registration.Registration.AdminLogType,
LogImpact.Medium,
$"Player {player} breached '{key}' rate limit ");
datum.Announced = true;
}
return RateLimitStatus.Blocked;
}
/// <summary>
/// Register a new rate limit category.
/// </summary>
/// <param name="key">
/// The key string that will be referred to later with <see cref="CountAction"/>.
/// Must be unique and should probably just be a constant somewhere.
/// </param>
/// <param name="registration">The data specifying the rate limit's parameters.</param>
/// <exception cref="InvalidOperationException"><paramref name="key"/> has already been registered.</exception>
/// <exception cref="ArgumentException"><paramref name="registration"/> is invalid.</exception>
public void Register(string key, RateLimitRegistration registration)
{
if (_registrations.ContainsKey(key))
throw new InvalidOperationException($"Key already registered: {key}");
var data = new RegistrationData
{
Registration = registration,
};
if ((registration.AdminAnnounceAction == null) != (registration.CVarAdminAnnounceDelay == null))
{
throw new ArgumentException(
$"Must set either both {nameof(registration.AdminAnnounceAction)} and {nameof(registration.CVarAdminAnnounceDelay)} or neither");
}
_cfg.OnValueChanged(
registration.CVarLimitCount,
i => data.LimitCount = i,
invokeImmediately: true);
_cfg.OnValueChanged(
registration.CVarLimitPeriodLength,
i => data.LimitPeriod = TimeSpan.FromSeconds(i),
invokeImmediately: true);
if (registration.CVarAdminAnnounceDelay != null)
{
_cfg.OnValueChanged(
registration.CVarLimitCount,
i => data.AdminAnnounceDelay = TimeSpan.FromSeconds(i),
invokeImmediately: true);
}
_registrations.Add(key, data);
}
/// <summary>
/// Initialize the manager's functionality at game startup.
/// </summary>
public void Initialize()
{
_playerManager.PlayerStatusChanged += PlayerManagerOnPlayerStatusChanged;
}
private void PlayerManagerOnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus == SessionStatus.Disconnected)
_rateLimitData.Remove(e.Session);
}
private sealed class RegistrationData
{
public required RateLimitRegistration Registration { get; init; }
public TimeSpan LimitPeriod { get; set; }
public int LimitCount { get; set; }
public TimeSpan? AdminAnnounceDelay { get; set; }
}
private struct RateLimitDatum
{
/// <summary>
/// Time stamp (relative to <see cref="IGameTiming.RealTime"/>) this rate limit period will expire at.
/// </summary>
public TimeSpan CountExpires;
/// <summary>
/// How many actions have been done in the current rate limit period.
/// </summary>
public int Count;
/// <summary>
/// Have we announced to the player that they've been blocked in this rate limit period?
/// </summary>
public bool Announced;
/// <summary>
/// Time stamp (relative to <see cref="IGameTiming.RealTime"/>) of the
/// next time we can send an announcement to admins about rate limit breach.
/// </summary>
public TimeSpan NextAdminAnnounce;
}
}
/// <summary>
/// Contains all data necessary to register a rate limit with <see cref="PlayerRateLimitManager.Register"/>.
/// </summary>
public sealed class RateLimitRegistration
{
/// <summary>
/// CVar that controls the period over which the rate limit is counted, measured in seconds.
/// </summary>
public required CVarDef<int> CVarLimitPeriodLength { get; init; }
/// <summary>
/// CVar that controls how many actions are allowed in a single rate limit period.
/// </summary>
public required CVarDef<int> CVarLimitCount { get; init; }
/// <summary>
/// An action that gets invoked when this rate limit has been breached by a player.
/// </summary>
/// <remarks>
/// This can be used for informing players or taking administrative action.
/// </remarks>
public required Action<ICommonSession> PlayerLimitedAction { get; init; }
/// <summary>
/// CVar that controls the minimum delay between admin notifications, measured in seconds.
/// This can be omitted to have no admin notification system.
/// </summary>
/// <remarks>
/// If set, <see cref="AdminAnnounceAction"/> must be set too.
/// </remarks>
public CVarDef<int>? CVarAdminAnnounceDelay { get; init; }
/// <summary>
/// An action that gets invoked when a rate limit was breached and admins should be notified.
/// </summary>
/// <remarks>
/// If set, <see cref="CVarAdminAnnounceDelay"/> must be set too.
/// </remarks>
public Action<ICommonSession>? AdminAnnounceAction { get; init; }
/// <summary>
/// Log type used to log rate limit violations to the admin logs system.
/// </summary>
public LogType AdminLogType { get; init; } = LogType.RateLimited;
}
/// <summary>
/// Result of a rate-limited operation.
/// </summary>
/// <seealso cref="PlayerRateLimitManager.CountAction"/>
public enum RateLimitStatus : byte
{
/// <summary>
/// The action was not blocked by the rate limit.
/// </summary>
Allowed,
/// <summary>
/// The action was blocked by the rate limit.
/// </summary>
Blocked,
}

View File

@@ -1,12 +0,0 @@
using Content.Server.Radio.EntitySystems;
namespace Content.Server.Radio.Components;
/// <summary>
/// Prevents all radio in range from sending messages
/// </summary>
[RegisterComponent]
[Access(typeof(JammerSystem))]
public sealed partial class ActiveRadioJammerComponent : Component
{
}

View File

@@ -1,14 +1,12 @@
using Content.Server.DeviceNetwork.Components;
using Content.Server.Popups;
using Content.Server.Power.EntitySystems;
using Content.Server.PowerCell;
using Content.Server.Radio.Components;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.PowerCell.Components;
using Content.Shared.RadioJammer;
using Content.Shared.Radio.EntitySystems;
using Content.Shared.Radio.Components;
using Content.Shared.DeviceNetwork.Systems;
namespace Content.Server.Radio.EntitySystems;
@@ -17,6 +15,7 @@ public sealed class JammerSystem : SharedJammerSystem
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedDeviceNetworkJammerSystem _jammer = default!;
public override void Initialize()
{
@@ -24,7 +23,6 @@ public sealed class JammerSystem : SharedJammerSystem
SubscribeLocalEvent<RadioJammerComponent, ActivateInWorldEvent>(OnActivate);
SubscribeLocalEvent<ActiveRadioJammerComponent, PowerCellChangedEvent>(OnPowerCellChanged);
SubscribeLocalEvent<RadioJammerComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<RadioSendAttemptEvent>(OnRadioSendAttempt);
}
@@ -37,27 +35,22 @@ public sealed class JammerSystem : SharedJammerSystem
if (_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery))
{
if (!_battery.TryUseCharge(batteryUid.Value, GetCurrentWattage(jam) * frameTime, battery))
if (!_battery.TryUseCharge(batteryUid.Value, GetCurrentWattage((uid, jam)) * frameTime, battery))
{
ChangeLEDState(false, uid);
ChangeLEDState(uid, false);
RemComp<ActiveRadioJammerComponent>(uid);
RemComp<DeviceNetworkJammerComponent>(uid);
}
else
{
var percentCharged = battery.CurrentCharge / battery.MaxCharge;
if (percentCharged > .50)
var chargeLevel = percentCharged switch
{
ChangeChargeLevel(RadioJammerChargeLevel.High, uid);
}
else if (percentCharged < .15)
{
ChangeChargeLevel(RadioJammerChargeLevel.Low, uid);
}
else
{
ChangeChargeLevel(RadioJammerChargeLevel.Medium, uid);
}
> 0.50f => RadioJammerChargeLevel.High,
< 0.15f => RadioJammerChargeLevel.Low,
_ => RadioJammerChargeLevel.Medium,
};
ChangeChargeLevel(uid, chargeLevel);
}
}
@@ -65,28 +58,27 @@ public sealed class JammerSystem : SharedJammerSystem
}
}
private void OnActivate(EntityUid uid, RadioJammerComponent comp, ActivateInWorldEvent args)
private void OnActivate(Entity<RadioJammerComponent> ent, ref ActivateInWorldEvent args)
{
if (args.Handled || !args.Complex)
return;
var activated = !HasComp<ActiveRadioJammerComponent>(uid) &&
_powerCell.TryGetBatteryFromSlot(uid, out var battery) &&
battery.CurrentCharge > GetCurrentWattage(comp);
var activated = !HasComp<ActiveRadioJammerComponent>(ent) &&
_powerCell.TryGetBatteryFromSlot(ent.Owner, out var battery) &&
battery.CurrentCharge > GetCurrentWattage(ent);
if (activated)
{
ChangeLEDState(true, uid);
EnsureComp<ActiveRadioJammerComponent>(uid);
EnsureComp<DeviceNetworkJammerComponent>(uid, out var jammingComp);
jammingComp.Range = GetCurrentRange(comp);
jammingComp.JammableNetworks.Add(DeviceNetworkComponent.DeviceNetIdDefaults.Wireless.ToString());
Dirty(uid, jammingComp);
ChangeLEDState(ent.Owner, true);
EnsureComp<ActiveRadioJammerComponent>(ent);
EnsureComp<DeviceNetworkJammerComponent>(ent, out var jammingComp);
_jammer.SetRange((ent, jammingComp), GetCurrentRange(ent));
_jammer.AddJammableNetwork((ent, jammingComp), DeviceNetworkComponent.DeviceNetIdDefaults.Wireless.ToString());
}
else
{
ChangeLEDState(false, uid);
RemCompDeferred<ActiveRadioJammerComponent>(uid);
RemCompDeferred<DeviceNetworkJammerComponent>(uid);
ChangeLEDState(ent.Owner, false);
RemCompDeferred<ActiveRadioJammerComponent>(ent);
RemCompDeferred<DeviceNetworkJammerComponent>(ent);
}
var state = Loc.GetString(activated ? "radio-jammer-component-on-state" : "radio-jammer-component-off-state");
var message = Loc.GetString("radio-jammer-component-on-use", ("state", state));
@@ -94,27 +86,12 @@ public sealed class JammerSystem : SharedJammerSystem
args.Handled = true;
}
private void OnPowerCellChanged(EntityUid uid, ActiveRadioJammerComponent comp, PowerCellChangedEvent args)
private void OnPowerCellChanged(Entity<ActiveRadioJammerComponent> ent, ref PowerCellChangedEvent args)
{
if (args.Ejected)
{
ChangeLEDState(false, uid);
RemCompDeferred<ActiveRadioJammerComponent>(uid);
}
}
private void OnExamine(EntityUid uid, RadioJammerComponent comp, ExaminedEvent args)
{
if (args.IsInDetailsRange)
{
var powerIndicator = HasComp<ActiveRadioJammerComponent>(uid)
? Loc.GetString("radio-jammer-component-examine-on-state")
: Loc.GetString("radio-jammer-component-examine-off-state");
args.PushMarkup(powerIndicator);
var powerLevel = Loc.GetString(comp.Settings[comp.SelectedPowerLevel].Name);
var switchIndicator = Loc.GetString("radio-jammer-component-switch-setting", ("powerLevel", powerLevel));
args.PushMarkup(switchIndicator);
ChangeLEDState(ent.Owner, false);
RemCompDeferred<ActiveRadioJammerComponent>(ent);
}
}
@@ -131,9 +108,9 @@ public sealed class JammerSystem : SharedJammerSystem
var source = Transform(sourceUid).Coordinates;
var query = EntityQueryEnumerator<ActiveRadioJammerComponent, RadioJammerComponent, TransformComponent>();
while (query.MoveNext(out _, out _, out var jam, out var transform))
while (query.MoveNext(out var uid, out _, out var jam, out var transform))
{
if (source.InRange(EntityManager, _transform, transform.Coordinates, GetCurrentRange(jam)))
if (_transform.InRange(source, transform.Coordinates, GetCurrentRange((uid, jam))))
{
return true;
}

View File

@@ -131,25 +131,6 @@ public sealed partial class ResearchSystem
RaiseLocalEvent(uid, ref ev);
}
/// <summary>
/// Adds a lathe recipe to the specified technology database
/// without checking if it can be unlocked.
/// </summary>
public void AddLatheRecipe(EntityUid uid, string recipe, TechnologyDatabaseComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
if (component.UnlockedRecipes.Contains(recipe))
return;
component.UnlockedRecipes.Add(recipe);
Dirty(uid, component);
var ev = new TechnologyDatabaseModifiedEvent();
RaiseLocalEvent(uid, ref ev);
}
/// <summary>
/// Returns whether a technology can be unlocked on this database,
/// taking parent technologies into account.

View File

@@ -1,20 +0,0 @@
using Content.Shared.Random;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Research.TechnologyDisk.Components;
[RegisterComponent]
public sealed partial class TechnologyDiskComponent : Component
{
/// <summary>
/// The recipe that will be added. If null, one will be randomly generated
/// </summary>
[DataField("recipes")]
public List<string>? Recipes;
/// <summary>
/// A weighted random prototype for how rare each tier should be.
/// </summary>
[DataField("tierWeightPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<WeightedRandomPrototype>))]
public string TierWeightPrototype = "TechDiskTierWeights";
}

View File

@@ -1,92 +0,0 @@
using System.Linq;
using Content.Server.Popups;
using Content.Server.Research.Systems;
using Content.Server.Research.TechnologyDisk.Components;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Random;
using Content.Shared.Random.Helpers;
using Content.Shared.Research.Components;
using Content.Shared.Research.Prototypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Research.TechnologyDisk.Systems;
public sealed class TechnologyDiskSystem : EntitySystem
{
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly ResearchSystem _research = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly IRobustRandom _random = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<TechnologyDiskComponent, AfterInteractEvent>(OnAfterInteract);
SubscribeLocalEvent<TechnologyDiskComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<TechnologyDiskComponent, MapInitEvent>(OnMapInit);
}
private void OnAfterInteract(EntityUid uid, TechnologyDiskComponent component, AfterInteractEvent args)
{
if (args.Handled || !args.CanReach || args.Target is not { } target)
return;
if (!HasComp<ResearchServerComponent>(target) || !TryComp<TechnologyDatabaseComponent>(target, out var database))
return;
if (component.Recipes != null)
{
foreach (var recipe in component.Recipes)
{
_research.AddLatheRecipe(target, recipe, database);
}
}
_popup.PopupEntity(Loc.GetString("tech-disk-inserted"), target, args.User);
QueueDel(uid);
args.Handled = true;
}
private void OnExamine(EntityUid uid, TechnologyDiskComponent component, ExaminedEvent args)
{
var message = Loc.GetString("tech-disk-examine-none");
if (component.Recipes != null && component.Recipes.Any())
{
var prototype = _prototype.Index<LatheRecipePrototype>(component.Recipes[0]);
var resultPrototype = _prototype.Index<EntityPrototype>(prototype.Result);
message = Loc.GetString("tech-disk-examine", ("result", resultPrototype.Name));
if (component.Recipes.Count > 1) //idk how to do this well. sue me.
message += " " + Loc.GetString("tech-disk-examine-more");
}
args.PushMarkup(message);
}
private void OnMapInit(EntityUid uid, TechnologyDiskComponent component, MapInitEvent args)
{
if (component.Recipes != null)
return;
var weightedRandom = _prototype.Index<WeightedRandomPrototype>(component.TierWeightPrototype);
var tier = int.Parse(weightedRandom.Pick(_random));
//get a list of every distinct recipe in all the technologies.
var techs = new List<ProtoId<LatheRecipePrototype>>();
foreach (var tech in _prototype.EnumeratePrototypes<TechnologyPrototype>())
{
if (tech.Tier != tier)
continue;
techs.AddRange(tech.RecipeUnlocks);
}
techs = techs.Distinct().ToList();
if (!techs.Any())
return;
//pick one
component.Recipes = new();
component.Recipes.Add(_random.Pick(techs));
}
}

View File

@@ -65,7 +65,8 @@ public sealed class ContainmentFieldGeneratorSystem : EntitySystem
/// </summary>
private void HandleGeneratorCollide(Entity<ContainmentFieldGeneratorComponent> generator, ref StartCollideEvent args)
{
if (_tags.HasTag(args.OtherEntity, generator.Comp.IDTag))
if (args.OtherFixtureId == generator.Comp.SourceFixtureId &&
_tags.HasTag(args.OtherEntity, generator.Comp.IDTag))
{
ReceivePower(generator.Comp.PowerReceived, generator);
generator.Comp.Accumulator = 0f;

View File

@@ -1,16 +0,0 @@
using Content.Server.Station.Systems;
namespace Content.Server.Station.Components;
/// <summary>
/// Stores station parameters that can be randomized by the roundstart
/// </summary>
[RegisterComponent, Access(typeof(StationSystem))]
public sealed partial class StationRandomTransformComponent : Component
{
[DataField]
public float? MaxStationOffset = 100.0f;
[DataField]
public bool EnableStationRotation = true;
}

View File

@@ -1,10 +1,9 @@
using System.Linq;
using System.Numerics;
using Content.Server.Chat.Systems;
using Content.Server.GameTicking;
using Content.Server.Station.Components;
using Content.Server.Station.Events;
using Content.Shared.Fax;
using Content.Shared.CCVar;
using Content.Shared.Station;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
@@ -28,10 +27,12 @@ namespace Content.Server.Station.Systems;
[PublicAPI]
public sealed class StationSystem : EntitySystem
{
[Dependency] private readonly IConfigurationManager _cfgManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ChatSystem _chatSystem = default!;
[Dependency] private readonly GameTicker _ticker = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly MapSystem _map = default!;
@@ -282,51 +283,11 @@ public sealed class StationSystem : EntitySystem
var data = Comp<StationDataComponent>(station);
name ??= MetaData(station).EntityName;
var entry = gridIds ?? Array.Empty<EntityUid>();
foreach (var grid in entry)
foreach (var grid in gridIds ?? Array.Empty<EntityUid>())
{
AddGridToStation(station, grid, null, data, name);
}
if (TryComp<StationRandomTransformComponent>(station, out var random))
{
Angle? rotation = null;
Vector2? offset = null;
if (random.MaxStationOffset != null)
offset = _random.NextVector2(-random.MaxStationOffset.Value, random.MaxStationOffset.Value);
if (random.EnableStationRotation)
rotation = _random.NextAngle();
foreach (var grid in entry)
{
//planetary maps give an error when trying to change from position or rotation.
//This is still the case, but it will be irrelevant after the https://github.com/space-wizards/space-station-14/pull/26510
if (rotation != null && offset != null)
{
var pos = _transform.GetWorldPosition(grid);
_transform.SetWorldPositionRotation(grid, pos + offset.Value, rotation.Value);
continue;
}
if (rotation != null)
{
_transform.SetWorldRotation(grid, rotation.Value);
continue;
}
if (offset != null)
{
var pos = _transform.GetWorldPosition(grid);
_transform.SetWorldPosition(grid, pos + offset.Value);
continue;
}
}
}
if (LifeStage(station) < EntityLifeStage.MapInitialized)
throw new Exception($"Station must be man-initialized");
var ev = new StationPostInitEvent((station, data));
RaiseLocalEvent(station, ref ev, true);

View File

@@ -90,7 +90,7 @@ public sealed class SurveillanceCameraMonitorSystem : EntitySystem
private void OnSubnetRequest(EntityUid uid, SurveillanceCameraMonitorComponent component,
SurveillanceCameraMonitorSubnetRequestMessage args)
{
if (args.Actor != null)
if (args.Actor is { Valid: true } actor && !Deleted(actor))
{
SetActiveSubnet(uid, args.Subnet, component);
}
@@ -146,6 +146,7 @@ public sealed class SurveillanceCameraMonitorSystem : EntitySystem
break;
case SurveillanceCameraSystem.CameraSubnetData:
if (args.Data.TryGetValue(SurveillanceCameraSystem.CameraSubnetData, out string? subnet)
&& !string.IsNullOrEmpty(subnet)
&& !component.KnownSubnets.ContainsKey(subnet))
{
component.KnownSubnets.Add(subnet, args.SenderAddress);
@@ -217,6 +218,7 @@ public sealed class SurveillanceCameraMonitorSystem : EntitySystem
{
if (!Resolve(uid, ref monitor)
|| monitor.LastHeartbeatSent < _heartbeatDelay
|| string.IsNullOrEmpty(monitor.ActiveSubnet)
|| !monitor.KnownSubnets.TryGetValue(monitor.ActiveSubnet, out var subnetAddress))
{
return;
@@ -278,6 +280,7 @@ public sealed class SurveillanceCameraMonitorSystem : EntitySystem
SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| string.IsNullOrEmpty(subnet)
|| !monitor.KnownSubnets.ContainsKey(subnet))
{
return;
@@ -295,6 +298,7 @@ public sealed class SurveillanceCameraMonitorSystem : EntitySystem
private void RequestActiveSubnetInfo(EntityUid uid, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| string.IsNullOrEmpty(monitor.ActiveSubnet)
|| !monitor.KnownSubnets.TryGetValue(monitor.ActiveSubnet, out var address))
{
return;
@@ -310,6 +314,7 @@ public sealed class SurveillanceCameraMonitorSystem : EntitySystem
private void ConnectToSubnet(EntityUid uid, string subnet, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| string.IsNullOrEmpty(subnet)
|| !monitor.KnownSubnets.TryGetValue(subnet, out var address))
{
return;
@@ -327,6 +332,7 @@ public sealed class SurveillanceCameraMonitorSystem : EntitySystem
private void DisconnectFromSubnet(EntityUid uid, string subnet, SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| string.IsNullOrEmpty(subnet)
|| !monitor.KnownSubnets.TryGetValue(subnet, out var address))
{
return;
@@ -415,6 +421,7 @@ public sealed class SurveillanceCameraMonitorSystem : EntitySystem
SurveillanceCameraMonitorComponent? monitor = null)
{
if (!Resolve(uid, ref monitor)
|| string.IsNullOrEmpty(monitor.ActiveSubnet)
|| !monitor.KnownSubnets.TryGetValue(monitor.ActiveSubnet, out var subnetAddress))
{
return;

View File

@@ -23,4 +23,10 @@ public sealed partial class ThiefUndeterminedBackpackComponent : Component
[DataField]
public SoundSpecifier ApproveSound = new SoundPathSpecifier("/Audio/Effects/rustle1.ogg");
/// <summary>
/// Max number of sets you can select.
/// </summary>
[DataField]
public int MaxSelectedSets = 2;
}

View File

@@ -18,7 +18,6 @@ public sealed class ThiefUndeterminedBackpackSystem : EntitySystem
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
private const int MaxSelectedSets = 2;
public override void Initialize()
{
base.Initialize();
@@ -35,7 +34,7 @@ public sealed class ThiefUndeterminedBackpackSystem : EntitySystem
private void OnApprove(Entity<ThiefUndeterminedBackpackComponent> backpack, ref ThiefBackpackApproveMessage args)
{
if (backpack.Comp.SelectedSets.Count != MaxSelectedSets)
if (backpack.Comp.SelectedSets.Count != backpack.Comp.MaxSelectedSets)
return;
foreach (var i in backpack.Comp.SelectedSets)
@@ -79,6 +78,6 @@ public sealed class ThiefUndeterminedBackpackSystem : EntitySystem
data.Add(i, info);
}
_ui.SetUiState(uid, ThiefBackpackUIKey.Key, new ThiefBackpackBoundUserInterfaceState(data, MaxSelectedSets));
_ui.SetUiState(uid, ThiefBackpackUIKey.Key, new ThiefBackpackBoundUserInterfaceState(data, component.MaxSelectedSets));
}
}

View File

@@ -1,17 +1,16 @@
using System.Linq;
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Weather;
using Robust.Shared.Console;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
namespace Content.Server.Weather;
public sealed class WeatherSystem : SharedWeatherSystem
{
[Dependency] private readonly IConsoleHost _console = default!;
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
public override void Initialize()
{
@@ -30,7 +29,7 @@ public sealed class WeatherSystem : SharedWeatherSystem
}
[AdminCommand(AdminFlags.Fun)]
private void WeatherTwo(IConsoleShell shell, string argstr, string[] args)
private void WeatherTwo(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 2)
{
@@ -60,7 +59,8 @@ public sealed class WeatherSystem : SharedWeatherSystem
var maxTime = TimeSpan.MaxValue;
// If it's already running then just fade out with how much time we're into the weather.
if (TryComp<WeatherComponent>(MapManager.GetMapEntityId(mapId), out var weatherComp) &&
if (_mapSystem.TryGetMap(mapId, out var mapUid) &&
TryComp<WeatherComponent>(mapUid, out var weatherComp) &&
weatherComp.Weather.TryGetValue(args[1], out var existing))
{
maxTime = curTime - existing.StartTime;

View File

@@ -1,7 +0,0 @@
namespace Content.Server.Zombies;
[RegisterComponent]
public sealed partial class InitialInfectedExemptComponent : Component
{
}

View File

@@ -96,5 +96,13 @@ public enum LogType
ChatRateLimited = 87,
AtmosTemperatureChanged = 88,
DeviceNetwork = 89,
StoreRefund = 90
StoreRefund = 90,
/// <summary>
/// User was rate-limited for some spam action.
/// </summary>
/// <remarks>
/// This is a default value used by <c>PlayerRateLimitManager</c>, though users can use different log types.
/// </remarks>
RateLimited = 91,
}

View File

@@ -70,7 +70,7 @@ namespace Content.Shared.ActionBlocker
return false;
var ev = new InteractionAttemptEvent(user, target);
RaiseLocalEvent(user, ev);
RaiseLocalEvent(user, ref ev);
if (ev.Cancelled)
return false;
@@ -79,7 +79,7 @@ namespace Content.Shared.ActionBlocker
return true;
var targetEv = new GettingInteractedWithAttemptEvent(user, target);
RaiseLocalEvent(target.Value, targetEv);
RaiseLocalEvent(target.Value, ref targetEv);
return !targetEv.Cancelled;
}
@@ -110,7 +110,7 @@ namespace Content.Shared.ActionBlocker
public bool CanConsciouslyPerformAction(EntityUid user)
{
var ev = new ConsciousAttemptEvent(user);
RaiseLocalEvent(user, ev);
RaiseLocalEvent(user, ref ev);
return !ev.Cancelled;
}

View File

@@ -11,6 +11,7 @@ using Content.Shared.Throwing;
namespace Content.Shared.Administration;
// TODO deduplicate with BlockMovementComponent
public abstract class SharedAdminFrozenSystem : EntitySystem
{
[Dependency] private readonly ActionBlockerSystem _blocker = default!;
@@ -23,7 +24,7 @@ public abstract class SharedAdminFrozenSystem : EntitySystem
SubscribeLocalEvent<AdminFrozenComponent, UseAttemptEvent>(OnAttempt);
SubscribeLocalEvent<AdminFrozenComponent, PickupAttemptEvent>(OnAttempt);
SubscribeLocalEvent<AdminFrozenComponent, ThrowAttemptEvent>(OnAttempt);
SubscribeLocalEvent<AdminFrozenComponent, InteractionAttemptEvent>(OnAttempt);
SubscribeLocalEvent<AdminFrozenComponent, InteractionAttemptEvent>(OnInteractAttempt);
SubscribeLocalEvent<AdminFrozenComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<AdminFrozenComponent, ComponentShutdown>(UpdateCanMove);
SubscribeLocalEvent<AdminFrozenComponent, UpdateCanMoveEvent>(OnUpdateCanMove);
@@ -34,6 +35,11 @@ public abstract class SharedAdminFrozenSystem : EntitySystem
SubscribeLocalEvent<AdminFrozenComponent, SpeakAttemptEvent>(OnSpeakAttempt);
}
private void OnInteractAttempt(Entity<AdminFrozenComponent> ent, ref InteractionAttemptEvent args)
{
args.Cancelled = true;
}
private void OnSpeakAttempt(EntityUid uid, AdminFrozenComponent component, SpeakAttemptEvent args)
{
if (!component.Muted)

View File

@@ -1,4 +1,5 @@
using Content.Shared.Actions;
using Content.Shared.Buckle.Components;
using Content.Shared.Damage;
using Content.Shared.Damage.ForceSay;
using Content.Shared.Examine;
@@ -59,6 +60,15 @@ public sealed partial class SleepingSystem : EntitySystem
SubscribeLocalEvent<SleepingComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<ForcedSleepingComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<SleepingComponent, UnbuckleAttemptEvent>(OnUnbuckleAttempt);
}
private void OnUnbuckleAttempt(Entity<SleepingComponent> ent, ref UnbuckleAttemptEvent args)
{
// TODO is this necessary?
// Shouldn't the interaction have already been blocked by a general interaction check?
if (ent.Owner == args.User)
args.Cancelled = true;
}
private void OnBedSleepAction(Entity<ActionsContainerComponent> ent, ref SleepActionEvent args)
@@ -153,7 +163,7 @@ public sealed partial class SleepingSystem : EntitySystem
private void OnConsciousAttempt(Entity<SleepingComponent> ent, ref ConsciousAttemptEvent args)
{
args.Cancel();
args.Cancelled = true;
}
private void OnExamined(Entity<SleepingComponent> ent, ref ExaminedEvent args)

View File

@@ -1,10 +1,15 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Interaction;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Buckle.Components;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
/// <summary>
/// This component allows an entity to be buckled to an entity with a <see cref="StrapComponent"/>.
/// </summary>
[RegisterComponent, NetworkedComponent]
[Access(typeof(SharedBuckleSystem))]
public sealed partial class BuckleComponent : Component
{
@@ -14,31 +19,23 @@ public sealed partial class BuckleComponent : Component
/// across a table two tiles away" problem.
/// </summary>
[DataField]
[ViewVariables(VVAccess.ReadWrite)]
public float Range = SharedInteractionSystem.InteractionRange / 1.4f;
/// <summary>
/// True if the entity is buckled, false otherwise.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[AutoNetworkedField]
public bool Buckled;
[ViewVariables]
[AutoNetworkedField]
public EntityUid? LastEntityBuckledTo;
[MemberNotNullWhen(true, nameof(BuckledTo))]
public bool Buckled => BuckledTo != null;
/// <summary>
/// Whether or not collisions should be possible with the entity we are strapped to
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField, AutoNetworkedField]
[DataField]
public bool DontCollide;
/// <summary>
/// Whether or not we should be allowed to pull the entity we are strapped to
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public bool PullStrap;
@@ -47,20 +44,18 @@ public sealed partial class BuckleComponent : Component
/// be able to unbuckle after recently buckling.
/// </summary>
[DataField]
[ViewVariables(VVAccess.ReadWrite)]
public TimeSpan Delay = TimeSpan.FromSeconds(0.25f);
/// <summary>
/// The time that this entity buckled at.
/// </summary>
[ViewVariables]
public TimeSpan BuckleTime;
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan? BuckleTime;
/// <summary>
/// The strap that this component is buckled to.
/// </summary>
[ViewVariables]
[AutoNetworkedField]
[DataField]
public EntityUid? BuckledTo;
/// <summary>
@@ -68,7 +63,6 @@ public sealed partial class BuckleComponent : Component
/// <see cref="StrapComponent"/>.
/// </summary>
[DataField]
[ViewVariables(VVAccess.ReadWrite)]
public int Size = 100;
/// <summary>
@@ -77,11 +71,90 @@ public sealed partial class BuckleComponent : Component
[ViewVariables] public int? OriginalDrawDepth;
}
[ByRefEvent]
public record struct BuckleAttemptEvent(EntityUid StrapEntity, EntityUid BuckledEntity, EntityUid UserEntity, bool Buckling, bool Cancelled = false);
[Serializable, NetSerializable]
public sealed class BuckleState(NetEntity? buckledTo, bool dontCollide, TimeSpan? buckleTime) : ComponentState
{
public readonly NetEntity? BuckledTo = buckledTo;
public readonly bool DontCollide = dontCollide;
public readonly TimeSpan? BuckleTime = buckleTime;
}
/// <summary>
/// Event raised directed at a strap entity before some entity gets buckled to it.
/// </summary>
[ByRefEvent]
public readonly record struct BuckleChangeEvent(EntityUid StrapEntity, EntityUid BuckledEntity, bool Buckling);
public record struct StrapAttemptEvent(
Entity<StrapComponent> Strap,
Entity<BuckleComponent> Buckle,
EntityUid? User,
bool Popup)
{
public bool Cancelled;
}
/// <summary>
/// Event raised directed at a buckle entity before it gets buckled to some strap entity.
/// </summary>
[ByRefEvent]
public record struct BuckleAttemptEvent(
Entity<StrapComponent> Strap,
Entity<BuckleComponent> Buckle,
EntityUid? User,
bool Popup)
{
public bool Cancelled;
}
/// <summary>
/// Event raised directed at a strap entity before some entity gets unbuckled from it.
/// </summary>
[ByRefEvent]
public record struct UnstrapAttemptEvent(
Entity<StrapComponent> Strap,
Entity<BuckleComponent> Buckle,
EntityUid? User,
bool Popup)
{
public bool Cancelled;
}
/// <summary>
/// Event raised directed at a buckle entity before it gets unbuckled.
/// </summary>
[ByRefEvent]
public record struct UnbuckleAttemptEvent(
Entity<StrapComponent> Strap,
Entity<BuckleComponent> Buckle,
EntityUid? User,
bool Popup)
{
public bool Cancelled;
}
/// <summary>
/// Event raised directed at a strap entity after something has been buckled to it.
/// </summary>
[ByRefEvent]
public readonly record struct StrappedEvent(Entity<StrapComponent> Strap, Entity<BuckleComponent> Buckle);
/// <summary>
/// Event raised directed at a buckle entity after it has been buckled.
/// </summary>
[ByRefEvent]
public readonly record struct BuckledEvent(Entity<StrapComponent> Strap, Entity<BuckleComponent> Buckle);
/// <summary>
/// Event raised directed at a strap entity after something has been unbuckled from it.
/// </summary>
[ByRefEvent]
public readonly record struct UnstrappedEvent(Entity<StrapComponent> Strap, Entity<BuckleComponent> Buckle);
/// <summary>
/// Event raised directed at a buckle entity after it has been unbuckled from some strap entity.
/// </summary>
[ByRefEvent]
public readonly record struct UnbuckledEvent(Entity<StrapComponent> Strap, Entity<BuckleComponent> Buckle);
[Serializable, NetSerializable]
public enum BuckleVisuals

View File

@@ -13,117 +13,77 @@ namespace Content.Shared.Buckle.Components;
public sealed partial class StrapComponent : Component
{
/// <summary>
/// The entities that are currently buckled
/// The entities that are currently buckled to this strap.
/// </summary>
[AutoNetworkedField]
[ViewVariables] // TODO serialization
[ViewVariables]
public HashSet<EntityUid> BuckledEntities = new();
/// <summary>
/// Entities that this strap accepts and can buckle
/// If null it accepts any entity
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public EntityWhitelist? Whitelist;
/// <summary>
/// Entities that this strap does not accept and cannot buckle.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public EntityWhitelist? Blacklist;
/// <summary>
/// The change in position to the strapped mob
/// </summary>
[DataField, AutoNetworkedField]
[ViewVariables(VVAccess.ReadWrite)]
public StrapPosition Position = StrapPosition.None;
/// <summary>
/// The distance above which a buckled entity will be automatically unbuckled.
/// Don't change it unless you really have to
/// </summary>
/// <remarks>
/// Dont set this below 0.2 because that causes audio issues with <see cref="SharedBuckleSystem.OnBuckleMove"/>
/// My guess after testing is that the client sets BuckledTo to the strap in *some* ticks for some reason
/// whereas the server doesnt, thus the client tries to unbuckle like 15 times because it passes the strap null check
/// This is why this needs to be above 0.1 to make the InRange check fail in both client and server.
/// </remarks>
[DataField, AutoNetworkedField]
[ViewVariables(VVAccess.ReadWrite)]
public float MaxBuckleDistance = 0.2f;
/// <summary>
/// Gets and clamps the buckle offset to MaxBuckleDistance
/// </summary>
[ViewVariables]
public Vector2 BuckleOffsetClamped => Vector2.Clamp(
BuckleOffset,
Vector2.One * -MaxBuckleDistance,
Vector2.One * MaxBuckleDistance);
/// <summary>
/// The buckled entity will be offset by this amount from the center of the strap object.
/// If this offset it too big, it will be clamped to <see cref="MaxBuckleDistance"/>
/// </summary>
[DataField, AutoNetworkedField]
[ViewVariables(VVAccess.ReadWrite)]
public Vector2 BuckleOffset = Vector2.Zero;
/// <summary>
/// The angle to rotate the player by when they get strapped
/// </summary>
[DataField]
[ViewVariables(VVAccess.ReadWrite)]
public Angle Rotation;
/// <summary>
/// The size of the strap which is compared against when buckling entities
/// </summary>
[DataField]
[ViewVariables(VVAccess.ReadWrite)]
public int Size = 100;
/// <summary>
/// If disabled, nothing can be buckled on this object, and it will unbuckle anything that's already buckled
/// </summary>
[ViewVariables]
[DataField, AutoNetworkedField]
public bool Enabled = true;
/// <summary>
/// You can specify the offset the entity will have after unbuckling.
/// </summary>
[DataField]
[ViewVariables(VVAccess.ReadWrite)]
public Vector2 UnbuckleOffset = Vector2.Zero;
/// <summary>
/// The sound to be played when a mob is buckled
/// </summary>
[DataField]
[ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier BuckleSound = new SoundPathSpecifier("/Audio/Effects/buckle.ogg");
/// <summary>
/// The sound to be played when a mob is unbuckled
/// </summary>
[DataField]
[ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier UnbuckleSound = new SoundPathSpecifier("/Audio/Effects/unbuckle.ogg");
/// <summary>
/// ID of the alert to show when buckled
/// </summary>
[DataField]
[ViewVariables(VVAccess.ReadWrite)]
public ProtoId<AlertPrototype> BuckledAlertType = "Buckled";
/// <summary>
/// The sum of the sizes of all the buckled entities in this strap
/// </summary>
[AutoNetworkedField]
[ViewVariables]
public int OccupiedSize;
}
public enum StrapPosition

View File

@@ -1,39 +1,46 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Content.Shared.Alert;
using Content.Shared.Bed.Sleep;
using Content.Shared.Buckle.Components;
using Content.Shared.Database;
using Content.Shared.Hands.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Mobs.Components;
using Content.Shared.Movement.Events;
using Content.Shared.Movement.Pulling.Events;
using Content.Shared.Popups;
using Content.Shared.Pulling.Events;
using Content.Shared.Standing;
using Content.Shared.Storage.Components;
using Content.Shared.Stunnable;
using Content.Shared.Throwing;
using Content.Shared.Verbs;
using Content.Shared.Whitelist;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using PullableComponent = Content.Shared.Movement.Pulling.Components.PullableComponent;
namespace Content.Shared.Buckle;
public abstract partial class SharedBuckleSystem
{
public static ProtoId<AlertCategoryPrototype> BuckledAlertCategory = "Buckled";
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
private void InitializeBuckle()
{
SubscribeLocalEvent<BuckleComponent, ComponentStartup>(OnBuckleComponentStartup);
SubscribeLocalEvent<BuckleComponent, ComponentShutdown>(OnBuckleComponentShutdown);
SubscribeLocalEvent<BuckleComponent, MoveEvent>(OnBuckleMove);
SubscribeLocalEvent<BuckleComponent, InteractHandEvent>(OnBuckleInteractHand);
SubscribeLocalEvent<BuckleComponent, GetVerbsEvent<InteractionVerb>>(AddUnbuckleVerb);
SubscribeLocalEvent<BuckleComponent, EntParentChangedMessage>(OnParentChanged);
SubscribeLocalEvent<BuckleComponent, EntGotInsertedIntoContainerMessage>(OnInserted);
SubscribeLocalEvent<BuckleComponent, StartPullAttemptEvent>(OnPullAttempt);
SubscribeLocalEvent<BuckleComponent, BeingPulledAttemptEvent>(OnBeingPulledAttempt);
SubscribeLocalEvent<BuckleComponent, PullStartedMessage>(OnPullStarted);
SubscribeLocalEvent<BuckleComponent, InsertIntoEntityStorageAttemptEvent>(OnBuckleInsertIntoEntityStorageAttempt);
SubscribeLocalEvent<BuckleComponent, PreventCollideEvent>(OnBucklePreventCollide);
@@ -41,69 +48,93 @@ public abstract partial class SharedBuckleSystem
SubscribeLocalEvent<BuckleComponent, StandAttemptEvent>(OnBuckleStandAttempt);
SubscribeLocalEvent<BuckleComponent, ThrowPushbackAttemptEvent>(OnBuckleThrowPushbackAttempt);
SubscribeLocalEvent<BuckleComponent, UpdateCanMoveEvent>(OnBuckleUpdateCanMove);
SubscribeLocalEvent<BuckleComponent, ComponentGetState>(OnGetState);
}
[ValidatePrototypeId<AlertCategoryPrototype>]
public const string BuckledAlertCategory = "Buckled";
private void OnBuckleComponentStartup(EntityUid uid, BuckleComponent component, ComponentStartup args)
private void OnGetState(Entity<BuckleComponent> ent, ref ComponentGetState args)
{
UpdateBuckleStatus(uid, component);
args.State = new BuckleState(GetNetEntity(ent.Comp.BuckledTo), ent.Comp.DontCollide, ent.Comp.BuckleTime);
}
private void OnBuckleComponentShutdown(EntityUid uid, BuckleComponent component, ComponentShutdown args)
private void OnBuckleComponentShutdown(Entity<BuckleComponent> ent, ref ComponentShutdown args)
{
TryUnbuckle(uid, uid, true, component);
component.BuckleTime = default;
Unbuckle(ent!, null);
}
private void OnBuckleMove(EntityUid uid, BuckleComponent component, ref MoveEvent ev)
#region Pulling
private void OnPullAttempt(Entity<BuckleComponent> ent, ref StartPullAttemptEvent args)
{
if (component.BuckledTo is not { } strapUid)
// Prevent people pulling the chair they're on, etc.
if (ent.Comp.BuckledTo == args.Pulled && !ent.Comp.PullStrap)
args.Cancel();
}
private void OnBeingPulledAttempt(Entity<BuckleComponent> ent, ref BeingPulledAttemptEvent args)
{
if (args.Cancelled || !ent.Comp.Buckled)
return;
if (!CanUnbuckle(ent!, args.Puller, false))
args.Cancel();
}
private void OnPullStarted(Entity<BuckleComponent> ent, ref PullStartedMessage args)
{
Unbuckle(ent!, args.PullerUid);
}
#endregion
#region Transform
private void OnParentChanged(Entity<BuckleComponent> ent, ref EntParentChangedMessage args)
{
BuckleTransformCheck(ent, args.Transform);
}
private void OnInserted(Entity<BuckleComponent> ent, ref EntGotInsertedIntoContainerMessage args)
{
BuckleTransformCheck(ent, Transform(ent));
}
private void OnBuckleMove(Entity<BuckleComponent> ent, ref MoveEvent ev)
{
BuckleTransformCheck(ent, ev.Component);
}
/// <summary>
/// Check if the entity should get unbuckled as a result of transform or container changes.
/// </summary>
private void BuckleTransformCheck(Entity<BuckleComponent> buckle, TransformComponent xform)
{
if (_gameTiming.ApplyingState)
return;
if (buckle.Comp.BuckledTo is not { } strapUid)
return;
if (!TryComp<StrapComponent>(strapUid, out var strapComp))
return;
var strapPosition = Transform(strapUid).Coordinates;
if (ev.NewPosition.EntityId.IsValid() && ev.NewPosition.InRange(EntityManager, _transform, strapPosition, strapComp.MaxBuckleDistance))
return;
TryUnbuckle(uid, uid, true, component);
}
private void OnBuckleInteractHand(EntityUid uid, BuckleComponent component, InteractHandEvent args)
{
if (!component.Buckled)
return;
if (TryUnbuckle(uid, args.User, buckleComp: component))
args.Handled = true;
}
private void AddUnbuckleVerb(EntityUid uid, BuckleComponent component, GetVerbsEvent<InteractionVerb> args)
{
if (!args.CanAccess || !args.CanInteract || !component.Buckled)
return;
InteractionVerb verb = new()
{
Act = () => TryUnbuckle(uid, args.User, buckleComp: component),
Text = Loc.GetString("verb-categories-unbuckle"),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/unbuckle.svg.192dpi.png"))
};
if (args.Target == args.User && args.Using == null)
{
// A user is left clicking themselves with an empty hand, while buckled.
// It is very likely they are trying to unbuckle themselves.
verb.Priority = 1;
Log.Error($"Encountered buckle entity {ToPrettyString(buckle)} without a valid strap entity {ToPrettyString(strapUid)}");
SetBuckledTo(buckle, null);
return;
}
args.Verbs.Add(verb);
if (xform.ParentUid != strapUid || _container.IsEntityInContainer(buckle))
{
Unbuckle(buckle, (strapUid, strapComp), null);
return;
}
var delta = (xform.LocalPosition - strapComp.BuckleOffset).LengthSquared();
if (delta > 1e-5)
Unbuckle(buckle, (strapUid, strapComp), null);
}
#endregion
private void OnBuckleInsertIntoEntityStorageAttempt(EntityUid uid, BuckleComponent component, ref InsertIntoEntityStorageAttemptEvent args)
{
if (component.Buckled)
@@ -112,10 +143,7 @@ public abstract partial class SharedBuckleSystem
private void OnBucklePreventCollide(EntityUid uid, BuckleComponent component, ref PreventCollideEvent args)
{
if (args.OtherEntity != component.BuckledTo)
return;
if (component.Buckled || component.DontCollide)
if (args.OtherEntity == component.BuckledTo && component.DontCollide)
args.Cancelled = true;
}
@@ -139,10 +167,7 @@ public abstract partial class SharedBuckleSystem
private void OnBuckleUpdateCanMove(EntityUid uid, BuckleComponent component, UpdateCanMoveEvent args)
{
if (component.LifeStage > ComponentLifeStage.Running)
return;
if (component.Buckled) // buckle shitcode
if (component.Buckled)
args.Cancel();
}
@@ -151,162 +176,139 @@ public abstract partial class SharedBuckleSystem
return Resolve(uid, ref component, false) && component.Buckled;
}
/// <summary>
/// Shows or hides the buckled status effect depending on if the
/// entity is buckled or not.
/// </summary>
/// <param name="uid"> Entity that we want to show the alert </param>
/// <param name="buckleComp"> buckle component of the entity </param>
/// <param name="strapComp"> strap component of the thing we are strapping to </param>
private void UpdateBuckleStatus(EntityUid uid, BuckleComponent buckleComp, StrapComponent? strapComp = null)
protected void SetBuckledTo(Entity<BuckleComponent> buckle, Entity<StrapComponent?>? strap)
{
Appearance.SetData(uid, StrapVisuals.State, buckleComp.Buckled);
if (buckleComp.BuckledTo != null)
{
if (!Resolve(buckleComp.BuckledTo.Value, ref strapComp))
return;
if (TryComp(buckle.Comp.BuckledTo, out StrapComponent? old))
old.BuckledEntities.Remove(buckle);
var alertType = strapComp.BuckledAlertType;
_alerts.ShowAlert(uid, alertType);
if (strap is {} strapEnt && Resolve(strapEnt.Owner, ref strapEnt.Comp))
{
strapEnt.Comp.BuckledEntities.Add(buckle);
_alerts.ShowAlert(buckle, strapEnt.Comp.BuckledAlertType);
}
else
{
_alerts.ClearAlertCategory(uid, BuckledAlertCategory);
}
}
/// <summary>
/// Sets the <see cref="BuckleComponent.BuckledTo"/> field in the component to a value
/// </summary>
/// <param name="strapUid"> Value tat with be assigned to the field </param>
private void SetBuckledTo(EntityUid buckleUid, EntityUid? strapUid, StrapComponent? strapComp, BuckleComponent buckleComp)
{
buckleComp.BuckledTo = strapUid;
if (strapUid == null)
{
buckleComp.Buckled = false;
}
else
{
buckleComp.LastEntityBuckledTo = strapUid;
buckleComp.DontCollide = true;
buckleComp.Buckled = true;
buckleComp.BuckleTime = _gameTiming.CurTime;
_alerts.ClearAlertCategory(buckle, BuckledAlertCategory);
}
ActionBlocker.UpdateCanMove(buckleUid);
UpdateBuckleStatus(buckleUid, buckleComp, strapComp);
Dirty(buckleUid, buckleComp);
buckle.Comp.BuckledTo = strap;
buckle.Comp.BuckleTime = _gameTiming.CurTime;
ActionBlocker.UpdateCanMove(buckle);
Appearance.SetData(buckle, StrapVisuals.State, buckle.Comp.Buckled);
Dirty(buckle);
}
/// <summary>
/// Checks whether or not buckling is possible
/// </summary>
/// <param name="buckleUid"> Uid of the owner of BuckleComponent </param>
/// <param name="userUid">
/// Uid of a third party entity,
/// i.e, the uid of someone else you are dragging to a chair.
/// Can equal buckleUid sometimes
/// <param name="user">
/// Uid of a third party entity,
/// i.e, the uid of someone else you are dragging to a chair.
/// Can equal buckleUid sometimes
/// </param>
/// <param name="strapUid"> Uid of the owner of strap component </param>
private bool CanBuckle(
EntityUid buckleUid,
EntityUid userUid,
/// <param name="strapComp"></param>
/// <param name="buckleComp"></param>
private bool CanBuckle(EntityUid buckleUid,
EntityUid? user,
EntityUid strapUid,
bool popup,
[NotNullWhen(true)] out StrapComponent? strapComp,
BuckleComponent? buckleComp = null)
BuckleComponent buckleComp)
{
strapComp = null;
if (userUid == strapUid ||
!Resolve(buckleUid, ref buckleComp, false) ||
!Resolve(strapUid, ref strapComp, false))
{
if (!Resolve(strapUid, ref strapComp, false))
return false;
}
// Does it pass the Whitelist
if (_whitelistSystem.IsWhitelistFail(strapComp.Whitelist, buckleUid) ||
_whitelistSystem.IsBlacklistPass(strapComp.Blacklist, buckleUid))
{
if (_netManager.IsServer)
_popup.PopupEntity(Loc.GetString("buckle-component-cannot-fit-message"), userUid, buckleUid, PopupType.Medium);
if (_netManager.IsServer && popup && user != null)
_popup.PopupEntity(Loc.GetString("buckle-component-cannot-fit-message"), user.Value, user.Value, PopupType.Medium);
return false;
}
// Is it within range
bool Ignored(EntityUid entity) => entity == buckleUid || entity == userUid || entity == strapUid;
if (!_interaction.InRangeUnobstructed(buckleUid, strapUid, buckleComp.Range, predicate: Ignored,
if (!_interaction.InRangeUnobstructed(buckleUid,
strapUid,
buckleComp.Range,
predicate: entity => entity == buckleUid || entity == user || entity == strapUid,
popup: true))
{
return false;
}
// If in a container
if (_container.TryGetContainingContainer(buckleUid, out var ownerContainer))
{
// And not in the same container as the strap
if (!_container.TryGetContainingContainer(strapUid, out var strapContainer) ||
ownerContainer != strapContainer)
{
return false;
}
}
if (!_container.IsInSameOrNoContainer((buckleUid, null, null), (strapUid, null, null)))
return false;
if (!HasComp<HandsComponent>(userUid))
if (user != null && !HasComp<HandsComponent>(user))
{
// PopupPredicted when
if (_netManager.IsServer)
_popup.PopupEntity(Loc.GetString("buckle-component-no-hands-message"), userUid, userUid);
if (_netManager.IsServer && popup)
_popup.PopupEntity(Loc.GetString("buckle-component-no-hands-message"), user.Value, user.Value);
return false;
}
if (buckleComp.Buckled)
{
var message = Loc.GetString(buckleUid == userUid
if (_netManager.IsClient || popup || user == null)
return false;
var message = Loc.GetString(buckleUid == user
? "buckle-component-already-buckled-message"
: "buckle-component-other-already-buckled-message",
("owner", Identity.Entity(buckleUid, EntityManager)));
if (_netManager.IsServer)
_popup.PopupEntity(message, userUid, userUid);
_popup.PopupEntity(message, user.Value, user.Value);
return false;
}
// Check whether someone is attempting to buckle something to their own child
var parent = Transform(strapUid).ParentUid;
while (parent.IsValid())
{
if (parent == userUid)
if (parent != buckleUid)
{
var message = Loc.GetString(buckleUid == userUid
? "buckle-component-cannot-buckle-message"
: "buckle-component-other-cannot-buckle-message", ("owner", Identity.Entity(buckleUid, EntityManager)));
if (_netManager.IsServer)
_popup.PopupEntity(message, userUid, userUid);
return false;
parent = Transform(parent).ParentUid;
continue;
}
parent = Transform(parent).ParentUid;
if (_netManager.IsClient || popup || user == null)
return false;
var message = Loc.GetString(buckleUid == user
? "buckle-component-cannot-buckle-message"
: "buckle-component-other-cannot-buckle-message",
("owner", Identity.Entity(buckleUid, EntityManager)));
_popup.PopupEntity(message, user.Value, user.Value);
return false;
}
if (!StrapHasSpace(strapUid, buckleComp, strapComp))
{
var message = Loc.GetString(buckleUid == userUid
? "buckle-component-cannot-fit-message"
: "buckle-component-other-cannot-fit-message", ("owner", Identity.Entity(buckleUid, EntityManager)));
if (_netManager.IsServer)
_popup.PopupEntity(message, userUid, userUid);
if (_netManager.IsClient || popup || user == null)
return false;
var message = Loc.GetString(buckleUid == user
? "buckle-component-cannot-fit-message"
: "buckle-component-other-cannot-fit-message",
("owner", Identity.Entity(buckleUid, EntityManager)));
_popup.PopupEntity(message, user.Value, user.Value);
return false;
}
var attemptEvent = new BuckleAttemptEvent(strapUid, buckleUid, userUid, true);
RaiseLocalEvent(attemptEvent.BuckledEntity, ref attemptEvent);
RaiseLocalEvent(attemptEvent.StrapEntity, ref attemptEvent);
if (attemptEvent.Cancelled)
var buckleAttempt = new BuckleAttemptEvent((strapUid, strapComp), (buckleUid, buckleComp), user, popup);
RaiseLocalEvent(buckleUid, ref buckleAttempt);
if (buckleAttempt.Cancelled)
return false;
var strapAttempt = new StrapAttemptEvent((strapUid, strapComp), (buckleUid, buckleComp), user, popup);
RaiseLocalEvent(strapUid, ref strapAttempt);
if (strapAttempt.Cancelled)
return false;
return true;
@@ -315,216 +317,194 @@ public abstract partial class SharedBuckleSystem
/// <summary>
/// Attempts to buckle an entity to a strap
/// </summary>
/// <param name="buckleUid"> Uid of the owner of BuckleComponent </param>
/// <param name="userUid">
/// <param name="buckle"> Uid of the owner of BuckleComponent </param>
/// <param name="user">
/// Uid of a third party entity,
/// i.e, the uid of someone else you are dragging to a chair.
/// Can equal buckleUid sometimes
/// </param>
/// <param name="strapUid"> Uid of the owner of strap component </param>
public bool TryBuckle(EntityUid buckleUid, EntityUid userUid, EntityUid strapUid, BuckleComponent? buckleComp = null)
/// <param name="strap"> Uid of the owner of strap component </param>
public bool TryBuckle(EntityUid buckle, EntityUid? user, EntityUid strap, BuckleComponent? buckleComp = null, bool popup = true)
{
if (!Resolve(buckleUid, ref buckleComp, false))
if (!Resolve(buckle, ref buckleComp, false))
return false;
if (!CanBuckle(buckleUid, userUid, strapUid, out var strapComp, buckleComp))
if (!CanBuckle(buckle, user, strap, popup, out var strapComp, buckleComp))
return false;
if (!StrapTryAdd(strapUid, buckleUid, buckleComp, false, strapComp))
{
var message = Loc.GetString(buckleUid == userUid
? "buckle-component-cannot-buckle-message"
: "buckle-component-other-cannot-buckle-message", ("owner", Identity.Entity(buckleUid, EntityManager)));
if (_netManager.IsServer)
_popup.PopupEntity(message, userUid, userUid);
return false;
}
if (TryComp<AppearanceComponent>(buckleUid, out var appearance))
Appearance.SetData(buckleUid, BuckleVisuals.Buckled, true, appearance);
_rotationVisuals.SetHorizontalAngle(buckleUid, strapComp.Rotation);
ReAttach(buckleUid, strapUid, buckleComp, strapComp);
SetBuckledTo(buckleUid, strapUid, strapComp, buckleComp);
// TODO user is currently set to null because if it isn't the sound fails to play in some situations, fix that
_audio.PlayPredicted(strapComp.BuckleSound, strapUid, userUid);
var ev = new BuckleChangeEvent(strapUid, buckleUid, true);
RaiseLocalEvent(ev.BuckledEntity, ref ev);
RaiseLocalEvent(ev.StrapEntity, ref ev);
if (TryComp<PullableComponent>(buckleUid, out var ownerPullable))
{
if (ownerPullable.Puller != null)
{
_pulling.TryStopPull(buckleUid, ownerPullable);
}
}
if (TryComp<PhysicsComponent>(buckleUid, out var physics))
{
_physics.ResetDynamics(buckleUid, physics);
}
if (!buckleComp.PullStrap && TryComp<PullableComponent>(strapUid, out var toPullable))
{
if (toPullable.Puller == buckleUid)
{
// can't pull it and buckle to it at the same time
_pulling.TryStopPull(strapUid, toPullable);
}
}
// Logging
if (userUid != buckleUid)
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} buckled {ToPrettyString(buckleUid)} to {ToPrettyString(strapUid)}");
else
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} buckled themselves to {ToPrettyString(strapUid)}");
Buckle((buckle, buckleComp), (strap, strapComp), user);
return true;
}
private void Buckle(Entity<BuckleComponent> buckle, Entity<StrapComponent> strap, EntityUid? user)
{
if (user == buckle.Owner)
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):player} buckled themselves to {ToPrettyString(strap)}");
else if (user != null)
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):player} buckled {ToPrettyString(buckle)} to {ToPrettyString(strap)}");
_audio.PlayPredicted(strap.Comp.BuckleSound, strap, user);
SetBuckledTo(buckle, strap!);
Appearance.SetData(strap, StrapVisuals.State, true);
Appearance.SetData(buckle, BuckleVisuals.Buckled, true);
_rotationVisuals.SetHorizontalAngle(buckle.Owner, strap.Comp.Rotation);
var xform = Transform(buckle);
var coords = new EntityCoordinates(strap, strap.Comp.BuckleOffset);
_transform.SetCoordinates(buckle, xform, coords, rotation: Angle.Zero);
_joints.SetRelay(buckle, strap);
switch (strap.Comp.Position)
{
case StrapPosition.Stand:
_standing.Stand(buckle);
break;
case StrapPosition.Down:
_standing.Down(buckle, false, false);
break;
}
var ev = new StrappedEvent(strap, buckle);
RaiseLocalEvent(strap, ref ev);
var gotEv = new BuckledEvent(strap, buckle);
RaiseLocalEvent(buckle, ref gotEv);
if (TryComp<PhysicsComponent>(buckle, out var physics))
_physics.ResetDynamics(buckle, physics);
DebugTools.AssertEqual(xform.ParentUid, strap.Owner);
}
/// <summary>
/// Tries to unbuckle the Owner of this component from its current strap.
/// </summary>
/// <param name="buckleUid">The entity to unbuckle.</param>
/// <param name="userUid">The entity doing the unbuckling.</param>
/// <param name="force">
/// Whether to force the unbuckling or not. Does not guarantee true to
/// be returned, but guarantees the owner to be unbuckled afterwards.
/// </param>
/// <param name="user">The entity doing the unbuckling.</param>
/// <param name="buckleComp">The buckle component of the entity to unbuckle.</param>
/// <returns>
/// true if the owner was unbuckled, otherwise false even if the owner
/// was previously already unbuckled.
/// </returns>
public bool TryUnbuckle(EntityUid buckleUid, EntityUid userUid, bool force = false, BuckleComponent? buckleComp = null)
public bool TryUnbuckle(EntityUid buckleUid,
EntityUid? user,
BuckleComponent? buckleComp = null,
bool popup = true)
{
if (!Resolve(buckleUid, ref buckleComp, false) ||
buckleComp.BuckledTo is not { } strapUid)
return TryUnbuckle((buckleUid, buckleComp), user, popup);
}
public bool TryUnbuckle(Entity<BuckleComponent?> buckle, EntityUid? user, bool popup)
{
if (!Resolve(buckle.Owner, ref buckle.Comp))
return false;
if (!force)
{
var attemptEvent = new BuckleAttemptEvent(strapUid, buckleUid, userUid, false);
RaiseLocalEvent(attemptEvent.BuckledEntity, ref attemptEvent);
RaiseLocalEvent(attemptEvent.StrapEntity, ref attemptEvent);
if (attemptEvent.Cancelled)
return false;
if (_gameTiming.CurTime < buckleComp.BuckleTime + buckleComp.Delay)
return false;
if (!_interaction.InRangeUnobstructed(userUid, strapUid, buckleComp.Range, popup: true))
return false;
if (HasComp<SleepingComponent>(buckleUid) && buckleUid == userUid)
return false;
// If the person is crit or dead in any kind of strap, return. This prevents people from unbuckling themselves while incapacitated.
if (_mobState.IsIncapacitated(buckleUid) && userUid == buckleUid)
return false;
}
// Logging
if (userUid != buckleUid)
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} unbuckled {ToPrettyString(buckleUid)} from {ToPrettyString(strapUid)}");
else
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(userUid):player} unbuckled themselves from {ToPrettyString(strapUid)}");
SetBuckledTo(buckleUid, null, null, buckleComp);
if (!TryComp<StrapComponent>(strapUid, out var strapComp))
if (!CanUnbuckle(buckle, user, popup, out var strap))
return false;
var buckleXform = Transform(buckleUid);
var oldBuckledXform = Transform(strapUid);
if (buckleXform.ParentUid == strapUid && !Terminating(buckleXform.ParentUid))
{
_container.AttachParentToContainerOrGrid((buckleUid, buckleXform));
var oldBuckledToWorldRot = _transform.GetWorldRotation(strapUid);
_transform.SetWorldRotation(buckleXform, oldBuckledToWorldRot);
if (strapComp.UnbuckleOffset != Vector2.Zero)
buckleXform.Coordinates = oldBuckledXform.Coordinates.Offset(strapComp.UnbuckleOffset);
}
if (TryComp(buckleUid, out AppearanceComponent? appearance))
Appearance.SetData(buckleUid, BuckleVisuals.Buckled, false, appearance);
_rotationVisuals.ResetHorizontalAngle(buckleUid);
if (TryComp<MobStateComponent>(buckleUid, out var mobState)
&& _mobState.IsIncapacitated(buckleUid, mobState)
|| HasComp<KnockedDownComponent>(buckleUid))
{
_standing.Down(buckleUid);
}
else
{
_standing.Stand(buckleUid);
}
if (_mobState.IsIncapacitated(buckleUid, mobState))
{
_standing.Down(buckleUid);
}
if (strapComp.BuckledEntities.Remove(buckleUid))
{
strapComp.OccupiedSize -= buckleComp.Size;
Dirty(strapUid, strapComp);
}
_joints.RefreshRelay(buckleUid);
Appearance.SetData(strapUid, StrapVisuals.State, strapComp.BuckledEntities.Count != 0);
// TODO: Buckle listening to moveevents is sussy anyway.
if (!TerminatingOrDeleted(strapUid))
_audio.PlayPredicted(strapComp.UnbuckleSound, strapUid, userUid);
var ev = new BuckleChangeEvent(strapUid, buckleUid, false);
RaiseLocalEvent(buckleUid, ref ev);
RaiseLocalEvent(strapUid, ref ev);
Unbuckle(buckle!, strap, user);
return true;
}
/// <summary>
/// Makes an entity toggle the buckling status of the owner to a
/// specific entity.
/// </summary>
/// <param name="buckleUid">The entity to buckle/unbuckle from <see cref="to"/>.</param>
/// <param name="userUid">The entity doing the buckling/unbuckling.</param>
/// <param name="strapUid">
/// The entity to toggle the buckle status of the owner to.
/// </param>
/// <param name="force">
/// Whether to force the unbuckling or not, if it happens. Does not
/// guarantee true to be returned, but guarantees the owner to be
/// unbuckled afterwards.
/// </param>
/// <param name="buckle">The buckle component of the entity to buckle/unbuckle from <see cref="to"/>.</param>
/// <returns>true if the buckling status was changed, false otherwise.</returns>
public bool ToggleBuckle(
EntityUid buckleUid,
EntityUid userUid,
EntityUid strapUid,
bool force = false,
BuckleComponent? buckle = null)
public void Unbuckle(Entity<BuckleComponent?> buckle, EntityUid? user)
{
if (!Resolve(buckleUid, ref buckle, false))
if (!Resolve(buckle.Owner, ref buckle.Comp, false))
return;
if (buckle.Comp.BuckledTo is not { } strap)
return;
if (!TryComp(strap, out StrapComponent? strapComp))
{
Log.Error($"Encountered buckle {ToPrettyString(buckle.Owner)} with invalid strap entity {ToPrettyString(strap)}");
SetBuckledTo(buckle!, null);
return;
}
Unbuckle(buckle!, (strap, strapComp), user);
}
private void Unbuckle(Entity<BuckleComponent> buckle, Entity<StrapComponent> strap, EntityUid? user)
{
if (user == buckle.Owner)
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{user} unbuckled themselves from {strap}");
else if (user != null)
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{user} unbuckled {buckle} from {strap}");
_audio.PlayPredicted(strap.Comp.UnbuckleSound, strap, user);
SetBuckledTo(buckle, null);
var buckleXform = Transform(buckle);
var oldBuckledXform = Transform(strap);
if (buckleXform.ParentUid == strap.Owner && !Terminating(buckleXform.ParentUid))
{
_container.AttachParentToContainerOrGrid((buckle, buckleXform));
var oldBuckledToWorldRot = _transform.GetWorldRotation(strap);
_transform.SetWorldRotation(buckleXform, oldBuckledToWorldRot);
if (strap.Comp.UnbuckleOffset != Vector2.Zero)
buckleXform.Coordinates = oldBuckledXform.Coordinates.Offset(strap.Comp.UnbuckleOffset);
}
_rotationVisuals.ResetHorizontalAngle(buckle.Owner);
Appearance.SetData(strap, StrapVisuals.State, strap.Comp.BuckledEntities.Count != 0);
Appearance.SetData(buckle, BuckleVisuals.Buckled, false);
if (HasComp<KnockedDownComponent>(buckle) || _mobState.IsIncapacitated(buckle))
_standing.Down(buckle);
else
_standing.Stand(buckle);
_joints.RefreshRelay(buckle);
var buckleEv = new UnbuckledEvent(strap, buckle);
RaiseLocalEvent(buckle, ref buckleEv);
var strapEv = new UnstrappedEvent(strap, buckle);
RaiseLocalEvent(strap, ref strapEv);
}
public bool CanUnbuckle(Entity<BuckleComponent?> buckle, EntityUid user, bool popup)
{
return CanUnbuckle(buckle, user, popup, out _);
}
private bool CanUnbuckle(Entity<BuckleComponent?> buckle, EntityUid? user, bool popup, out Entity<StrapComponent> strap)
{
strap = default;
if (!Resolve(buckle.Owner, ref buckle.Comp))
return false;
if (!buckle.Buckled)
if (buckle.Comp.BuckledTo is not { } strapUid)
return false;
if (!TryComp(strapUid, out StrapComponent? strapComp))
{
return TryBuckle(buckleUid, userUid, strapUid, buckle);
}
else
{
return TryUnbuckle(buckleUid, userUid, force, buckle);
Log.Error($"Encountered buckle {ToPrettyString(buckle.Owner)} with invalid strap entity {ToPrettyString(strap)}");
SetBuckledTo(buckle!, null);
return false;
}
strap = (strapUid, strapComp);
if (_gameTiming.CurTime < buckle.Comp.BuckleTime + buckle.Comp.Delay)
return false;
if (user != null && !_interaction.InRangeUnobstructed(user.Value, strap.Owner, buckle.Comp.Range, popup: popup))
return false;
var unbuckleAttempt = new UnbuckleAttemptEvent(strap, buckle!, user, popup);
RaiseLocalEvent(buckle, ref unbuckleAttempt);
if (unbuckleAttempt.Cancelled)
return false;
var unstrapAttempt = new UnstrapAttemptEvent(strap, buckle!, user, popup);
RaiseLocalEvent(strap, ref unstrapAttempt);
return !unstrapAttempt.Cancelled;
}
}

View File

@@ -0,0 +1,188 @@
using Content.Shared.Buckle.Components;
using Content.Shared.DragDrop;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Verbs;
using Robust.Shared.Utility;
namespace Content.Shared.Buckle;
// Partial class containing interaction & verb event handlers
public abstract partial class SharedBuckleSystem
{
private void InitializeInteraction()
{
SubscribeLocalEvent<StrapComponent, GetVerbsEvent<InteractionVerb>>(AddStrapVerbs);
SubscribeLocalEvent<StrapComponent, InteractHandEvent>(OnStrapInteractHand, after: [typeof(InteractionPopupSystem)]);
SubscribeLocalEvent<StrapComponent, DragDropTargetEvent>(OnStrapDragDropTarget);
SubscribeLocalEvent<StrapComponent, CanDropTargetEvent>(OnCanDropTarget);
SubscribeLocalEvent<BuckleComponent, InteractHandEvent>(OnBuckleInteractHand, after: [typeof(InteractionPopupSystem)]);
SubscribeLocalEvent<BuckleComponent, GetVerbsEvent<InteractionVerb>>(AddUnbuckleVerb);
}
private void OnCanDropTarget(EntityUid uid, StrapComponent component, ref CanDropTargetEvent args)
{
args.CanDrop = StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component);
args.Handled = true;
}
private void OnStrapDragDropTarget(EntityUid uid, StrapComponent component, ref DragDropTargetEvent args)
{
if (!StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component))
return;
args.Handled = TryBuckle(args.Dragged, args.User, uid, popup: false);
}
private bool StrapCanDragDropOn(
EntityUid strapUid,
EntityUid userUid,
EntityUid targetUid,
EntityUid buckleUid,
StrapComponent? strapComp = null,
BuckleComponent? buckleComp = null)
{
if (!Resolve(strapUid, ref strapComp, false) ||
!Resolve(buckleUid, ref buckleComp, false))
{
return false;
}
bool Ignored(EntityUid entity) => entity == userUid || entity == buckleUid || entity == targetUid;
return _interaction.InRangeUnobstructed(targetUid, buckleUid, buckleComp.Range, predicate: Ignored);
}
private void OnStrapInteractHand(EntityUid uid, StrapComponent component, InteractHandEvent args)
{
if (args.Handled)
return;
if (!component.Enabled)
return;
if (!TryComp(args.User, out BuckleComponent? buckle))
return;
if (buckle.BuckledTo == null)
TryBuckle(args.User, args.User, uid, buckle, popup: true);
else if (buckle.BuckledTo == uid)
TryUnbuckle(args.User, args.User, buckle, popup: true);
else
return;
// TODO BUCKLE add out bool for whether a pop-up was generated or not.
args.Handled = true;
}
private void OnBuckleInteractHand(Entity<BuckleComponent> ent, ref InteractHandEvent args)
{
if (args.Handled)
return;
if (ent.Comp.BuckledTo != null)
TryUnbuckle(ent!, args.User, popup: true);
// TODO BUCKLE add out bool for whether a pop-up was generated or not.
args.Handled = true;
}
private void AddStrapVerbs(EntityUid uid, StrapComponent component, GetVerbsEvent<InteractionVerb> args)
{
if (args.Hands == null || !args.CanAccess || !args.CanInteract || !component.Enabled)
return;
// Note that for whatever bloody reason, buckle component has its own interaction range. Additionally, this
// range can be set per-component, so we have to check a modified InRangeUnobstructed for every verb.
// Add unstrap verbs for every strapped entity.
foreach (var entity in component.BuckledEntities)
{
var buckledComp = Comp<BuckleComponent>(entity);
if (!_interaction.InRangeUnobstructed(args.User, args.Target, range: buckledComp.Range))
continue;
var verb = new InteractionVerb()
{
Act = () => TryUnbuckle(entity, args.User, buckleComp: buckledComp),
Category = VerbCategory.Unbuckle,
Text = entity == args.User
? Loc.GetString("verb-self-target-pronoun")
: Identity.Name(entity, EntityManager)
};
// In the event that you have more than once entity with the same name strapped to the same object,
// these two verbs will be identical according to Verb.CompareTo, and only one with actually be added to
// the verb list. However this should rarely ever be a problem. If it ever is, it could be fixed by
// appending an integer to verb.Text to distinguish the verbs.
args.Verbs.Add(verb);
}
// Add a verb to buckle the user.
if (TryComp<BuckleComponent>(args.User, out var buckle) &&
buckle.BuckledTo != uid &&
args.User != uid &&
StrapHasSpace(uid, buckle, component) &&
_interaction.InRangeUnobstructed(args.User, args.Target, range: buckle.Range))
{
InteractionVerb verb = new()
{
Act = () => TryBuckle(args.User, args.User, args.Target, buckle),
Category = VerbCategory.Buckle,
Text = Loc.GetString("verb-self-target-pronoun")
};
args.Verbs.Add(verb);
}
// If the user is currently holding/pulling an entity that can be buckled, add a verb for that.
if (args.Using is { Valid: true } @using &&
TryComp<BuckleComponent>(@using, out var usingBuckle) &&
StrapHasSpace(uid, usingBuckle, component) &&
_interaction.InRangeUnobstructed(@using, args.Target, range: usingBuckle.Range))
{
// Check that the entity is unobstructed from the target (ignoring the user).
bool Ignored(EntityUid entity) => entity == args.User || entity == args.Target || entity == @using;
if (!_interaction.InRangeUnobstructed(@using, args.Target, usingBuckle.Range, predicate: Ignored))
return;
var isPlayer = _playerManager.TryGetSessionByEntity(@using, out var _);
InteractionVerb verb = new()
{
Act = () => TryBuckle(@using, args.User, args.Target, usingBuckle),
Category = VerbCategory.Buckle,
Text = Identity.Name(@using, EntityManager),
// just a held object, the user is probably just trying to sit down.
// If the used entity is a person being pulled, prioritize this verb. Conversely, if it is
Priority = isPlayer ? 1 : -1
};
args.Verbs.Add(verb);
}
}
private void AddUnbuckleVerb(EntityUid uid, BuckleComponent component, GetVerbsEvent<InteractionVerb> args)
{
if (!args.CanAccess || !args.CanInteract || !component.Buckled)
return;
InteractionVerb verb = new()
{
Act = () => TryUnbuckle(uid, args.User, buckleComp: component),
Text = Loc.GetString("verb-categories-unbuckle"),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/unbuckle.svg.192dpi.png"))
};
if (args.Target == args.User && args.Using == null)
{
// A user is left clicking themselves with an empty hand, while buckled.
// It is very likely they are trying to unbuckle themselves.
verb.Priority = 1;
}
args.Verbs.Add(verb);
}
}

View File

@@ -2,39 +2,25 @@
using Content.Shared.Buckle.Components;
using Content.Shared.Construction;
using Content.Shared.Destructible;
using Content.Shared.DragDrop;
using Content.Shared.Foldable;
using Content.Shared.Interaction;
using Content.Shared.Rotation;
using Content.Shared.Storage;
using Content.Shared.Verbs;
using Robust.Shared.Containers;
namespace Content.Shared.Buckle;
public abstract partial class SharedBuckleSystem
{
[Dependency] private readonly SharedRotationVisualsSystem _rotationVisuals = default!;
private void InitializeStrap()
{
SubscribeLocalEvent<StrapComponent, ComponentStartup>(OnStrapStartup);
SubscribeLocalEvent<StrapComponent, ComponentShutdown>(OnStrapShutdown);
SubscribeLocalEvent<StrapComponent, ComponentRemove>((e, c, _) => StrapRemoveAll(e, c));
SubscribeLocalEvent<StrapComponent, EntInsertedIntoContainerMessage>(OnStrapEntModifiedFromContainer);
SubscribeLocalEvent<StrapComponent, EntRemovedFromContainerMessage>(OnStrapEntModifiedFromContainer);
SubscribeLocalEvent<StrapComponent, GetVerbsEvent<InteractionVerb>>(AddStrapVerbs);
SubscribeLocalEvent<StrapComponent, ContainerGettingInsertedAttemptEvent>(OnStrapContainerGettingInsertedAttempt);
SubscribeLocalEvent<StrapComponent, InteractHandEvent>(OnStrapInteractHand);
SubscribeLocalEvent<StrapComponent, DestructionEventArgs>((e, c, _) => StrapRemoveAll(e, c));
SubscribeLocalEvent<StrapComponent, BreakageEventArgs>((e, c, _) => StrapRemoveAll(e, c));
SubscribeLocalEvent<StrapComponent, DragDropTargetEvent>(OnStrapDragDropTarget);
SubscribeLocalEvent<StrapComponent, CanDropTargetEvent>(OnCanDropTarget);
SubscribeLocalEvent<StrapComponent, FoldAttemptEvent>(OnAttemptFold);
SubscribeLocalEvent<StrapComponent, MoveEvent>(OnStrapMoveEvent);
SubscribeLocalEvent<StrapComponent, MachineDeconstructedEvent>((e, c, _) => StrapRemoveAll(e, c));
}
@@ -45,145 +31,17 @@ public abstract partial class SharedBuckleSystem
private void OnStrapShutdown(EntityUid uid, StrapComponent component, ComponentShutdown args)
{
if (LifeStage(uid) > EntityLifeStage.MapInitialized)
return;
StrapRemoveAll(uid, component);
}
private void OnStrapEntModifiedFromContainer(EntityUid uid, StrapComponent component, ContainerModifiedMessage message)
{
if (_gameTiming.ApplyingState)
return;
foreach (var buckledEntity in component.BuckledEntities)
{
if (!TryComp<BuckleComponent>(buckledEntity, out var buckleComp))
{
continue;
}
ContainerModifiedReAttach(buckledEntity, uid, buckleComp, component);
}
}
private void ContainerModifiedReAttach(EntityUid buckleUid, EntityUid strapUid, BuckleComponent? buckleComp = null, StrapComponent? strapComp = null)
{
if (!Resolve(buckleUid, ref buckleComp, false) ||
!Resolve(strapUid, ref strapComp, false))
return;
var contained = _container.TryGetContainingContainer(buckleUid, out var ownContainer);
var strapContained = _container.TryGetContainingContainer(strapUid, out var strapContainer);
if (contained != strapContained || ownContainer != strapContainer)
{
TryUnbuckle(buckleUid, buckleUid, true, buckleComp);
return;
}
if (!contained)
{
ReAttach(buckleUid, strapUid, buckleComp, strapComp);
}
if (!TerminatingOrDeleted(uid))
StrapRemoveAll(uid, component);
}
private void OnStrapContainerGettingInsertedAttempt(EntityUid uid, StrapComponent component, ContainerGettingInsertedAttemptEvent args)
{
// If someone is attempting to put this item inside of a backpack, ensure that it has no entities strapped to it.
if (HasComp<StorageComponent>(args.Container.Owner) && component.BuckledEntities.Count != 0)
if (args.Container.ID == StorageComponent.ContainerId && component.BuckledEntities.Count != 0)
args.Cancel();
}
private void OnStrapInteractHand(EntityUid uid, StrapComponent component, InteractHandEvent args)
{
if (args.Handled)
return;
args.Handled = ToggleBuckle(args.User, args.User, uid);
}
private void AddStrapVerbs(EntityUid uid, StrapComponent component, GetVerbsEvent<InteractionVerb> args)
{
if (args.Hands == null || !args.CanAccess || !args.CanInteract || !component.Enabled)
return;
// Note that for whatever bloody reason, buckle component has its own interaction range. Additionally, this
// range can be set per-component, so we have to check a modified InRangeUnobstructed for every verb.
// Add unstrap verbs for every strapped entity.
foreach (var entity in component.BuckledEntities)
{
var buckledComp = Comp<BuckleComponent>(entity);
if (!_interaction.InRangeUnobstructed(args.User, args.Target, range: buckledComp.Range))
continue;
var verb = new InteractionVerb()
{
Act = () => TryUnbuckle(entity, args.User, buckleComp: buckledComp),
Category = VerbCategory.Unbuckle,
Text = entity == args.User
? Loc.GetString("verb-self-target-pronoun")
: Comp<MetaDataComponent>(entity).EntityName
};
// In the event that you have more than once entity with the same name strapped to the same object,
// these two verbs will be identical according to Verb.CompareTo, and only one with actually be added to
// the verb list. However this should rarely ever be a problem. If it ever is, it could be fixed by
// appending an integer to verb.Text to distinguish the verbs.
args.Verbs.Add(verb);
}
// Add a verb to buckle the user.
if (TryComp<BuckleComponent>(args.User, out var buckle) &&
buckle.BuckledTo != uid &&
args.User != uid &&
StrapHasSpace(uid, buckle, component) &&
_interaction.InRangeUnobstructed(args.User, args.Target, range: buckle.Range))
{
InteractionVerb verb = new()
{
Act = () => TryBuckle(args.User, args.User, args.Target, buckle),
Category = VerbCategory.Buckle,
Text = Loc.GetString("verb-self-target-pronoun")
};
args.Verbs.Add(verb);
}
// If the user is currently holding/pulling an entity that can be buckled, add a verb for that.
if (args.Using is { Valid: true } @using &&
TryComp<BuckleComponent>(@using, out var usingBuckle) &&
StrapHasSpace(uid, usingBuckle, component) &&
_interaction.InRangeUnobstructed(@using, args.Target, range: usingBuckle.Range))
{
// Check that the entity is unobstructed from the target (ignoring the user).
bool Ignored(EntityUid entity) => entity == args.User || entity == args.Target || entity == @using;
if (!_interaction.InRangeUnobstructed(@using, args.Target, usingBuckle.Range, predicate: Ignored))
return;
var isPlayer = _playerManager.TryGetSessionByEntity(@using, out var _);
InteractionVerb verb = new()
{
Act = () => TryBuckle(@using, args.User, args.Target, usingBuckle),
Category = VerbCategory.Buckle,
Text = Comp<MetaDataComponent>(@using).EntityName,
// just a held object, the user is probably just trying to sit down.
// If the used entity is a person being pulled, prioritize this verb. Conversely, if it is
Priority = isPlayer ? 1 : -1
};
args.Verbs.Add(verb);
}
}
private void OnCanDropTarget(EntityUid uid, StrapComponent component, ref CanDropTargetEvent args)
{
args.CanDrop = StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component);
args.Handled = true;
}
private void OnAttemptFold(EntityUid uid, StrapComponent component, ref FoldAttemptEvent args)
{
if (args.Cancelled)
@@ -192,69 +50,6 @@ public abstract partial class SharedBuckleSystem
args.Cancelled = component.BuckledEntities.Count != 0;
}
private void OnStrapDragDropTarget(EntityUid uid, StrapComponent component, ref DragDropTargetEvent args)
{
if (!StrapCanDragDropOn(uid, args.User, uid, args.Dragged, component))
return;
args.Handled = TryBuckle(args.Dragged, args.User, uid);
}
private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveEvent args)
{
// TODO: This looks dirty af.
// On rotation of a strap, reattach all buckled entities.
// This fixes buckle offsets and draw depths.
// This is mega cursed. Please somebody save me from Mr Buckle's wild ride.
// Oh god I'm back here again. Send help.
// Consider a chair that has a player strapped to it. Then the client receives a new server state, showing
// that the player entity has moved elsewhere, and the chair has rotated. If the client applies the player
// state, then the chairs transform comp state, and then the buckle state. The transform state will
// forcefully teleport the player back to the chair (client-side only). This causes even more issues if the
// chair was teleporting in from nullspace after having left PVS.
//
// One option is to just never trigger re-buckles during state application.
// another is to.. just not do this? Like wtf is this code. But I CBF with buckle atm.
if (_gameTiming.ApplyingState || args.NewRotation == args.OldRotation)
return;
foreach (var buckledEntity in component.BuckledEntities)
{
if (!TryComp<BuckleComponent>(buckledEntity, out var buckled))
continue;
if (!buckled.Buckled || buckled.LastEntityBuckledTo != uid)
{
Log.Error($"A moving strap entity {ToPrettyString(uid)} attempted to re-parent an entity that does not 'belong' to it {ToPrettyString(buckledEntity)}");
continue;
}
ReAttach(buckledEntity, uid, buckled, component);
Dirty(buckledEntity, buckled);
}
}
private bool StrapCanDragDropOn(
EntityUid strapUid,
EntityUid userUid,
EntityUid targetUid,
EntityUid buckleUid,
StrapComponent? strapComp = null,
BuckleComponent? buckleComp = null)
{
if (!Resolve(strapUid, ref strapComp, false) ||
!Resolve(buckleUid, ref buckleComp, false))
{
return false;
}
bool Ignored(EntityUid entity) => entity == userUid || entity == buckleUid || entity == targetUid;
return _interaction.InRangeUnobstructed(targetUid, buckleUid, buckleComp.Range, predicate: Ignored);
}
/// <summary>
/// Remove everything attached to the strap
/// </summary>
@@ -264,10 +59,6 @@ public abstract partial class SharedBuckleSystem
{
TryUnbuckle(entity, entity, true);
}
strapComp.BuckledEntities.Clear();
strapComp.OccupiedSize = 0;
Dirty(uid, strapComp);
}
private bool StrapHasSpace(EntityUid strapUid, BuckleComponent buckleComp, StrapComponent? strapComp = null)
@@ -275,30 +66,13 @@ public abstract partial class SharedBuckleSystem
if (!Resolve(strapUid, ref strapComp, false))
return false;
return strapComp.OccupiedSize + buckleComp.Size <= strapComp.Size;
}
var avail = strapComp.Size;
foreach (var buckle in strapComp.BuckledEntities)
{
avail -= CompOrNull<BuckleComponent>(buckle)?.Size ?? 0;
}
/// <summary>
/// Try to add an entity to the strap
/// </summary>
private bool StrapTryAdd(EntityUid strapUid, EntityUid buckleUid, BuckleComponent buckleComp, bool force = false, StrapComponent? strapComp = null)
{
if (!Resolve(strapUid, ref strapComp, false) ||
!strapComp.Enabled)
return false;
if (!force && !StrapHasSpace(strapUid, buckleComp, strapComp))
return false;
if (!strapComp.BuckledEntities.Add(buckleUid))
return false;
strapComp.OccupiedSize += buckleComp.Size;
Appearance.SetData(strapUid, StrapVisuals.State, true);
Dirty(strapUid, strapComp);
return true;
return avail >= buckleComp.Size;
}
/// <summary>
@@ -311,6 +85,7 @@ public abstract partial class SharedBuckleSystem
return;
strapComp.Enabled = enabled;
Dirty(strapUid, strapComp);
if (!enabled)
StrapRemoveAll(strapUid, strapComp);

View File

@@ -1,21 +1,17 @@
using Content.Shared.ActionBlocker;
using Content.Shared.Administration.Logs;
using Content.Shared.Alert;
using Content.Shared.Buckle.Components;
using Content.Shared.Interaction;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Pulling;
using Content.Shared.Rotation;
using Content.Shared.Standing;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Timing;
using PullingSystem = Content.Shared.Movement.Pulling.Systems.PullingSystem;
namespace Content.Shared.Buckle;
@@ -36,10 +32,10 @@ public abstract partial class SharedBuckleSystem : EntitySystem
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedJointSystem _joints = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly PullingSystem _pulling = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly StandingStateSystem _standing = default!;
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
[Dependency] private readonly SharedRotationVisualsSystem _rotationVisuals = default!;
/// <inheritdoc/>
public override void Initialize()
@@ -51,45 +47,6 @@ public abstract partial class SharedBuckleSystem : EntitySystem
InitializeBuckle();
InitializeStrap();
}
/// <summary>
/// Reattaches this entity to the strap, modifying its position and rotation.
/// </summary>
/// <param name="buckleUid">The entity to reattach.</param>
/// <param name="strapUid">The entity to reattach the buckleUid entity to.</param>
private void ReAttach(
EntityUid buckleUid,
EntityUid strapUid,
BuckleComponent? buckleComp = null,
StrapComponent? strapComp = null)
{
if (!Resolve(strapUid, ref strapComp, false)
|| !Resolve(buckleUid, ref buckleComp, false))
return;
_transform.SetCoordinates(buckleUid, new EntityCoordinates(strapUid, strapComp.BuckleOffsetClamped));
var buckleTransform = Transform(buckleUid);
// Buckle subscribes to move for <reasons> so this might fail.
// TODO: Make buckle not do that.
if (buckleTransform.ParentUid != strapUid)
return;
_transform.SetLocalRotation(buckleUid, Angle.Zero, buckleTransform);
_joints.SetRelay(buckleUid, strapUid);
switch (strapComp.Position)
{
case StrapPosition.None:
break;
case StrapPosition.Stand:
_standing.Stand(buckleUid);
break;
case StrapPosition.Down:
_standing.Down(buckleUid, false, false);
break;
}
InitializeInteraction();
}
}

View File

@@ -128,13 +128,13 @@ namespace Content.Shared.CCVar
/// Minimum time between meteor swarms in minutes.
/// </summary>
public static readonly CVarDef<float>
MeteorSwarmMinTime = CVarDef.Create("events.meteor_swarm_min_time", 7.5f, CVar.ARCHIVE | CVar.SERVERONLY);
MeteorSwarmMinTime = CVarDef.Create("events.meteor_swarm_min_time", 12.5f, CVar.ARCHIVE | CVar.SERVERONLY);
/// <summary>
/// Maximum time between meteor swarms in minutes.
/// </summary>
public static readonly CVarDef<float>
MeteorSwarmMaxTime = CVarDef.Create("events.meteor_swarm_max_time", 12.5f, CVar.ARCHIVE | CVar.SERVERONLY);
MeteorSwarmMaxTime = CVarDef.Create("events.meteor_swarm_max_time", 17.5f, CVar.ARCHIVE | CVar.SERVERONLY);
/*
* Game
@@ -432,6 +432,14 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<string> RoundEndSoundCollection =
CVarDef.Create("game.round_end_sound_collection", "RoundEnd", CVar.SERVERONLY);
/// <summary>
/// Whether or not to add every player as a global override to PVS at round end.
/// This will allow all players to see their clothing in the round screen player list screen,
/// but may cause lag during round end with very high player counts.
/// </summary>
public static readonly CVarDef<bool> RoundEndPVSOverrides =
CVarDef.Create("game.round_end_pvs_overrides", true, CVar.SERVERONLY);
/*
* Discord
*/
@@ -875,6 +883,25 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<bool> AdminBypassMaxPlayers =
CVarDef.Create("admin.bypass_max_players", true, CVar.SERVERONLY);
/*
* AHELP
*/
/// <summary>
/// Ahelp rate limit values are accounted in periods of this size (seconds).
/// After the period has passed, the count resets.
/// </summary>
/// <seealso cref="AhelpRateLimitCount"/>
public static readonly CVarDef<int> AhelpRateLimitPeriod =
CVarDef.Create("ahelp.rate_limit_period", 2, CVar.SERVERONLY);
/// <summary>
/// How many ahelp messages are allowed in a single rate limit period.
/// </summary>
/// <seealso cref="AhelpRateLimitPeriod"/>
public static readonly CVarDef<int> AhelpRateLimitCount =
CVarDef.Create("ahelp.rate_limit_count", 10, CVar.SERVERONLY);
/*
* Explosions
*/

View File

@@ -1,11 +0,0 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Chemistry.Components;
/// <summary>
/// Blocks all attempts to access solutions contained by this entity.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class BlockSolutionAccessComponent : Component
{
}

View File

@@ -48,6 +48,12 @@ public partial record struct SolutionOverflowEvent(Entity<SolutionComponent> Sol
public bool Handled = false;
}
[ByRefEvent]
public partial record struct SolutionAccessAttemptEvent(string SolutionName)
{
public bool Cancelled;
}
/// <summary>
/// Part of Chemistry system deal with SolutionContainers
/// </summary>
@@ -156,12 +162,6 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
[NotNullWhen(true)] out Entity<SolutionComponent>? entity,
bool errorOnMissing = false)
{
if (TryComp(container, out BlockSolutionAccessComponent? blocker))
{
entity = null;
return false;
}
EntityUid uid;
if (name is null)
uid = container;
@@ -170,7 +170,18 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
solutionContainer is ContainerSlot solutionSlot &&
solutionSlot.ContainedEntity is { } containedSolution
)
{
var attemptEv = new SolutionAccessAttemptEvent(name);
RaiseLocalEvent(container, ref attemptEv);
if (attemptEv.Cancelled)
{
entity = null;
return false;
}
uid = containedSolution;
}
else
{
entity = null;
@@ -218,11 +229,14 @@ public abstract partial class SharedSolutionContainerSystem : EntitySystem
if (!Resolve(container, ref container.Comp, logMissing: false))
yield break;
if (HasComp<BlockSolutionAccessComponent>(container))
yield break;
foreach (var name in container.Comp.Containers)
{
var attemptEv = new SolutionAccessAttemptEvent(name);
RaiseLocalEvent(container, ref attemptEv);
if (attemptEv.Cancelled)
continue;
if (ContainerSystem.GetContainer(container, $"solution@{name}") is ContainerSlot slot && slot.ContainedEntity is { } solutionId)
yield return (name, (solutionId, Comp<SolutionComponent>(solutionId)));
}

View File

@@ -58,7 +58,7 @@ public sealed partial class ClimbSystem : VirtualController
SubscribeLocalEvent<ClimbingComponent, EntParentChangedMessage>(OnParentChange);
SubscribeLocalEvent<ClimbingComponent, ClimbDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<ClimbingComponent, EndCollideEvent>(OnClimbEndCollide);
SubscribeLocalEvent<ClimbingComponent, BuckleChangeEvent>(OnBuckleChange);
SubscribeLocalEvent<ClimbingComponent, BuckledEvent>(OnBuckled);
SubscribeLocalEvent<ClimbableComponent, CanDropTargetEvent>(OnCanDragDropOn);
SubscribeLocalEvent<ClimbableComponent, GetVerbsEvent<AlternativeVerb>>(AddClimbableVerb);
@@ -468,10 +468,8 @@ public sealed partial class ClimbSystem : VirtualController
Climb(uid, uid, climbable, true, component);
}
private void OnBuckleChange(EntityUid uid, ClimbingComponent component, ref BuckleChangeEvent args)
private void OnBuckled(EntityUid uid, ClimbingComponent component, ref BuckledEvent args)
{
if (!args.Buckling)
return;
StopClimb(uid, component);
}

View File

@@ -0,0 +1,12 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Clothing.Components;
/// <summary>
/// Disables client-side physics prediction for this entity.
/// Without this, movement with <see cref="PilotedClothingSystem"/> is very rubberbandy.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class PilotedByClothingComponent : Component
{
}

View File

@@ -0,0 +1,38 @@
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
namespace Content.Shared.Clothing.Components;
/// <summary>
/// Allows an entity stored in this clothing item to pass inputs to the entity wearing it.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class PilotedClothingComponent : Component
{
/// <summary>
/// Whitelist for entities that are allowed to act as pilots when inside this entity.
/// </summary>
[DataField]
public EntityWhitelist? PilotWhitelist;
/// <summary>
/// Should movement input be relayed from the pilot to the target?
/// </summary>
[DataField]
public bool RelayMovement = true;
/// <summary>
/// Reference to the entity contained in the clothing and acting as pilot.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? Pilot;
/// <summary>
/// Reference to the entity wearing this clothing who will be controlled by the pilot.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? Wearer;
public bool IsActive => Pilot != null && Wearer != null;
}

View File

@@ -87,7 +87,7 @@ public abstract class ClothingSystem : EntitySystem
foreach (HumanoidVisualLayers layer in layers)
{
if (!appearanceLayers.Contains(layer))
break;
continue;
InventorySystem.InventorySlotEnumerator enumerator = _invSystem.GetSlotEnumerator(equipee);

View File

@@ -0,0 +1,169 @@
using Content.Shared.Clothing.Components;
using Content.Shared.Inventory.Events;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Content.Shared.Storage;
using Content.Shared.Whitelist;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
namespace Content.Shared.Clothing.EntitySystems;
public sealed partial class PilotedClothingSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedMoverController _moverController = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PilotedClothingComponent, EntInsertedIntoContainerMessage>(OnEntInserted);
SubscribeLocalEvent<PilotedClothingComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
SubscribeLocalEvent<PilotedClothingComponent, GotEquippedEvent>(OnEquipped);
SubscribeLocalEvent<PilotedClothingComponent, GotUnequippedEvent>(OnUnequipped);
}
private void OnEntInserted(Entity<PilotedClothingComponent> entity, ref EntInsertedIntoContainerMessage args)
{
// Make sure the entity was actually inserted into storage and not a different container.
if (!TryComp(entity, out StorageComponent? storage) || args.Container != storage.Container)
return;
// Check potential pilot against whitelist, if one exists.
if (_whitelist.IsWhitelistFail(entity.Comp.PilotWhitelist, args.Entity))
return;
entity.Comp.Pilot = args.Entity;
Dirty(entity);
// Attempt to setup control link, if Pilot and Wearer are both present.
StartPiloting(entity);
}
private void OnEntRemoved(Entity<PilotedClothingComponent> entity, ref EntRemovedFromContainerMessage args)
{
// Make sure the removed entity is actually the pilot.
if (args.Entity != entity.Comp.Pilot)
return;
StopPiloting(entity);
entity.Comp.Pilot = null;
Dirty(entity);
}
private void OnEquipped(Entity<PilotedClothingComponent> entity, ref GotEquippedEvent args)
{
if (!TryComp(entity, out ClothingComponent? clothing))
return;
// Make sure the clothing item was equipped to the right slot, and not just held in a hand.
var isCorrectSlot = (clothing.Slots & args.SlotFlags) != Inventory.SlotFlags.NONE;
if (!isCorrectSlot)
return;
entity.Comp.Wearer = args.Equipee;
Dirty(entity);
// Attempt to setup control link, if Pilot and Wearer are both present.
StartPiloting(entity);
}
private void OnUnequipped(Entity<PilotedClothingComponent> entity, ref GotUnequippedEvent args)
{
StopPiloting(entity);
entity.Comp.Wearer = null;
Dirty(entity);
}
/// <summary>
/// Attempts to establish movement/interaction relay connection(s) from Pilot to Wearer.
/// If either is missing, fails and returns false.
/// </summary>
private bool StartPiloting(Entity<PilotedClothingComponent> entity)
{
// Make sure we have both a Pilot and a Wearer
if (entity.Comp.Pilot == null || entity.Comp.Wearer == null)
return false;
if (!_timing.IsFirstTimePredicted)
return false;
var pilotEnt = entity.Comp.Pilot.Value;
var wearerEnt = entity.Comp.Wearer.Value;
// Add component to block prediction of wearer
EnsureComp<PilotedByClothingComponent>(wearerEnt);
if (entity.Comp.RelayMovement)
{
// Establish movement input relay.
_moverController.SetRelay(pilotEnt, wearerEnt);
}
var pilotEv = new StartedPilotingClothingEvent(entity, wearerEnt);
RaiseLocalEvent(pilotEnt, ref pilotEv);
var wearerEv = new StartingBeingPilotedByClothing(entity, pilotEnt);
RaiseLocalEvent(wearerEnt, ref wearerEv);
return true;
}
/// <summary>
/// Removes components from the Pilot and Wearer to stop the control relay.
/// Returns false if a connection does not already exist.
/// </summary>
private bool StopPiloting(Entity<PilotedClothingComponent> entity)
{
if (entity.Comp.Pilot == null || entity.Comp.Wearer == null)
return false;
// Clean up components on the Pilot
var pilotEnt = entity.Comp.Pilot.Value;
RemCompDeferred<RelayInputMoverComponent>(pilotEnt);
// Clean up components on the Wearer
var wearerEnt = entity.Comp.Wearer.Value;
RemCompDeferred<MovementRelayTargetComponent>(wearerEnt);
RemCompDeferred<PilotedByClothingComponent>(wearerEnt);
// Raise an event on the Pilot
var pilotEv = new StoppedPilotingClothingEvent(entity, wearerEnt);
RaiseLocalEvent(pilotEnt, ref pilotEv);
// Raise an event on the Wearer
var wearerEv = new StoppedBeingPilotedByClothing(entity, pilotEnt);
RaiseLocalEvent(wearerEnt, ref wearerEv);
return true;
}
}
/// <summary>
/// Raised on the Pilot when they gain control of the Wearer.
/// </summary>
[ByRefEvent]
public record struct StartedPilotingClothingEvent(EntityUid Clothing, EntityUid Wearer);
/// <summary>
/// Raised on the Pilot when they lose control of the Wearer,
/// due to the Pilot exiting the clothing or the clothing being unequipped by the Wearer.
/// </summary>
[ByRefEvent]
public record struct StoppedPilotingClothingEvent(EntityUid Clothing, EntityUid Wearer);
/// <summary>
/// Raised on the Wearer when the Pilot gains control of them.
/// </summary>
[ByRefEvent]
public record struct StartingBeingPilotedByClothing(EntityUid Clothing, EntityUid Pilot);
/// <summary>
/// Raised on the Wearer when the Pilot loses control of them
/// due to the Pilot exiting the clothing or the clothing being unequipped by the Wearer.
/// </summary>
[ByRefEvent]
public record struct StoppedBeingPilotedByClothing(EntityUid Clothing, EntityUid Pilot);

View File

@@ -1,12 +0,0 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Conveyor;
/// <summary>
/// Used to track which conveyors are relevant in case there's a lot of them.
/// </summary>
[RegisterComponent]
public sealed partial class ActiveConveyorComponent : Component
{
}

View File

@@ -0,0 +1,13 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Conveyor;
/// <summary>
/// Indicates this entity is currently being conveyed.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ConveyedComponent : Component
{
[ViewVariables, AutoNetworkedField]
public List<EntityUid> Colliding = new();
}

View File

@@ -39,9 +39,6 @@ public sealed partial class ConveyorComponent : Component
[DataField]
public ProtoId<SinkPortPrototype> OffPort = "Off";
[ViewVariables]
public readonly HashSet<EntityUid> Intersecting = new();
}
[Serializable, NetSerializable]

View File

@@ -71,6 +71,7 @@ namespace Content.Shared.Cuffs
SubscribeLocalEvent<CuffableComponent, IsUnequippingAttemptEvent>(OnUnequipAttempt);
SubscribeLocalEvent<CuffableComponent, BeingPulledAttemptEvent>(OnBeingPulledAttempt);
SubscribeLocalEvent<CuffableComponent, BuckleAttemptEvent>(OnBuckleAttemptEvent);
SubscribeLocalEvent<CuffableComponent, UnbuckleAttemptEvent>(OnUnbuckleAttemptEvent);
SubscribeLocalEvent<CuffableComponent, GetVerbsEvent<Verb>>(AddUncuffVerb);
SubscribeLocalEvent<CuffableComponent, UnCuffDoAfterEvent>(OnCuffableDoAfter);
SubscribeLocalEvent<CuffableComponent, PullStartedMessage>(OnPull);
@@ -79,7 +80,7 @@ namespace Content.Shared.Cuffs
SubscribeLocalEvent<CuffableComponent, PickupAttemptEvent>(CheckAct);
SubscribeLocalEvent<CuffableComponent, AttackAttemptEvent>(CheckAct);
SubscribeLocalEvent<CuffableComponent, UseAttemptEvent>(CheckAct);
SubscribeLocalEvent<CuffableComponent, InteractionAttemptEvent>(CheckAct);
SubscribeLocalEvent<CuffableComponent, InteractionAttemptEvent>(CheckInteract);
SubscribeLocalEvent<HandcuffComponent, AfterInteractEvent>(OnCuffAfterInteract);
SubscribeLocalEvent<HandcuffComponent, MeleeHitEvent>(OnCuffMeleeHit);
@@ -87,6 +88,12 @@ namespace Content.Shared.Cuffs
SubscribeLocalEvent<HandcuffComponent, VirtualItemDeletedEvent>(OnCuffVirtualItemDeleted);
}
private void CheckInteract(Entity<CuffableComponent> ent, ref InteractionAttemptEvent args)
{
if (!ent.Comp.CanStillInteract)
args.Cancelled = true;
}
private void OnUncuffAttempt(ref UncuffAttemptEvent args)
{
if (args.Cancelled)
@@ -189,21 +196,33 @@ namespace Content.Shared.Cuffs
args.Cancel();
}
private void OnBuckleAttemptEvent(EntityUid uid, CuffableComponent component, ref BuckleAttemptEvent args)
private void OnBuckleAttempt(Entity<CuffableComponent> ent, EntityUid? user, ref bool cancelled, bool buckling, bool popup)
{
// if someone else is doing it, let it pass.
if (args.UserEntity != uid)
if (cancelled || user != ent.Owner)
return;
if (!TryComp<HandsComponent>(uid, out var hands) || component.CuffedHandCount != hands.Count)
if (!TryComp<HandsComponent>(ent, out var hands) || ent.Comp.CuffedHandCount != hands.Count)
return;
args.Cancelled = true;
var message = args.Buckling
cancelled = true;
if (!popup)
return;
var message = buckling
? Loc.GetString("handcuff-component-cuff-interrupt-buckled-message")
: Loc.GetString("handcuff-component-cuff-interrupt-unbuckled-message");
_popup.PopupClient(message, uid, args.UserEntity);
_popup.PopupClient(message, ent, user);
}
private void OnBuckleAttemptEvent(Entity<CuffableComponent> ent, ref BuckleAttemptEvent args)
{
OnBuckleAttempt(ent, args.User, ref args.Cancelled, true, args.Popup);
}
private void OnUnbuckleAttemptEvent(Entity<CuffableComponent> ent, ref UnbuckleAttemptEvent args)
{
OnBuckleAttempt(ent, args.User, ref args.Cancelled, false, args.Popup);
}
private void OnPull(EntityUid uid, CuffableComponent component, PullMessage args)

View File

@@ -1,4 +1,5 @@
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
namespace Content.Shared.DeviceLinking;
@@ -11,13 +12,14 @@ public sealed partial class DeviceLinkSinkComponent : Component
/// <summary>
/// The ports this sink has
/// </summary>
[DataField("ports", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<SinkPortPrototype>))]
public HashSet<string>? Ports;
[DataField]
public HashSet<ProtoId<SinkPortPrototype>> Ports = new();
/// <summary>
/// Used for removing a sink from all linked sources when it gets removed
/// Used for removing a sink from all linked sources when this component gets removed.
/// This is not serialized to yaml as it can be inferred from source components.
/// </summary>
[DataField("links")]
[ViewVariables]
public HashSet<EntityUid> LinkedSources = new();
/// <summary>
@@ -25,14 +27,13 @@ public sealed partial class DeviceLinkSinkComponent : Component
/// The counter is counted down by one every tick if it's higher than 0
/// This is for preventing infinite loops
/// </summary>
[DataField("invokeCounter")]
[DataField]
public int InvokeCounter;
/// <summary>
/// How high the invoke counter is allowed to get before the links to the sink are removed and the DeviceLinkOverloadedEvent gets raised
/// If the invoke limit is smaller than 1 the sink can't overload
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("invokeLimit")]
[DataField]
public int InvokeLimit = 10;
}

View File

@@ -12,12 +12,12 @@ public sealed partial class DeviceLinkSourceComponent : Component
/// The ports the device link source sends signals from
/// </summary>
[DataField]
public HashSet<ProtoId<SourcePortPrototype>>? Ports;
public HashSet<ProtoId<SourcePortPrototype>> Ports = new();
/// <summary>
/// A list of sink uids that got linked for each port
/// Dictionary mapping each port to a set of linked sink entities.
/// </summary>
[ViewVariables]
[ViewVariables] // This is not serialized as it can be constructed from LinkedPorts
public Dictionary<ProtoId<SourcePortPrototype>, HashSet<EntityUid>> Outputs = new();
/// <summary>
@@ -32,7 +32,7 @@ public sealed partial class DeviceLinkSourceComponent : Component
/// The list of source to sink ports for each linked sink entity for easier managing of links
/// </summary>
[DataField]
public Dictionary<EntityUid, HashSet<(ProtoId<SourcePortPrototype> source, ProtoId<SinkPortPrototype> sink)>> LinkedPorts = new();
public Dictionary<EntityUid, HashSet<(ProtoId<SourcePortPrototype> Source, ProtoId<SinkPortPrototype> Sink)>> LinkedPorts = new();
/// <summary>
/// Limits the range devices can be linked across.

View File

@@ -20,98 +20,56 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<DeviceLinkSourceComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<DeviceLinkSourceComponent, ComponentStartup>(OnSourceStartup);
SubscribeLocalEvent<DeviceLinkSinkComponent, ComponentStartup>(OnSinkStartup);
SubscribeLocalEvent<DeviceLinkSourceComponent, ComponentRemove>(OnSourceRemoved);
SubscribeLocalEvent<DeviceLinkSinkComponent, ComponentRemove>(OnSinkRemoved);
}
#region Link Validation
private void OnInit(EntityUid uid, DeviceLinkSourceComponent component, ComponentInit args)
{
// Populate the output dictionary.
foreach (var (sinkUid, links) in component.LinkedPorts)
{
foreach (var link in links)
{
component.Outputs.GetOrNew(link.source).Add(sinkUid);
}
}
}
/// <summary>
/// Removes invalid links where the saved sink doesn't exist/have a sink component for example
/// </summary>
private void OnSourceStartup(EntityUid sourceUid, DeviceLinkSourceComponent sourceComponent, ComponentStartup args)
private void OnSourceStartup(Entity<DeviceLinkSourceComponent> source, ref ComponentStartup args)
{
List<EntityUid> invalidSinks = new();
foreach (var sinkUid in sourceComponent.LinkedPorts.Keys)
List<(string, string)> invalidLinks = new();
foreach (var (sink, links) in source.Comp.LinkedPorts)
{
if (!TryComp<DeviceLinkSinkComponent>(sinkUid, out var sinkComponent))
if (!TryComp(sink, out DeviceLinkSinkComponent? sinkComponent))
{
invalidSinks.Add(sinkUid);
foreach (var savedSinks in sourceComponent.Outputs.Values)
{
savedSinks.Remove(sinkUid);
}
invalidSinks.Add(sink);
continue;
}
sinkComponent.LinkedSources.Add(sourceUid);
}
foreach (var invalidSink in invalidSinks)
{
sourceComponent.LinkedPorts.Remove(invalidSink);
}
}
/// <summary>
/// Same with <see cref="OnSourceStartup"/> but also checks that the saved ports are present on the sink
/// </summary>
private void OnSinkStartup(EntityUid sinkUid, DeviceLinkSinkComponent sinkComponent, ComponentStartup args)
{
List<EntityUid> invalidSources = new();
foreach (var sourceUid in sinkComponent.LinkedSources)
{
if (!TryComp<DeviceLinkSourceComponent>(sourceUid, out var sourceComponent))
foreach (var link in links)
{
invalidSources.Add(sourceUid);
continue;
}
if (!sourceComponent.LinkedPorts.TryGetValue(sinkUid, out var linkedPorts))
{
foreach (var savedSinks in sourceComponent.Outputs.Values)
{
savedSinks.Remove(sinkUid);
}
continue;
}
if (sinkComponent.Ports == null)
continue;
List<(string, string)> invalidLinks = new();
foreach (var link in linkedPorts)
{
if (!sinkComponent.Ports.Contains(link.sink))
if (sinkComponent.Ports.Contains(link.Sink) && source.Comp.Ports.Contains(link.Source))
source.Comp.Outputs.GetOrNew(link.Source).Add(sink);
else
invalidLinks.Add(link);
}
foreach (var invalidLink in invalidLinks)
foreach (var link in invalidLinks)
{
linkedPorts.Remove(invalidLink);
sourceComponent.Outputs.GetValueOrDefault(invalidLink.Item1)?.Remove(sinkUid);
Log.Warning($"Device source {ToPrettyString(source)} contains invalid links to entity {ToPrettyString(sink)}: {link.Item1}->{link.Item2}");
links.Remove(link);
}
if (links.Count == 0)
{
invalidSinks.Add(sink);
continue;
}
invalidLinks.Clear();
sinkComponent.LinkedSources.Add(source.Owner);
}
foreach (var invalidSource in invalidSources)
foreach (var sink in invalidSinks)
{
sinkComponent.LinkedSources.Remove(invalidSource);
source.Comp.LinkedPorts.Remove(sink);
Log.Warning($"Device source {ToPrettyString(source)} contains invalid sink: {ToPrettyString(sink)}");
}
}
#endregion
@@ -119,26 +77,29 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
/// <summary>
/// Ensures that its links get deleted when a source gets removed
/// </summary>
private void OnSourceRemoved(EntityUid uid, DeviceLinkSourceComponent component, ComponentRemove args)
private void OnSourceRemoved(Entity<DeviceLinkSourceComponent> source, ref ComponentRemove args)
{
var query = GetEntityQuery<DeviceLinkSinkComponent>();
foreach (var sinkUid in component.LinkedPorts.Keys)
foreach (var sinkUid in source.Comp.LinkedPorts.Keys)
{
if (query.TryGetComponent(sinkUid, out var sink))
RemoveSinkFromSourceInternal(uid, sinkUid, component, sink);
RemoveSinkFromSourceInternal(source, sinkUid, source, sink);
else
Log.Error($"Device source {ToPrettyString(source)} links to invalid entity: {ToPrettyString(sinkUid)}");
}
}
/// <summary>
/// Ensures that its links get deleted when a sink gets removed
/// </summary>
private void OnSinkRemoved(EntityUid sinkUid, DeviceLinkSinkComponent sinkComponent, ComponentRemove args)
private void OnSinkRemoved(Entity<DeviceLinkSinkComponent> sink, ref ComponentRemove args)
{
var query = GetEntityQuery<DeviceLinkSourceComponent>();
foreach (var linkedSource in sinkComponent.LinkedSources)
foreach (var sourceUid in sink.Comp.LinkedSources)
{
if (query.TryGetComponent(sinkUid, out var source))
RemoveSinkFromSourceInternal(linkedSource, sinkUid, source, sinkComponent);
if (TryComp(sourceUid, out DeviceLinkSourceComponent? source))
RemoveSinkFromSourceInternal(sourceUid, sink, source, sink);
else
Log.Error($"Device sink {ToPrettyString(sink)} source list contains invalid entity: {ToPrettyString(sourceUid)}");
}
}
@@ -146,36 +107,36 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
/// <summary>
/// Convenience function to add several ports to an entity
/// </summary>
public void EnsureSourcePorts(EntityUid uid, params string[] ports)
public void EnsureSourcePorts(EntityUid uid, params ProtoId<SourcePortPrototype>[] ports)
{
if (ports.Length == 0)
return;
var comp = EnsureComp<DeviceLinkSourceComponent>(uid);
comp.Ports ??= new HashSet<ProtoId<SourcePortPrototype>>();
foreach (var port in ports)
{
DebugTools.Assert(_prototypeManager.HasIndex<SourcePortPrototype>(port));
comp.Ports?.Add(port);
if (!_prototypeManager.HasIndex(port))
Log.Error($"Attempted to add invalid port {port} to {ToPrettyString(uid)}");
else
comp.Ports.Add(port);
}
}
/// <summary>
/// Convenience function to add several ports to an entity.
/// </summary>
public void EnsureSinkPorts(EntityUid uid, params string[] ports)
public void EnsureSinkPorts(EntityUid uid, params ProtoId<SinkPortPrototype>[] ports)
{
if (ports.Length == 0)
return;
var comp = EnsureComp<DeviceLinkSinkComponent>(uid);
comp.Ports ??= new HashSet<string>();
foreach (var port in ports)
{
DebugTools.Assert(_prototypeManager.HasIndex<SinkPortPrototype>(port));
comp.Ports?.Add(port);
if (!_prototypeManager.HasIndex(port))
Log.Error($"Attempted to add invalid port {port} to {ToPrettyString(uid)}");
else
comp.Ports.Add(port);
}
}
@@ -185,13 +146,13 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
/// <returns>A list of source port prototypes</returns>
public List<SourcePortPrototype> GetSourcePorts(EntityUid sourceUid, DeviceLinkSourceComponent? sourceComponent = null)
{
if (!Resolve(sourceUid, ref sourceComponent) || sourceComponent.Ports == null)
if (!Resolve(sourceUid, ref sourceComponent))
return new List<SourcePortPrototype>();
var sourcePorts = new List<SourcePortPrototype>();
foreach (var port in sourceComponent.Ports)
{
sourcePorts.Add(_prototypeManager.Index<SourcePortPrototype>(port));
sourcePorts.Add(_prototypeManager.Index(port));
}
return sourcePorts;
@@ -203,13 +164,13 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
/// <returns>A list of sink port prototypes</returns>
public List<SinkPortPrototype> GetSinkPorts(EntityUid sinkUid, DeviceLinkSinkComponent? sinkComponent = null)
{
if (!Resolve(sinkUid, ref sinkComponent) || sinkComponent.Ports == null)
if (!Resolve(sinkUid, ref sinkComponent))
return new List<SinkPortPrototype>();
var sinkPorts = new List<SinkPortPrototype>();
foreach (var port in sinkComponent.Ports)
{
sinkPorts.Add(_prototypeManager.Index<SinkPortPrototype>(port));
sinkPorts.Add(_prototypeManager.Index(port));
}
return sinkPorts;
@@ -315,9 +276,6 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
if (!Resolve(sourceUid, ref sourceComponent) || !Resolve(sinkUid, ref sinkComponent))
return;
if (sourceComponent.Ports == null || sinkComponent.Ports == null)
return;
if (!InRange(sourceUid, sinkUid, sourceComponent.Range))
{
if (userId != null)
@@ -391,7 +349,7 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
else
{
Log.Error($"Attempted to remove link between {ToPrettyString(sourceUid)} and {ToPrettyString(sinkUid)}, but the sink component was missing.");
sourceComponent.LinkedPorts.Remove(sourceUid);
sourceComponent.LinkedPorts.Remove(sinkUid);
}
}
@@ -414,12 +372,10 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
sinkComponent.LinkedSources.Remove(sourceUid);
sourceComponent.LinkedPorts.Remove(sinkUid);
var outputLists = sourceComponent.Outputs.Values;
foreach (var outputList in outputLists)
foreach (var outputList in sourceComponent.Outputs.Values)
{
outputList.Remove(sinkUid);
}
}
/// <summary>
@@ -438,9 +394,6 @@ public abstract class SharedDeviceLinkSystem : EntitySystem
if (!Resolve(sourceUid, ref sourceComponent) || !Resolve(sinkUid, ref sinkComponent))
return false;
if (sourceComponent.Ports == null || sinkComponent.Ports == null)
return false;
var outputs = sourceComponent.Outputs.GetOrNew(source);
var linkedPorts = sourceComponent.LinkedPorts.GetOrNew(sinkUid);

View File

@@ -1,3 +1,4 @@
using Content.Shared.DeviceNetwork.Systems;
using Robust.Shared.GameStates;
namespace Content.Shared.DeviceNetwork.Components;
@@ -6,6 +7,7 @@ namespace Content.Shared.DeviceNetwork.Components;
/// Allow entities to jam DeviceNetwork packets.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedDeviceNetworkJammerSystem))]
public sealed partial class DeviceNetworkJammerComponent : Component
{
/// <summary>

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