mirror of
https://github.com/wega-team/ss14-wega.git
synced 2026-02-14 19:30:01 +01:00
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:
5
.github/workflows/rsi-diff.yml
vendored
5
.github/workflows/rsi-diff.yml
vendored
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
19
Content.Client/Clothing/Systems/PilotedByClothingSystem.cs
Normal file
19
Content.Client/Clothing/Systems/PilotedByClothingSystem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
using Content.Shared.DeviceNetwork.Systems;
|
||||
|
||||
namespace Content.Client.DeviceNetwork.Systems;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed class DeviceNetworkJammerSystem : SharedDeviceNetworkJammerSystem;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
56
Content.IntegrationTests/Tests/Buckle/BuckleDragTest.cs
Normal file
56
Content.IntegrationTests/Tests/Buckle/BuckleDragTest.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
73
Content.IntegrationTests/Tests/Movement/PullingTest.cs
Normal file
73
Content.IntegrationTests/Tests/Movement/PullingTest.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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;
|
||||
|
||||
7
Content.Server/Antag/Components/AntagImmuneComponent.cs
Normal file
7
Content.Server/Antag/Components/AntagImmuneComponent.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace Content.Server.Antag.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public partial class AntagImmuneComponent : Component
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
namespace Content.Server.Bed.Components
|
||||
{
|
||||
// TODO rename this component
|
||||
[RegisterComponent]
|
||||
public sealed partial class HealOnBuckleHealingComponent : Component
|
||||
{}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
254
Content.Server/Players/RateLimiting/PlayerRateLimitManager.cs
Normal file
254
Content.Server/Players/RateLimiting/PlayerRateLimitManager.cs
Normal 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,
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Content.Server.Zombies;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class InitialInfectedExemptComponent : Component
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
188
Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs
Normal file
188
Content.Shared/Buckle/SharedBuckleSystem.Interaction.cs
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
169
Content.Shared/Clothing/EntitySystems/PilotedClothingSystem.cs
Normal file
169
Content.Shared/Clothing/EntitySystems/PilotedClothingSystem.cs
Normal 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);
|
||||
@@ -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
|
||||
{
|
||||
|
||||
}
|
||||
13
Content.Shared/Conveyor/ConveyedComponent.cs
Normal file
13
Content.Shared/Conveyor/ConveyedComponent.cs
Normal 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();
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user