merge remote wizden/master

This commit is contained in:
Dmitry
2026-06-07 19:36:24 +07:00
738 changed files with 26149 additions and 16458 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ public class DeltaPressureBenchmark
/// <summary>
/// Number of entities (windows, really) to spawn with a <see cref="DeltaPressureComponent"/>.
/// </summary>
[Params(1, 10, 100, 1000, 5000, 10000, 50000, 100000)]
[Params(100, 1000, 5000, 10000)]
public int EntityCount;
/// <summary>
@@ -1,3 +1,4 @@
using System.Numerics.Tensors;
using System.Runtime.CompilerServices;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Reactions;
@@ -26,15 +27,15 @@ public sealed partial class AtmosphereSystem
public override bool IsMixtureFuel(GasMixture mixture, float epsilon = Atmospherics.Epsilon)
{
var tmp = new float[Atmospherics.AdjustedNumberOfGases];
NumericsHelpers.Multiply(mixture.Moles, GasFuelMask, tmp);
return NumericsHelpers.HorizontalAdd(tmp) > epsilon;
TensorPrimitives.Multiply(mixture.Moles, GasFuelMask, tmp);
return TensorPrimitives.Sum(tmp) > epsilon;
}
public override bool IsMixtureOxidizer(GasMixture mixture, float epsilon = Atmospherics.Epsilon)
{
var tmp = new float[Atmospherics.AdjustedNumberOfGases];
NumericsHelpers.Multiply(mixture.Moles, GasOxidizerMask, tmp);
return NumericsHelpers.HorizontalAdd(tmp) > epsilon;
TensorPrimitives.Multiply(mixture.Moles, GasOxidizerMask, tmp);
return TensorPrimitives.Sum(tmp) > epsilon;
}
public override float GetMass(GasMixture mix)
@@ -45,17 +46,17 @@ public sealed partial class AtmosphereSystem
public override float GetMass(float[] moles)
{
var tmp = new float[moles.Length];
NumericsHelpers.Multiply(moles, GasMolarMasses, tmp);
TensorPrimitives.Multiply(moles, GasMolarMasses, tmp);
// Conversion of grams to kilograms.
return NumericsHelpers.HorizontalAdd(tmp) * Atmospherics.gToKg;
return TensorPrimitives.Sum(tmp) * Atmospherics.gToKg;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override float GetHeatCapacityCalculation(float[] moles, bool space)
{
// Little hack to make space gas mixtures have heat capacity, therefore allowing them to cool down rooms.
if (space && MathHelper.CloseTo(NumericsHelpers.HorizontalAdd(moles), 0f))
if (space && MathHelper.CloseTo(TensorPrimitives.Sum(moles), 0f))
{
return Atmospherics.SpaceHeatCapacity;
}
@@ -65,9 +66,9 @@ public sealed partial class AtmosphereSystem
// though this isnt the hottest code path so it should be fine
// the gc can eat a little as a treat
var tmp = new float[moles.Length];
NumericsHelpers.Multiply(moles, GasMolarHeatCapacities, tmp);
TensorPrimitives.Multiply(moles, GasMolarHeatCapacities, tmp);
// Adjust heat capacity by speedup, because this is primarily what
// determines how quickly gases heat up/cool.
return MathF.Max(NumericsHelpers.HorizontalAdd(tmp), Atmospherics.MinimumHeatCapacity);
return MathF.Max(TensorPrimitives.Sum(tmp), Atmospherics.MinimumHeatCapacity);
}
}
+1 -4
View File
@@ -1,5 +1,4 @@
using Content.Client.Beam.Components;
using Content.Shared.Beam;
using Content.Shared.Beam;
using Content.Shared.Beam.Components;
using Robust.Client.GameObjects;
@@ -23,8 +22,6 @@ public sealed partial class BeamSystem : SharedBeamSystem
if (TryComp<SpriteComponent>(beam, out var sprites))
{
_sprite.SetRotation((beam, sprites), args.UserAngle);
if (args.BodyState != null)
{
_sprite.LayerSetRsiState((beam, sprites), 0, args.BodyState);
@@ -1,8 +0,0 @@
using Content.Shared.Beam.Components;
namespace Content.Client.Beam.Components;
[RegisterComponent]
public sealed partial class BeamComponent : SharedBeamComponent
{
}
+9
View File
@@ -49,6 +49,9 @@ internal sealed partial class BuckleSystem : SharedBuckleSystem
// Give some of the sprite rotations their own drawdepth, maybe as an offset within the rsi, or something like this
// And we won't ever need to set the draw depth manually
if (!component.ModifyBuckleDrawDepth)
return;
if (args.NewRotation == args.OldRotation)
return;
@@ -86,6 +89,9 @@ internal sealed partial class BuckleSystem : SharedBuckleSystem
/// </summary>
private void OnBuckledEvent(Entity<BuckleComponent> ent, ref BuckledEvent args)
{
if (!args.Strap.Comp.ModifyBuckleDrawDepth)
return;
if (!TryComp<SpriteComponent>(args.Strap, out var strapSprite))
return;
@@ -106,6 +112,9 @@ internal sealed partial class BuckleSystem : SharedBuckleSystem
/// </summary>
private void OnUnbuckledEvent(Entity<BuckleComponent> ent, ref UnbuckledEvent args)
{
if (!args.Strap.Comp.ModifyBuckleDrawDepth)
return;
if (!TryComp<SpriteComponent>(ent.Owner, out var buckledSprite))
return;
@@ -27,29 +27,30 @@ public sealed partial class SolutionContainerVisualsSystem : VisualizerSystem<So
private void OnMapInit(EntityUid uid, SolutionContainerVisualsComponent component, MapInitEvent args)
{
var meta = MetaData(uid);
component.InitialDescription = meta.EntityDescription;
component.InitialDescription = MetaData(uid).EntityDescription;
}
protected override void OnAppearanceChange(EntityUid uid, SolutionContainerVisualsComponent component, ref AppearanceChangeEvent args)
{
// Check if the solution that was updated is the one set as represented
if (!string.IsNullOrEmpty(component.SolutionName))
{
if (AppearanceSystem.TryGetData<string>(uid, SolutionContainerVisuals.SolutionName, out var name,
args.Component) && name != component.SolutionName)
{
return;
}
}
if (!AppearanceSystem.TryGetData<float>(uid, SolutionContainerVisuals.FillFraction, out var fraction, args.Component))
return;
if (args.Sprite == null)
return;
if (!SpriteSystem.LayerMapTryGet((uid, args.Sprite), component.Layer, out var fillLayer, false))
// Check if the solution that was updated is the one set as represented
if (!string.IsNullOrEmpty(component.SolutionName)
&& AppearanceSystem.TryGetData(uid, SolutionContainerVisuals.SolutionName, out string name, args.Component)
&& name != component.SolutionName)
return;
if (!AppearanceSystem.TryGetData(uid,
SolutionContainerVisuals.FillFraction,
out float fraction,
args.Component))
return;
// C# moment; setting it as nullable (even though it's not) avoids a
// gazillion .AsNullable calls.
Entity<SpriteComponent?> ent = (uid, args.Sprite);
if (!SpriteSystem.LayerMapTryGet(ent, component.Layer, out var fillLayer, false))
return;
var maxFillLevels = component.MaxFillLevels;
@@ -62,52 +63,27 @@ public sealed partial class SolutionContainerVisualsSystem : VisualizerSystem<So
// a giant error sign and error for debug.
if (fraction > 1f)
{
Log.Error("Attempted to set solution container visuals volume ratio on " + ToPrettyString(uid) + " to a value greater than 1. Volume should never be greater than max volume!");
Log.Error($"Attempted to set solution container visuals volume ratio on {ToPrettyString(uid)} to a "
+ $"value greater than 1. Volume should never be greater than max volume!");
fraction = 1f;
}
if (component.Metamorphic)
{
if (SpriteSystem.LayerMapTryGet((uid, args.Sprite), component.BaseLayer, out var baseLayer, false))
{
var hasOverlay = SpriteSystem.LayerMapTryGet((uid, args.Sprite), component.OverlayLayer, out var overlayLayer, false);
if (AppearanceSystem.TryGetData<string>(uid, SolutionContainerVisuals.BaseOverride,
out var baseOverride,
args.Component))
{
_prototype.TryIndex<ReagentPrototype>(baseOverride, out var reagentProto);
if (reagentProto?.MetamorphicSprite is { } sprite)
{
SpriteSystem.LayerSetSprite((uid, args.Sprite), baseLayer, sprite);
if (reagentProto.MetamorphicMaxFillLevels > 0)
{
SpriteSystem.LayerSetVisible((uid, args.Sprite), fillLayer, true);
maxFillLevels = reagentProto.MetamorphicMaxFillLevels;
fillBaseName = reagentProto.MetamorphicFillBaseName;
changeColor = reagentProto.MetamorphicChangeColor;
fillSprite = sprite;
}
else
SpriteSystem.LayerSetVisible((uid, args.Sprite), fillLayer, false);
if (hasOverlay)
SpriteSystem.LayerSetVisible((uid, args.Sprite), overlayLayer, false);
}
else
{
SpriteSystem.LayerSetVisible((uid, args.Sprite), fillLayer, true);
if (hasOverlay)
SpriteSystem.LayerSetVisible((uid, args.Sprite), overlayLayer, true);
if (component.MetamorphicDefaultSprite != null)
SpriteSystem.LayerSetSprite((uid, args.Sprite), baseLayer, component.MetamorphicDefaultSprite);
}
}
}
}
if (!component.Metamorphic)
SpriteSystem.LayerSetVisible(ent, fillLayer, true);
else
{
SpriteSystem.LayerSetVisible((uid, args.Sprite), fillLayer, true);
var reagentProto = MetamorphicChanged(uid, component, args, ent, fillLayer);
if (reagentProto?.MetamorphicMaxFillLevels > 0)
{
SpriteSystem.LayerSetVisible(ent, fillLayer, true);
maxFillLevels = reagentProto.MetamorphicMaxFillLevels;
fillBaseName = reagentProto.MetamorphicFillBaseName;
changeColor = reagentProto.MetamorphicChangeColor;
fillSprite = reagentProto.MetamorphicSprite ?? fillSprite;
}
else
SpriteSystem.LayerSetVisible(ent, fillLayer, false);
}
var closestFillSprite = ContentHelpers.RoundToLevels(fraction, 1, maxFillLevels + 1);
@@ -117,27 +93,25 @@ public sealed partial class SolutionContainerVisualsSystem : VisualizerSystem<So
if (fillBaseName == null)
return;
var stateName = fillBaseName + closestFillSprite;
if (fillSprite != null)
SpriteSystem.LayerSetSprite((uid, args.Sprite), fillLayer, fillSprite);
SpriteSystem.LayerSetRsiState((uid, args.Sprite), fillLayer, stateName);
SpriteSystem.LayerSetSprite(ent, fillLayer, fillSprite);
if (changeColor && AppearanceSystem.TryGetData<Color>(uid, SolutionContainerVisuals.Color, out var color, args.Component))
SpriteSystem.LayerSetColor((uid, args.Sprite), fillLayer, color);
SpriteSystem.LayerSetRsiState(ent, fillLayer, fillBaseName + closestFillSprite);
if (changeColor
&& AppearanceSystem.TryGetData(uid, SolutionContainerVisuals.Color, out Color color, args.Component))
SpriteSystem.LayerSetColor(ent, fillLayer, color);
else
SpriteSystem.LayerSetColor((uid, args.Sprite), fillLayer, Color.White);
SpriteSystem.LayerSetColor(ent, fillLayer, Color.White);
}
else
{
if (component.EmptySpriteName == null)
SpriteSystem.LayerSetVisible((uid, args.Sprite), fillLayer, false);
SpriteSystem.LayerSetVisible(ent, fillLayer, false);
else
{
SpriteSystem.LayerSetRsiState((uid, args.Sprite), fillLayer, component.EmptySpriteName);
if (changeColor)
SpriteSystem.LayerSetColor((uid, args.Sprite), fillLayer, component.EmptySpriteColor);
else
SpriteSystem.LayerSetColor((uid, args.Sprite), fillLayer, Color.White);
SpriteSystem.LayerSetRsiState(ent, fillLayer, component.EmptySpriteName);
SpriteSystem.LayerSetColor(ent, fillLayer, changeColor ? component.EmptySpriteColor : Color.White);
}
}
@@ -145,36 +119,51 @@ public sealed partial class SolutionContainerVisualsSystem : VisualizerSystem<So
_itemSystem.VisualsChanged(uid);
}
private void OnGetHeldVisuals(EntityUid uid, SolutionContainerVisualsComponent component, GetInhandVisualsEvent args)
private ReagentPrototype? MetamorphicChanged(EntityUid uid,
SolutionContainerVisualsComponent component,
AppearanceChangeEvent args,
Entity<SpriteComponent?> ent,
int fillLayer)
{
if (component.InHandsFillBaseName == null)
return;
if (!AppearanceSystem.TryGetData(uid,
SolutionContainerVisuals.BaseOverride,
out string baseOverride,
args.Component))
return null;
if (!TryComp(uid, out AppearanceComponent? appearance))
return;
var reagentProto = _prototype.Index<ReagentPrototype>(baseOverride);
if (!TryComp<ItemComponent>(uid, out var item))
return;
if (SpriteSystem.LayerMapTryGet(ent, component.OverlayLayer, out var overlayLayer, false))
SpriteSystem.LayerSetVisible(ent, overlayLayer, reagentProto.MetamorphicSprite is not null);
if (!AppearanceSystem.TryGetData<float>(uid, SolutionContainerVisuals.FillFraction, out var fraction, appearance))
return;
if (!SpriteSystem.LayerMapTryGet(ent, component.BaseLayer, out var baseLayer, false))
return null;
var closestFillSprite = ContentHelpers.RoundToLevels(fraction, 1, component.InHandsMaxFillLevels + 1);
if (closestFillSprite > 0)
if (reagentProto.MetamorphicSprite is { } sprite)
SpriteSystem.LayerSetSprite(ent, baseLayer, sprite);
else
{
var layer = new PrototypeLayerData();
var heldPrefix = item.HeldPrefix == null ? "inhand-" : $"{item.HeldPrefix}-inhand-";
var key = heldPrefix + args.Location.ToString().ToLowerInvariant() + component.InHandsFillBaseName + closestFillSprite;
layer.State = key;
if (component.ChangeColor && AppearanceSystem.TryGetData<Color>(uid, SolutionContainerVisuals.Color, out var color, appearance))
layer.Color = color;
args.Layers.Add((key, layer));
SpriteSystem.LayerSetVisible(ent, fillLayer, true);
if (component.MetamorphicDefaultSprite != null)
SpriteSystem.LayerSetSprite(ent, baseLayer, component.MetamorphicDefaultSprite);
}
return reagentProto;
}
private void OnGetHeldVisuals(Entity<SolutionContainerVisualsComponent> ent, ref GetInhandVisualsEvent args)
{
if (ent.Comp.InHandsFillBaseName == null)
return;
if (!TryComp<ItemComponent>(ent, out var item))
return;
var inhandPrefix = item.HeldPrefix == null ? "inhand-" : $"{item.HeldPrefix}-inhand-";
var layerKeyPrefix = inhandPrefix + args.Location.ToString().ToLowerInvariant() + ent.Comp.InHandsFillBaseName;
if (GetVisualsLayer(ent, layerKeyPrefix, ent.Comp.InHandsMaxFillLevels) is { } layer)
args.Layers.Add(layer);
}
private void OnGetClothingVisuals(Entity<SolutionContainerVisualsComponent> ent, ref GetEquipmentVisualsEvent args)
@@ -182,35 +171,51 @@ public sealed partial class SolutionContainerVisualsSystem : VisualizerSystem<So
if (ent.Comp.EquippedFillBaseName == null)
return;
if (!TryComp<AppearanceComponent>(ent, out var appearance))
return;
if (!TryComp<ClothingComponent>(ent, out var clothing))
return;
if (!AppearanceSystem.TryGetData<float>(ent, SolutionContainerVisuals.FillFraction, out var fraction, appearance))
return;
var equippedPrefix = clothing.EquippedPrefix == null
? $"equipped-{args.Slot}"
: $" {clothing.EquippedPrefix}-equipped-{args.Slot}";
var layerKeyPrefix = equippedPrefix + ent.Comp.EquippedFillBaseName;
var closestFillSprite = ContentHelpers.RoundToLevels(fraction, 1, ent.Comp.EquippedMaxFillLevels + 1);
if (GetVisualsLayer(ent, layerKeyPrefix, ent.Comp.EquippedMaxFillLevels) is { } layer)
args.Layers.Add(layer);
}
if (closestFillSprite > 0)
{
var layer = new PrototypeLayerData();
private (string Key, PrototypeLayerData Layer)? GetVisualsLayer(Entity<SolutionContainerVisualsComponent> ent,
string layerKeyPrefix,
int maxFillLevels)
{
if (!TryComp<AppearanceComponent>(ent, out var appearance))
return null;
var equippedPrefix = clothing.EquippedPrefix == null ? $"equipped-{args.Slot}" : $" {clothing.EquippedPrefix}-equipped-{args.Slot}";
var key = equippedPrefix + ent.Comp.EquippedFillBaseName + closestFillSprite;
if (!AppearanceSystem.TryGetData<float>(ent,
SolutionContainerVisuals.FillFraction,
out var fraction,
appearance))
return null;
// Make sure the sprite state is valid so we don't show a big red error message
// This saves us from having to make fill level sprites for every possible slot the item could be in (including pockets).
if (!TryComp<SpriteComponent>(ent, out var sprite) || sprite.BaseRSI == null || !sprite.BaseRSI.TryGetState(key, out _))
return;
var closestFillSprite = ContentHelpers.RoundToLevels(fraction, 1, maxFillLevels + 1);
if (closestFillSprite <= 0)
return null;
layer.State = key;
var layer = new PrototypeLayerData();
var key = layerKeyPrefix + closestFillSprite;
if (ent.Comp.ChangeColor && AppearanceSystem.TryGetData<Color>(ent, SolutionContainerVisuals.Color, out var color, appearance))
layer.Color = color;
// Make sure the sprite state is valid so we don't show a big red error message
// This saves us from having to make fill level sprites for every possible slot the item could be in (including pockets).
if (!TryComp<SpriteComponent>(ent, out var sprite)
|| sprite.BaseRSI?.TryGetState(key, out _) != true)
return null;
layer.State = key;
if (ent.Comp.ChangeColor
&& AppearanceSystem.TryGetData<Color>(ent, SolutionContainerVisuals.Color, out var color, appearance))
layer.Color = color;
return (key, layer);
args.Layers.Add((key, layer));
}
}
}
@@ -129,9 +129,6 @@ public sealed partial class ClickableSystem : EntitySystem
return true;
}
drawDepth = default;
renderOrder = default;
bottom = default;
return false;
}
@@ -45,6 +45,7 @@ namespace Content.Client.Construction
WarmupRecipesCache();
UpdatesOutsidePrediction = true;
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypeReload);
SubscribeLocalEvent<LocalPlayerAttachedEvent>(HandlePlayerAttached);
SubscribeNetworkEvent<AckStructureConstructionMessage>(HandleAckStructure);
SubscribeNetworkEvent<ResponseConstructionGuide>(OnConstructionGuideReceived);
@@ -76,8 +77,16 @@ namespace Content.Client.Construction
return false;
}
private void OnPrototypeReload(PrototypesReloadedEventArgs obj)
{
if (obj.WasModified<ConstructionPrototype>())
WarmupRecipesCache();
}
private void WarmupRecipesCache()
{
_recipesMetadataCache.Clear();
foreach (var constructionProto in PrototypeManager.EnumeratePrototypes<ConstructionPrototype>())
{
if (!PrototypeManager.Resolve(constructionProto.Graph, out var graphProto))
+2 -3
View File
@@ -367,7 +367,7 @@ public sealed partial class DamageVisualsSystem : VisualizerSystem<DamageVisuals
if (damageVisComp.TargetLayers != null && damageVisComp.DamageOverlayGroups != null)
UpdateDisabledLayers(uid, spriteComponent, component, damageVisComp);
if (damageVisComp.Overlay && damageVisComp.DamageOverlayGroups != null && damageVisComp.TargetLayers == null)
if (damageVisComp.Overlay && damageVisComp.TargetLayers == null)
CheckOverlayOrdering((uid, spriteComponent), damageVisComp);
if (AppearanceSystem.TryGetData<bool>(uid, DamageVisualizerKeys.ForceUpdate, out var update, component)
@@ -474,8 +474,7 @@ public sealed partial class DamageVisualsSystem : VisualizerSystem<DamageVisuals
new SpriteSpecifier.Rsi(
new(sprite.Sprite),
$"{statePrefix}_{threshold}"
),
spriteLayer);
));
SpriteSystem.LayerMapSet(spriteEnt.AsNullable(), key, spriteLayer);
SpriteSystem.LayerSetVisible(spriteEnt.AsNullable(), spriteLayer, visibility);
// this is somewhat iffy since it constantly reallocates
@@ -0,0 +1,6 @@
using Content.Shared.Disposal.Holder;
namespace Content.Client.Disposal.Holder;
/// <inheritdoc/>
public sealed partial class DisposalHolderSystem : SharedDisposalHolderSystem;
@@ -1,6 +1,5 @@
using Content.Client.Disposal.Unit;
using Content.Client.Power.EntitySystems;
using Content.Shared.Disposal;
using Content.Shared.Disposal.Components;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
@@ -11,70 +10,84 @@ namespace Content.Client.Disposal.Mailing;
public sealed class MailingUnitBoundUserInterface : BoundUserInterface
{
[ViewVariables]
public MailingUnitWindow? MailingUnitWindow;
private MailingUnitWindow? _mailingUnitWindow;
public MailingUnitBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
private void ButtonPressed(DisposalUnitComponent.UiButton button)
private void ButtonPressed(DisposalUnitUiButton button)
{
SendMessage(new DisposalUnitComponent.UiButtonPressedMessage(button));
// If we get client-side power stuff then we can predict the button presses but for now we won't as it stuffs
// the pressure lerp up.
SendPredictedMessage(new DisposalUnitUiButtonPressedMessage(button));
}
private void TargetSelected(ItemList.ItemListSelectedEventArgs args)
{
var item = args.ItemList[args.ItemIndex];
SendMessage(new TargetSelectedMessage(item.Text));
SendPredictedMessage(new TargetSelectedMessage(item.Text));
}
protected override void Open()
{
base.Open();
MailingUnitWindow = this.CreateWindow<MailingUnitWindow>();
MailingUnitWindow.OpenCenteredRight();
_mailingUnitWindow = this.CreateWindow<MailingUnitWindow>();
_mailingUnitWindow.OpenCenteredRight();
MailingUnitWindow.Eject.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Eject);
MailingUnitWindow.Engage.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Engage);
MailingUnitWindow.Power.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Power);
_mailingUnitWindow.Eject.OnPressed += _ => ButtonPressed(DisposalUnitUiButton.Eject);
_mailingUnitWindow.Engage.OnPressed += _ => ButtonPressed(DisposalUnitUiButton.Engage);
_mailingUnitWindow.Power.OnPressed += _ => ButtonPressed(DisposalUnitUiButton.Power);
MailingUnitWindow.TargetListContainer.OnItemSelected += TargetSelected;
_mailingUnitWindow.TargetListContainer.OnItemSelected += TargetSelected;
if (EntMan.TryGetComponent(Owner, out MailingUnitComponent? component))
{
Refresh((Owner, component));
}
}
public override void Update()
{
base.Update();
if (EntMan.TryGetComponent(Owner, out MailingUnitComponent? component))
{
Refresh((Owner, component));
}
}
public void Refresh(Entity<MailingUnitComponent> entity)
{
if (MailingUnitWindow == null)
if (_mailingUnitWindow == null)
return;
// TODO: This should be decoupled from disposals
if (EntMan.TryGetComponent(entity.Owner, out DisposalUnitComponent? disposals))
var name = EntMan.GetComponent<MetaDataComponent>(entity.Owner).EntityName;
_mailingUnitWindow.Title = string.IsNullOrEmpty(entity.Comp.Tag)
? Loc.GetString("ui-mailing-unit-window-title", ("name", name))
: Loc.GetString("ui-mailing-unit-window-title-tagged", ("tag", entity.Comp.Tag));
_mailingUnitWindow.Target.Text = entity.Comp.Target;
var entries = entity.Comp.TargetList.Select(target => new ItemList.Item(_mailingUnitWindow.TargetListContainer)
{
var disposalSystem = EntMan.System<DisposalUnitSystem>();
var disposalState = disposalSystem.GetState(Owner, disposals);
var fullPressure = disposalSystem.EstimatedFullPressure(Owner, disposals);
MailingUnitWindow.UnitState.Text = Loc.GetString($"disposal-unit-state-{disposalState}");
MailingUnitWindow.FullPressure = fullPressure;
MailingUnitWindow.PressureBar.UpdatePressure(fullPressure);
MailingUnitWindow.Power.Pressed = EntMan.System<PowerReceiverSystem>().IsPowered(Owner);
MailingUnitWindow.Engage.Pressed = disposals.Engaged;
}
MailingUnitWindow.Title = Loc.GetString("ui-mailing-unit-window-title", ("tag", entity.Comp.Tag ?? " "));
//UnitTag.Text = state.Tag;
MailingUnitWindow.Target.Text = entity.Comp.Target;
var entries = entity.Comp.TargetList.Select(target => new ItemList.Item(MailingUnitWindow.TargetListContainer) {
Text = target,
Selected = target == entity.Comp.Target
}).ToList();
MailingUnitWindow.TargetListContainer.SetItems(entries);
_mailingUnitWindow.TargetListContainer.SetItems(entries);
if (!EntMan.TryGetComponent(entity.Owner, out DisposalUnitComponent? disposals))
return;
var disposalSystem = EntMan.System<DisposalUnitSystem>();
var disposalState = disposalSystem.GetState((Owner, disposals));
var fullPressure = disposalSystem.EstimatedFullPressure((Owner, disposals));
var pressurePerSecond = disposals.PressurePerSecond;
_mailingUnitWindow.UnitState.Text = Loc.GetString($"disposal-unit-state-{disposalState}");
_mailingUnitWindow.FullPressure = fullPressure;
_mailingUnitWindow.PressurePerSecond = pressurePerSecond;
_mailingUnitWindow.PressureBar.UpdatePressure(fullPressure, pressurePerSecond);
_mailingUnitWindow.Power.Pressed = EntMan.System<PowerReceiverSystem>().IsPowered(Owner);
_mailingUnitWindow.Engage.Pressed = disposals.Engaged;
}
}
@@ -1,11 +1,12 @@
using Content.Shared.Disposal;
using Content.Shared.Disposal.Components;
using Content.Shared.Disposal.Mailing;
namespace Content.Client.Disposal.Mailing;
public sealed class MailingUnitSystem : SharedMailingUnitSystem
public sealed partial class MailingUnitSystem : SharedMailingUnitSystem
{
[Dependency] private SharedUserInterfaceSystem _userInterface = default!;
public override void Initialize()
{
base.Initialize();
@@ -14,7 +15,7 @@ public sealed class MailingUnitSystem : SharedMailingUnitSystem
private void OnMailingState(Entity<MailingUnitComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (UserInterfaceSystem.TryGetOpenUi<MailingUnitBoundUserInterface>(ent.Owner, MailingUnitUiKey.Key, out var bui))
if (_userInterface.TryGetOpenUi<MailingUnitBoundUserInterface>(ent.Owner, MailingUnitUiKey.Key, out var bui))
{
bui.Refresh(ent);
}
@@ -4,7 +4,7 @@
MinSize="300 400"
SetSize="300 400"
Resizable="False">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Vertical" Margin="10">
<BoxContainer Orientation="Horizontal" SeparationOverride="8">
<Label Text="{Loc 'ui-mailing-unit-target-label'}" />
<Label Name="Target"
@@ -28,7 +28,7 @@
SeparationOverride="4">
<Label Text="{Loc 'ui-disposal-unit-label-pressure'}" />
<disposal:PressureBar Name="PressureBar"
Access="Public"
Access="Public"
MinSize="190 20"
HorizontalAlignment="Right"
MinValue="0"
@@ -48,9 +48,10 @@
Text="{Loc 'ui-disposal-unit-button-eject'}"
StyleClasses="OpenBoth" />
<Button Name="Power"
Access="Public"
Text="{Loc 'ui-disposal-unit-button-power'}"
StyleClasses="OpenLeft" />
Access="Public"
Text="{Loc 'ui-disposal-unit-button-power'}"
StyleClasses="OpenLeft"
ToggleMode="True" />
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>
@@ -12,6 +12,7 @@ namespace Content.Client.Disposal.Mailing
public sealed partial class MailingUnitWindow : FancyWindow
{
public TimeSpan FullPressure;
public float PressurePerSecond;
public MailingUnitWindow()
{
@@ -21,7 +22,7 @@ namespace Content.Client.Disposal.Mailing
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
PressureBar.UpdatePressure(FullPressure);
PressureBar.UpdatePressure(FullPressure, PressurePerSecond);
}
}
}
+3 -5
View File
@@ -1,6 +1,4 @@
using System.Numerics;
using Content.Shared.Disposal;
using Content.Shared.Disposal.Unit;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Timing;
@@ -9,10 +7,10 @@ namespace Content.Client.Disposal;
public sealed class PressureBar : ProgressBar
{
public bool UpdatePressure(TimeSpan fullTime)
public bool UpdatePressure(TimeSpan fullPressureTime, float pressurePreSecond)
{
var currentTime = IoCManager.Resolve<IGameTiming>().CurTime;
var pressure = (float) Math.Min(1.0f, 1.0f - (fullTime.TotalSeconds - currentTime.TotalSeconds) * SharedDisposalUnitSystem.PressurePerSecond);
var pressure = (float)Math.Min(1.0f, 1.0f - (fullPressureTime.TotalSeconds - currentTime.TotalSeconds) * pressurePreSecond);
UpdatePressureBar(pressure);
return pressure >= 1.0f;
}
@@ -1,8 +1,9 @@
using JetBrains.Annotations;
using Content.Shared.Disposal.Components;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
using static Content.Shared.Disposal.Components.SharedDisposalRouterComponent;
using System.Collections.Generic;
namespace Content.Client.Disposal.Tube
namespace Content.Client.Disposal.Router
{
/// <summary>
/// Initializes a <see cref="DisposalRouterWindow"/> and updates it when new server messages are received.
@@ -10,9 +11,10 @@ namespace Content.Client.Disposal.Tube
[UsedImplicitly]
public sealed class DisposalRouterBoundUserInterface : BoundUserInterface
{
[ViewVariables]
private DisposalRouterWindow? _window;
private const int TagLimit = 150;
public DisposalRouterBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
@@ -23,13 +25,19 @@ namespace Content.Client.Disposal.Tube
_window = this.CreateWindow<DisposalRouterWindow>();
_window.Confirm.OnPressed += _ => ButtonPressed(UiAction.Ok, _window.TagInput.Text);
_window.TagInput.OnTextEntered += args => ButtonPressed(UiAction.Ok, args.Text);
_window.Confirm.OnPressed += _ => AcceptButtonPressed(_window.TagInput.Text);
_window.TagInput.OnTextEntered += args => AcceptButtonPressed(args.Text);
if (EntMan.TryGetComponent<DisposalRouterComponent>(Owner, out var router) &&
router.Tags.Count > 0)
{
_window.TagInput.Text = string.Join(",", router.Tags);
}
}
private void ButtonPressed(UiAction action, string tag)
private void AcceptButtonPressed(string tag)
{
SendMessage(new UiActionMessage(action, tag));
SendMessage(new DisposalRouterUiActionMessage(tag, TagLimit));
Close();
}
@@ -0,0 +1,34 @@
using Content.Shared.Disposal.Components;
using Content.Shared.Disposal.Holder;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Disposal.Router;
/// <summary>
/// Client-side UI used to control a <see cref="DisposalRouterComponent"/>
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class DisposalRouterWindow : DefaultWindow
{
[Dependency] private IEntityManager _entManager = default!;
private readonly SharedDisposalHolderSystem _disposalHolder;
public DisposalRouterWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_disposalHolder = _entManager.System<SharedDisposalHolderSystem>();
TagInput.IsValid = tags => _disposalHolder.TagIsValid(tags);
}
public void UpdateState(DisposalRouterUserInterfaceState state)
{
TagInput.Text = state.Tags;
}
}
@@ -1,8 +1,8 @@
using JetBrains.Annotations;
using Content.Shared.Disposal.Components;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
using static Content.Shared.Disposal.Components.SharedDisposalTaggerComponent;
namespace Content.Client.Disposal.Tube
namespace Content.Client.Disposal.Tagger
{
/// <summary>
/// Initializes a <see cref="DisposalTaggerWindow"/> and updates it when new server messages are received.
@@ -10,9 +10,10 @@ namespace Content.Client.Disposal.Tube
[UsedImplicitly]
public sealed class DisposalTaggerBoundUserInterface : BoundUserInterface
{
[ViewVariables]
private DisposalTaggerWindow? _window;
private const int TagLimit = 30;
public DisposalTaggerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
@@ -23,14 +24,19 @@ namespace Content.Client.Disposal.Tube
_window = this.CreateWindow<DisposalTaggerWindow>();
_window.Confirm.OnPressed += _ => ButtonPressed(UiAction.Ok, _window.TagInput.Text);
_window.TagInput.OnTextEntered += args => ButtonPressed(UiAction.Ok, args.Text);
_window.Confirm.OnPressed += _ => AcceptButtonPressed(_window.TagInput.Text);
_window.TagInput.OnTextEntered += args => AcceptButtonPressed(args.Text);
if (EntMan.TryGetComponent<DisposalTaggerComponent>(Owner, out var tagger) &&
tagger.Tag != string.Empty)
{
_window.TagInput.Text = tagger.Tag;
}
}
private void ButtonPressed(UiAction action, string tag)
private void AcceptButtonPressed(string tag)
{
// TODO: This looks copy-pasted with the other mailing stuff...
SendMessage(new UiActionMessage(action, tag));
SendMessage(new DisposalTaggerUiActionMessage(tag, TagLimit));
Close();
}
@@ -0,0 +1,33 @@
using Content.Shared.Disposal.Components;
using Content.Shared.Disposal.Holder;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Disposal.Tagger;
/// <summary>
/// Client-side UI used to control a <see cref="DisposalTaggerComponent"/>
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class DisposalTaggerWindow : DefaultWindow
{
[Dependency] private IEntityManager _entManager = default!;
private readonly SharedDisposalHolderSystem _disposalHolder;
public DisposalTaggerWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_disposalHolder = _entManager.System<SharedDisposalHolderSystem>();
TagInput.IsValid = tag => _disposalHolder.TagIsValid(tag);
}
public void UpdateState(DisposalTaggerUserInterfaceState state)
{
TagInput.Text = state.Tags;
}
}
@@ -1,28 +0,0 @@
using Content.Shared.Disposal.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using static Content.Shared.Disposal.Components.SharedDisposalRouterComponent;
namespace Content.Client.Disposal.Tube
{
/// <summary>
/// Client-side UI used to control a <see cref="SharedDisposalRouterComponent"/>
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class DisposalRouterWindow : DefaultWindow
{
public DisposalRouterWindow()
{
RobustXamlLoader.Load(this);
TagInput.IsValid = tags => TagRegex.IsMatch(tags);
}
public void UpdateState(DisposalRouterUserInterfaceState state)
{
TagInput.Text = state.Tags;
}
}
}
@@ -1,28 +0,0 @@
using Content.Shared.Disposal.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using static Content.Shared.Disposal.Components.SharedDisposalTaggerComponent;
namespace Content.Client.Disposal.Tube
{
/// <summary>
/// Client-side UI used to control a <see cref="SharedDisposalTaggerComponent"/>
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class DisposalTaggerWindow : DefaultWindow
{
public DisposalTaggerWindow()
{
RobustXamlLoader.Load(this);
TagInput.IsValid = tag => TagRegex.IsMatch(tag);
}
public void UpdateState(DisposalTaggerUserInterfaceState state)
{
TagInput.Text = state.Tag;
}
}
}
@@ -1,8 +0,0 @@
using Content.Shared.Disposal.Unit;
namespace Content.Client.Disposal.Tube;
public sealed class DisposalTubeSystem : SharedDisposalTubeSystem
{
}
@@ -0,0 +1,6 @@
using Content.Shared.Disposal.Unit;
namespace Content.Client.Disposal.Unit;
/// <inheritdoc/>
public sealed class BeingDisposedSystem : SharedBeingDisposedSystem;
@@ -1,4 +1,3 @@
using Content.Client.Disposal.Mailing;
using Content.Client.Power.EntitySystems;
using Content.Shared.Disposal.Components;
using JetBrains.Annotations;
@@ -6,23 +5,19 @@ using Robust.Client.UserInterface;
namespace Content.Client.Disposal.Unit
{
/// <summary>
/// Initializes a <see cref="MailingUnitWindow"/> or a <see cref="_disposalUnitWindow"/> and updates it when new server messages are received.
/// </summary>
[UsedImplicitly]
public sealed class DisposalUnitBoundUserInterface : BoundUserInterface
{
[ViewVariables] private DisposalUnitWindow? _disposalUnitWindow;
[ViewVariables]
private DisposalUnitWindow? _disposalUnitWindow;
public DisposalUnitBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
private void ButtonPressed(DisposalUnitComponent.UiButton button)
private void ButtonPressed(DisposalUnitUiButton button)
{
SendPredictedMessage(new DisposalUnitComponent.UiButtonPressedMessage(button));
// If we get client-side power stuff then we can predict the button presses but for now we won't as it stuffs
// the pressure lerp up.
SendPredictedMessage(new DisposalUnitUiButtonPressedMessage(button));
}
protected override void Open()
@@ -30,12 +25,21 @@ namespace Content.Client.Disposal.Unit
base.Open();
_disposalUnitWindow = this.CreateWindow<DisposalUnitWindow>();
_disposalUnitWindow.OpenCenteredRight();
_disposalUnitWindow.Eject.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Eject);
_disposalUnitWindow.Engage.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Engage);
_disposalUnitWindow.Power.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Power);
_disposalUnitWindow.Eject.OnPressed += _ => ButtonPressed(DisposalUnitUiButton.Eject);
_disposalUnitWindow.Engage.OnPressed += _ => ButtonPressed(DisposalUnitUiButton.Engage);
_disposalUnitWindow.Power.OnPressed += _ => ButtonPressed(DisposalUnitUiButton.Power);
if (EntMan.TryGetComponent(Owner, out DisposalUnitComponent? component))
{
Refresh((Owner, component));
}
}
public override void Update()
{
base.Update();
if (EntMan.TryGetComponent(Owner, out DisposalUnitComponent? component))
{
@@ -48,16 +52,23 @@ namespace Content.Client.Disposal.Unit
if (_disposalUnitWindow == null)
return;
var disposalSystem = EntMan.System<DisposalUnitSystem>();
var name = EntMan.GetComponent<MetaDataComponent>(entity.Owner).EntityName;
_disposalUnitWindow.Title = Loc.GetString("ui-disposal-unit-title", ("name", name));
_disposalUnitWindow.Title = EntMan.GetComponent<MetaDataComponent>(entity.Owner).EntityName;
if (!EntMan.TryGetComponent(entity.Owner, out DisposalUnitComponent? disposals))
return;
var state = disposalSystem.GetState(entity.Owner, entity.Comp);
var disposalUnit = EntMan.System<DisposalUnitSystem>();
var disposalState = disposalUnit.GetState(entity);
var fullPressure = disposalUnit.EstimatedFullPressure((Owner, disposals));
var pressurePerSecond = disposals.PressurePerSecond;
_disposalUnitWindow.UnitState.Text = Loc.GetString($"disposal-unit-state-{state}");
_disposalUnitWindow.UnitState.Text = Loc.GetString($"disposal-unit-state-{disposalState}");
_disposalUnitWindow.FullPressure = disposalUnit.EstimatedFullPressure(entity);
_disposalUnitWindow.PressurePerSecond = entity.Comp.PressurePerSecond;
_disposalUnitWindow.PressureBar.UpdatePressure(fullPressure, pressurePerSecond);
_disposalUnitWindow.Power.Pressed = EntMan.System<PowerReceiverSystem>().IsPowered(Owner);
_disposalUnitWindow.Engage.Pressed = entity.Comp.Engaged;
_disposalUnitWindow.FullPressure = disposalSystem.EstimatedFullPressure(entity.Owner, entity.Comp);
}
}
}
@@ -2,56 +2,64 @@ using Content.Shared.Disposal.Components;
using Content.Shared.Disposal.Unit;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Audio.Systems;
namespace Content.Client.Disposal.Unit;
/// <inheritdoc/>
public sealed partial class DisposalUnitSystem : SharedDisposalUnitSystem
{
[Dependency] private AppearanceSystem _appearanceSystem = default!;
[Dependency] private AnimationPlayerSystem _animationSystem = default!;
[Dependency] private SharedAudioSystem _audioSystem = default!;
[Dependency] private SharedUserInterfaceSystem _uiSystem = default!;
[Dependency] private SpriteSystem _sprite = default!;
private const string AnimationKey = "disposal_unit_animation";
private const string DefaultFlushState = "disposal-flush";
private const string DefaultChargeState = "disposal-charging";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DisposalUnitComponent, AfterAutoHandleStateEvent>(OnHandleState);
SubscribeLocalEvent<DisposalUnitComponent, AppearanceChangeEvent>(OnAppearanceChange);
}
protected override void OnComponentInit(Entity<DisposalUnitComponent> ent, ref ComponentInit args)
{
base.OnComponentInit(ent, ref args);
// Create and store flushing animation.
var anim = new Animation
{
Length = ent.Comp.FlushDelay,
AnimationTracks =
{
new AnimationTrackSpriteFlick()
{
LayerKey = DisposalUnitVisualLayers.OverlayFlushing,
KeyFrames = { new AnimationTrackSpriteFlick.KeyFrame(ent.Comp.FlushingState, 0f) },
},
}
};
// Try to add flushing sound
if (ent.Comp.FlushSound != null)
{
anim.AnimationTracks.Add(
new AnimationTrackPlaySound
{
KeyFrames = { new AnimationTrackPlaySound.KeyFrame(_audioSystem.ResolveSound(ent.Comp.FlushSound), 0) }
}
);
}
ent.Comp.FlushingAnimation = anim;
}
private void OnHandleState(EntityUid uid, DisposalUnitComponent component, ref AfterAutoHandleStateEvent args)
{
UpdateUI((uid, component));
}
protected override void UpdateUI(Entity<DisposalUnitComponent> entity)
{
if (_uiSystem.TryGetOpenUi<DisposalUnitBoundUserInterface>(entity.Owner, DisposalUnitComponent.DisposalUnitUiKey.Key, out var bui))
{
bui.Refresh(entity);
}
}
protected override void OnDisposalInit(Entity<DisposalUnitComponent> ent, ref ComponentInit args)
{
base.OnDisposalInit(ent, ref args);
if (!TryComp<SpriteComponent>(ent, out var sprite) || !TryComp<AppearanceComponent>(ent, out var appearance))
return;
UpdateState(ent, sprite, appearance);
}
private void OnAppearanceChange(Entity<DisposalUnitComponent> ent, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
@@ -61,92 +69,28 @@ public sealed partial class DisposalUnitSystem : SharedDisposalUnitSystem
}
/// <summary>
/// Update visuals and tick animation
/// Updates the animation of a disposal unit.
/// </summary>
/// <param name="ent">The disposal unit.</param>
/// <param name="sprite">The disposal unit's sprite.</param>
/// <param name="appearance">The disposal unit's appearance.</param>
private void UpdateState(Entity<DisposalUnitComponent> ent, SpriteComponent sprite, AppearanceComponent appearance)
{
if (!_appearanceSystem.TryGetData<DisposalUnitComponent.VisualState>(ent, DisposalUnitComponent.Visuals.VisualState, out var state, appearance))
if (!_appearanceSystem.TryGetData<bool>(ent, DisposalUnitVisuals.IsFlushing, out var isFlushing, appearance))
return;
_sprite.LayerSetVisible((ent, sprite), DisposalUnitVisualLayers.Unanchored, state == DisposalUnitComponent.VisualState.UnAnchored);
_sprite.LayerSetVisible((ent, sprite), DisposalUnitVisualLayers.Base, state == DisposalUnitComponent.VisualState.Anchored);
_sprite.LayerSetVisible((ent, sprite), DisposalUnitVisualLayers.OverlayFlush, state == DisposalUnitComponent.VisualState.OverlayFlushing);
_sprite.LayerSetVisible((ent, sprite), DisposalUnitVisualLayers.BaseCharging, state == DisposalUnitComponent.VisualState.OverlayCharging);
var chargingState = _sprite.LayerMapTryGet((ent, sprite), DisposalUnitVisualLayers.BaseCharging, out var chargingLayer, false)
? _sprite.LayerGetRsiState((ent, sprite), chargingLayer)
: new RSI.StateId(DefaultChargeState);
// This is a transient state so not too worried about replaying in range.
if (state == DisposalUnitComponent.VisualState.OverlayFlushing)
if (isFlushing)
{
if (!_animationSystem.HasRunningAnimation(ent, AnimationKey))
{
var flushState = _sprite.LayerMapTryGet((ent, sprite), DisposalUnitVisualLayers.OverlayFlush, out var flushLayer, false)
? _sprite.LayerGetRsiState((ent, sprite), flushLayer)
: new RSI.StateId(DefaultFlushState);
// Setup the flush animation to play
var anim = new Animation
{
Length = ent.Comp.FlushDelay,
AnimationTracks =
{
new AnimationTrackSpriteFlick
{
LayerKey = DisposalUnitVisualLayers.OverlayFlush,
KeyFrames =
{
// Play the flush animation
new AnimationTrackSpriteFlick.KeyFrame(flushState, 0),
}
},
}
};
if (ent.Comp.FlushSound != null)
{
anim.AnimationTracks.Add(
new AnimationTrackPlaySound
{
KeyFrames =
{
new AnimationTrackPlaySound.KeyFrame(_audioSystem.ResolveSound(ent.Comp.FlushSound), 0)
}
});
}
_animationSystem.Play(ent, anim, AnimationKey);
_animationSystem.Play(ent, (Animation)ent.Comp.FlushingAnimation, AnimationKey);
}
return;
}
else
_animationSystem.Stop(ent.Owner, AnimationKey);
if (!_appearanceSystem.TryGetData<DisposalUnitComponent.HandleState>(ent, DisposalUnitComponent.Visuals.Handle, out var handleState, appearance))
handleState = DisposalUnitComponent.HandleState.Normal;
_sprite.LayerSetVisible((ent, sprite), DisposalUnitVisualLayers.OverlayEngaged, handleState != DisposalUnitComponent.HandleState.Normal);
if (!_appearanceSystem.TryGetData<DisposalUnitComponent.LightStates>(ent, DisposalUnitComponent.Visuals.Light, out var lightState, appearance))
lightState = DisposalUnitComponent.LightStates.Off;
_sprite.LayerSetVisible((ent, sprite), DisposalUnitVisualLayers.OverlayCharging,
(lightState & DisposalUnitComponent.LightStates.Charging) != 0);
_sprite.LayerSetVisible((ent, sprite), DisposalUnitVisualLayers.OverlayReady,
(lightState & DisposalUnitComponent.LightStates.Ready) != 0);
_sprite.LayerSetVisible((ent, sprite), DisposalUnitVisualLayers.OverlayFull,
(lightState & DisposalUnitComponent.LightStates.Full) != 0);
_animationSystem.Stop(ent.Owner, AnimationKey);
}
}
public enum DisposalUnitVisualLayers : byte
{
Unanchored,
Base,
BaseCharging,
OverlayFlush,
OverlayCharging,
OverlayReady,
OverlayFull,
OverlayEngaged
}
@@ -5,8 +5,7 @@
SetSize="300 160"
Resizable="False">
<BoxContainer Orientation="Vertical" Margin="10">
<BoxContainer Orientation="Horizontal"
SeparationOverride="4">
<BoxContainer Orientation="Horizontal" SeparationOverride="4">
<Label Text="{Loc 'ui-disposal-unit-label-state'}" />
<Label Name="UnitState" Access="Public"
Text="{Loc 'ui-disposal-unit-label-status'}" />
@@ -16,6 +15,7 @@
SeparationOverride="4">
<Label Text="{Loc 'ui-disposal-unit-label-pressure'}" />
<disposal:PressureBar Name="PressureBar"
Access="Public"
MinSize="190 20"
HorizontalAlignment="Right"
MinValue="0"
@@ -35,9 +35,10 @@
Text="{Loc 'ui-disposal-unit-button-eject'}"
StyleClasses="OpenBoth" />
<Button Name="Power"
Access="Public"
Text="{Loc 'ui-disposal-unit-button-power'}"
StyleClasses="OpenLeft" />
Access="Public"
Text="{Loc 'ui-disposal-unit-button-power'}"
StyleClasses="OpenLeft"
ToggleMode="True" />
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>
@@ -12,17 +12,17 @@ namespace Content.Client.Disposal.Unit
public sealed partial class DisposalUnitWindow : FancyWindow
{
public TimeSpan FullPressure;
public float PressurePerSecond;
public DisposalUnitWindow()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
PressureBar.UpdatePressure(FullPressure);
PressureBar.UpdatePressure(FullPressure, PressurePerSecond);
}
}
}
+1 -3
View File
@@ -161,13 +161,11 @@ namespace Content.Client.Gameplay
// Check the entities against whether or not we can click them
var foundEntities = new List<(EntityUid, int, uint, float)>(entities.Count);
var clickQuery = _entityManager.GetEntityQuery<ClickableComponent>();
var clickables = _entityManager.System<ClickableSystem>();
foreach (var entity in entities)
{
if (clickQuery.TryGetComponent(entity.Uid, out var component) &&
clickables.CheckClick((entity.Uid, component, entity.Component, entity.Transform), coordinates.Position, eye, excludeFaded, out var drawDepthClicked, out var renderOrder, out var bottom))
if (clickables.CheckClick((entity.Uid, null, entity.Component, entity.Transform), coordinates.Position, eye, excludeFaded, out var drawDepthClicked, out var renderOrder, out var bottom))
{
foundEntities.Add((entity.Uid, drawDepthClicked, renderOrder, bottom));
}
+3 -2
View File
@@ -1,4 +1,5 @@
using Content.Client.Popups;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Controls;
using Content.Shared.Access.Systems;
using Content.Shared.Holopad;
@@ -61,8 +62,8 @@ public sealed partial class HolopadWindow : FancyWindow
// XML formatting
AnswerCallButton.AddStyleClass("ButtonAccept");
EndCallButton.AddStyleClass("Caution");
StartBroadcastButton.AddStyleClass("Caution");
EndCallButton.AddStyleClass(StyleClass.Negative);
StartBroadcastButton.AddStyleClass(StyleClass.Negative);
HolopadContactListPanel.PanelOverride = new StyleBoxFlat
{
+9 -15
View File
@@ -2,14 +2,12 @@
using Content.Client.Items;
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
using Robust.Shared.Prototypes;
namespace Content.Client.Implants;
public sealed partial class ImplanterSystem : SharedImplanterSystem
{
[Dependency] private SharedUserInterfaceSystem _uiSystem = default!;
[Dependency] private IPrototypeManager _proto = default!;
public override void Initialize()
{
@@ -19,22 +17,18 @@ public sealed partial class ImplanterSystem : SharedImplanterSystem
Subs.ItemStatus<ImplanterComponent>(ent => new ImplanterStatusControl(ent));
}
private void OnHandleImplanterState(EntityUid uid, ImplanterComponent component, ref AfterAutoHandleStateEvent args)
private void OnHandleImplanterState(Entity<ImplanterComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (_uiSystem.TryGetOpenUi<DeimplantBoundUserInterface>(uid, DeimplantUiKey.Key, out var bui))
{
// TODO: Don't use protoId for deimplanting
// and especially not raw strings!
Dictionary<string, string> implants = new();
foreach (var implant in component.DeimplantWhitelist)
{
if (_proto.Resolve(implant, out var proto))
implants.Add(proto.ID, proto.Name);
}
UpdateUi(ent);
}
bui.UpdateState(implants, component.DeimplantChosen);
protected override void UpdateUi(Entity<ImplanterComponent> ent)
{
if (_uiSystem.TryGetOpenUi(ent.Owner, DeimplantUiKey.Key, out var bui))
{
bui.Update();
}
component.UiUpdateNeeded = true;
ent.Comp.UiUpdateNeeded = true;
}
}
@@ -1,10 +1,14 @@
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
using Robust.Client.UserInterface;
using Robust.Shared.Prototypes;
namespace Content.Client.Implants.UI;
public sealed class DeimplantBoundUserInterface : BoundUserInterface
public sealed partial class DeimplantBoundUserInterface : BoundUserInterface
{
[Dependency] private IPrototypeManager _proto = default!;
[ViewVariables]
private DeimplantChoiceWindow? _window;
@@ -18,15 +22,26 @@ public sealed class DeimplantBoundUserInterface : BoundUserInterface
_window = this.CreateWindow<DeimplantChoiceWindow>();
_window.OnImplantChange += implant => SendMessage(new DeimplantChangeVerbMessage(implant));
_window.OnImplantChange += implant => SendPredictedMessage(new DeimplantChangeVerbMessage(implant));
}
public void UpdateState(Dictionary<string, string> implantList, string? implant)
public override void Update()
{
if (!EntMan.TryGetComponent<ImplanterComponent>(Owner, out var implanterComp))
return;
// TODO: Don't use protoId for deimplanting
// and especially not raw strings!
Dictionary<string, string> implants = new();
foreach (var implant in implanterComp.DeimplantWhitelist)
{
if (_proto.Resolve(implant, out var proto))
implants.Add(proto.ID, proto.Name);
}
if (_window != null)
{
_window.UpdateImplantList(implantList);
_window.UpdateState(implant);
_window.UpdateImplantList(implants);
_window.UpdateState(implanterComp.DeimplantChosen);
}
}
}
+1 -1
View File
@@ -120,7 +120,7 @@
HorizontalAlignment="Right"
ToolTip="{Loc 'lathe-menu-delete-fabricating-tooltip'}">
<Button.StyleClasses>
<system:String>Caution</system:String>
<system:String>negative</system:String>
<system:String>OpenLeft</system:String>
</Button.StyleClasses>
</Button>
@@ -27,7 +27,7 @@
Text="✖"
ToolTip="{Loc 'lathe-menu-delete-item-tooltip'}">
<Button.StyleClasses>
<system:String>Caution</system:String>
<system:String>negative</system:String>
<system:String>OpenLeft</system:String>
</Button.StyleClasses>
</Button>
@@ -156,7 +156,7 @@ namespace Content.Client.Launcher
var tip = tipList[randomIndex];
LoginTip.SetMessage(Loc.GetString(tip));
LoginTipTitle.Text = Loc.GetString("connecting-window-tip", ("numberTip", randomIndex));
LoginTipTitle.Text = Loc.GetString("connecting-window-tip", ("numberTip", randomIndex + 1));
}
protected override void FrameUpdate(FrameEventArgs args)
@@ -27,7 +27,7 @@ public sealed class MagicMirrorBoundUserInterface : BoundUserInterface
_markingsModel.MarkingsChanged += (_, _) =>
{
SendMessage(new MagicMirrorSelectMessage(_markingsModel.Markings));
SendPredictedMessage(new MagicMirrorSelectMessage(_markingsModel.Markings));
};
}
@@ -13,7 +13,10 @@
HorizontalAlignment="Center"
VerticalAlignment="Center"
HorizontalExpand="True"
VerticalExpand="True">
VerticalExpand="True"
SeparationOverride="24">
<TextureRect Name="Logo"
Stretch="KeepCentered" />
<PanelContainer StyleClasses="BackgroundPanel">
<BoxContainer Orientation="Vertical"
HorizontalAlignment="Center"
@@ -21,9 +24,8 @@
HorizontalExpand="True"
VerticalExpand="True"
StyleIdentifier="mainMenuVBox"
Margin="4 16">
<TextureRect Name="Logo"
Stretch="KeepCentered" />
MinWidth="320"
Margin="0 4 0 12">
<GridContainer Columns="2" Margin="0 4">
<Label Text="{Loc 'main-menu-username-label'}" />
<LineEdit Name="UsernameBox"
+2 -3
View File
@@ -4,7 +4,7 @@ using Content.Client.Message;
using Content.Client.Power.Visualizers;
using Content.Client.Stylesheets;
using Content.Shared.Atmos.Components;
using Content.Shared.Disposal.Components;
using Content.Shared.Disposal.Tube;
using Content.Shared.Input;
using Content.Shared.Inventory;
using Content.Shared.SubFloor;
@@ -185,8 +185,7 @@ public sealed partial class TrayScannerSystem : SharedTrayScannerSystem
{
TrayScannerMode.All => true,
TrayScannerMode.Wiring => HasComp<CableVisualizerComponent>(uid),
// TODO: proper comp query after disposals refactor
TrayScannerMode.Piping => HasComp<AtmosPipeLayersComponent>(uid) || _appearance.TryGetData(uid, DisposalTubeVisuals.VisualState, out _),
TrayScannerMode.Piping => HasComp<AtmosPipeLayersComponent>(uid) || HasComp<DisposalTubeComponent>(uid),
_ => false,
};
}
@@ -69,6 +69,7 @@ public sealed partial class DamageOverlayUiController : UIController
private void ClearOverlay()
{
_overlay.State = MobState.Alive;
_overlay.DeadLevel = 0f;
_overlay.CritLevel = 0f;
_overlay.PainLevel = 0f;
@@ -1,29 +1,34 @@
using System.Linq;
using System.Numerics;
using Content.Client.Animations;
using Content.Client.Gameplay;
using Content.Client.Clickable;
using Content.Client.Items;
using Content.Client.Weapons.Ranged.Components;
using Content.Shared.Camera;
using Content.Shared.CCVar;
using Content.Shared.CombatMode;
using Content.Shared.Damage;
using Content.Shared.Mobs.Systems;
using Content.Shared.Physics;
using Content.Shared.Weapons.Hitscan.Components;
using Content.Shared.Weapons.Ranged;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Client.Animations;
using Robust.Client.ComponentTrees;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.State;
using Robust.Shared.Animations;
using Robust.Shared.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.Graphics;
using Robust.Shared.Input;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using SharedGunSystem = Content.Shared.Weapons.Ranged.Systems.SharedGunSystem;
@@ -33,20 +38,23 @@ namespace Content.Client.Weapons.Ranged.Systems;
public sealed partial class GunSystem : SharedGunSystem
{
[Dependency] private AnimationPlayerSystem _animPlayer = default!;
[Dependency] private IEyeManager _eyeManager = default!;
[Dependency] private IInputManager _inputManager = default!;
[Dependency] private InputSystem _inputSystem = default!;
[Dependency] private IOverlayManager _overlayManager = default!;
[Dependency] private IPlayerManager _player = default!;
[Dependency] private IStateManager _state = default!;
[Dependency] private IConfigurationManager _cfg = default!;
[Dependency] private AnimationPlayerSystem _animPlayer = default!;
[Dependency] private ClickableSystem _clickable = default!;
[Dependency] private MobStateSystem _mobState = default!;
[Dependency] private SharedCameraRecoilSystem _recoil = default!;
[Dependency] private SharedMapSystem _maps = default!;
[Dependency] private SharedTransformSystem _xform = default!;
[Dependency] private SpriteSystem _sprite = default!;
[Dependency] private SpriteTreeSystem _spriteTree = default!;
public static readonly EntProtoId HitscanProto = "HitscanEffect";
private GunTargetEntityComparer _comparer = default!;
public bool SpreadOverlay
{
@@ -90,6 +98,8 @@ public sealed partial class GunSystem : SharedGunSystem
InitializeMagazineVisuals();
InitializeSpentAmmo();
_comparer = new GunTargetEntityComparer();
}
@@ -198,11 +208,9 @@ public sealed partial class GunSystem : SharedGunSystem
}
// Define target coordinates relative to gun entity, so that network latency on moving grids doesn't fuck up the target location.
var coordinates = TransformSystem.ToCoordinates(entity, mousePos);
var target = GetBestTarget(_eyeManager.CurrentEye, mousePos);
NetEntity? target = null;
if (_state.CurrentState is GameplayStateBase screen)
target = GetNetEntity(screen.GetClickedEntity(mousePos));
var coordinates = TransformSystem.ToCoordinates(entity, mousePos);
Log.Debug($"Sending shoot request tick {Timing.CurTick} / {Timing.CurTime}");
@@ -411,6 +419,131 @@ public sealed partial class GunSystem : SharedGunSystem
_animPlayer.Play((gunUid, uidPlayer), animTwo, "muzzle-flash-light");
}
// TODO: Move RangedDamageSoundComponent to shared so this can be predicted.
/// <remarks>We use our own sorting algorithm separate from the default for smarter configurability.</remarks>
private NetEntity? GetBestTarget(IEye eye, MapCoordinates coordinates)
{
// Find all the entities intersecting our click
var entities = _spriteTree.QueryAabb(coordinates.MapId, Box2.CenteredAround(coordinates.Position, new Vector2(1, 1)));
// Check the entities against whether or not we can click them
var foundEntities = new List<(EntityUid, bool, bool, int, uint, float, float)>(entities.Count);
foreach (var entity in entities)
{
// Don't add the target if we can't shoot the target!
if (!CheckFixtures(entity.Uid))
continue;
var entry = CheckTarget((entity.Uid, entity.Component, entity.Transform), eye, coordinates);
foundEntities.Add(entry);
}
if (foundEntities.Count == 0)
return null;
// Do drawdepth & y-sorting. First index is the top-most sprite (opposite of normal render order).
foundEntities.Sort(_comparer);
var (target, alive, occluded, _, _, _, _) = foundEntities.FirstOrDefault();
// Prevents us from just selecting a random target nearby our cursor. It must either be alive, or our cursor must be on top of it!
if (!occluded && !alive)
return null;
return GetNetEntity(target);
}
private (EntityUid, bool, bool, int, uint, float, float) CheckTarget(Entity<SpriteComponent, TransformComponent> target, IEye eye, MapCoordinates coordinates)
{
var occluded = _clickable.CheckClick((target.Owner, null, target.Comp1, target.Comp2),
coordinates.Position,
eye,
true,
out var drawDepthClicked,
out var renderOrder,
out var bottom);
var difference = (target.Comp2.Coordinates.Position - coordinates.Position).LengthSquared();
return (target.Owner, _mobState.IsAlive(target.Owner), occluded, drawDepthClicked, renderOrder, bottom, difference);
}
/// <summary>
/// This Comparer takes a list of Entities that we can hit and orders them by which target the player is probably trying to shoot.
/// We organize based on these criteria in this order:
/// alive means the entity has a MobState and is currently alive. We check it first since they typically shoot back.
/// occluded is whether the cursor is above the sprite or just near it.
/// depth is the order in which sprites are layered, bigger number means its rendered above others.
/// renderOrder is used to indicate if a sprite should be visually more important, typically this value is 0.
/// bottom indicates which sprite is visually the lowest on the screen and therefore typically above other sprites.
/// distance indicates the distance from the entity's coordinates to our mouse.
/// If all of those tie, then we organize by whichever entity has the highest EntityUid.
/// </summary>
private sealed class GunTargetEntityComparer : IComparer<(EntityUid clicked, bool alive, bool occluded, int depth, uint renderOrder, float bottom, float distance)>
{
public int Compare((EntityUid clicked, bool alive, bool occluded, int depth, uint renderOrder, float bottom, float distance) x,
(EntityUid clicked, bool alive, bool occluded, int depth, uint renderOrder, float bottom, float distance) y)
{
var cmp = y.alive.CompareTo(x.alive);
if (cmp != 0)
{
return cmp;
}
cmp = y.occluded.CompareTo(x.occluded);
if (cmp != 0)
{
return cmp;
}
cmp = y.depth.CompareTo(x.depth);
if (cmp != 0)
{
return cmp;
}
cmp = y.renderOrder.CompareTo(x.renderOrder);
if (cmp != 0)
{
return cmp;
}
cmp = -y.bottom.CompareTo(x.bottom);
if (cmp != 0)
{
return cmp;
}
cmp = -y.distance.CompareTo(x.distance);
if (cmp != 0)
{
return cmp;
}
return y.clicked.CompareTo(x.clicked);
}
}
private bool CheckFixtures(Entity<FixturesComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp))
return false;
foreach (var fix in entity.Comp.Fixtures)
{
if (!fix.Value.Hard || (fix.Value.CollisionLayer & (int)CollisionGroup.BulletImpassable) == 0)
continue;
// Only need to check if we're hitting one fixture
return true;
}
// If we cannot collide then we absolutely do not want to target it!
return false;
}
public override void PlayImpactSound(EntityUid otherEntity, DamageSpecifier? modifiedDamage, SoundSpecifier? weaponSound, bool forceWeaponSound) { }
}
@@ -100,12 +100,12 @@ public abstract partial class GameTest
}
/// <summary>
/// Attempts to retrieve the given component from an entity on the server.
/// Attempts to retrieve the given component from an entity on the client.
/// </summary>
public bool CTryComp<T>(EntityUid? target, [NotNullWhen(true)] out T? component)
where T : IComponent
{
return SEntMan.TryGetComponent(target, out component);
return CEntMan.TryGetComponent(target, out component);
}
/// <summary>
@@ -40,9 +40,9 @@ public sealed class LogWindowTest : InteractionTest
// Search for the log we added earlier.
await Client.WaitPost(() => search.Text = guid.ToString());
await ClickControl(refresh);
await RunTicks(5);
await RunTicks(10);
var searchResult = cont.Children.Where(x => x.Visible && x is AdminLogLabel).Cast<AdminLogLabel>().ToArray();
Assert.That(searchResult.Length, Is.EqualTo(1));
Assert.That(searchResult, Has.Length.EqualTo(1));
Assert.That(searchResult[0].Log.Message, Contains.Substring($" test log 1: {guid}"));
// Add a new log
@@ -52,9 +52,9 @@ public sealed class LogWindowTest : InteractionTest
// Update the search and refresh
await Client.WaitPost(() => search.Text = guid.ToString());
await ClickControl(refresh);
await RunTicks(5);
await RunTicks(10);
searchResult = cont.Children.Where(x => x.Visible && x is AdminLogLabel).Cast<AdminLogLabel>().ToArray();
Assert.That(searchResult.Length, Is.EqualTo(1));
Assert.That(searchResult, Has.Length.EqualTo(1));
Assert.That(searchResult[0].Log.Message, Contains.Substring($" test log 2: {guid}"));
}
}
@@ -122,4 +122,20 @@ public abstract class AtmosTest : InteractionTest
Assert.That(MathHelper.CloseToPercent(mix1.TotalMoles, mix2.TotalMoles, tolerance),
$"GasMixtures do not match. Got {mix1.TotalMoles} and {mix2.TotalMoles} moles");
}
/// <summary>
/// Sets the tile's air mixture to have a certain pressure at a certain temperature.
/// </summary>
/// <param name="tile">Tile to set the air mixture of.</param>
/// <param name="pressure">The pressure to set the tile to.</param>
/// <param name="temp">The temperature to set the tile to.</param>
/// <param name="gas">The gas to fill the tile with.</param>
/// <remarks>Yeah, it could be a general atmospherics API, but the test assertion is desired.</remarks>
protected static void SetTilePressure(TileAtmosphere tile, float pressure, float temp = Atmospherics.T20C, Gas gas = Gas.Nitrogen)
{
Assert.That(tile.Air, Is.Not.Null, "Target tile should have an air mixture.");
tile.Air!.Clear();
var moles = pressure * tile.Air.Volume / (Atmospherics.R * temp);
tile.Air.AdjustMoles(gas, moles);
}
}
@@ -85,7 +85,19 @@ public sealed class DeltaPressureTest : AtmosTest
baseDamage:
types:
Structural: 1000
";
- type: entity
parent: DeltaPressureSolidTest
id: DeltaPressureSolidTestDeltaOnly
components:
- type: DeltaPressure
minPressure: 100000
minPressureDelta: 10000
scalingType: Threshold
baseDamage:
types:
Structural: 1000
";
#endregion
@@ -339,4 +351,89 @@ public sealed class DeltaPressureTest : AtmosTest
await Server.WaitRunTicks(30);
}
}
/// <summary>
/// Asserts that diagonal pressure groupings do not trigger delta pressure damage.
/// </summary>
[Test]
public async Task ProcessingDeltaCardinalOnlyStandbyTest()
{
Entity<DeltaPressureComponent> dpEnt = default;
await Server.WaitPost(delegate
{
SAtmos.SetAtmosphereSimulation(ProcessEnt, false);
var uid = SEntMan.SpawnAtPosition("DeltaPressureSolidTestDeltaOnly", new EntityCoordinates(ProcessEnt.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, SEntMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(SAtmos.IsDeltaPressureEntityInList(ProcessEnt.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
var indices = Transform.GetGridOrMapTilePosition(dpEnt);
var tiles = ProcessEnt.Comp1.Tiles;
// Same pressure on each opposing pair: N==S and E==W, so delta should be zero.
SetTilePressure(tiles[indices.Offset(AtmosDirection.North)], 12_000f);
SetTilePressure(tiles[indices.Offset(AtmosDirection.South)], 12_000f);
SetTilePressure(tiles[indices.Offset(AtmosDirection.East)], 100f);
SetTilePressure(tiles[indices.Offset(AtmosDirection.West)], 100f);
});
await Server.WaitPost(delegate
{
SAtmos.RunProcessingFull(ProcessEnt, ProcessEnt.Owner, SAtmos.AtmosTickRate);
});
await Server.WaitRunTicks(30);
await Server.WaitAssertion(delegate
{
Assert.That(!SEntMan.Deleted(dpEnt), $"{dpEnt} should not take damage when only diagonal comparisons would differ.");
foreach (var mix in SAtmos.GetAllMixtures(ProcessEnt))
{
mix.Clear();
}
});
}
/// <summary>
/// Asserts that opposing pressure groupings do trigger delta pressure damage.
/// </summary>
[Test]
public async Task ProcessingDeltaCardinalOnlyDamageTest()
{
Entity<DeltaPressureComponent> dpEnt = default;
await Server.WaitPost(delegate
{
SAtmos.SetAtmosphereSimulation(ProcessEnt, false);
var uid = SEntMan.SpawnAtPosition("DeltaPressureSolidTestDeltaOnly", new EntityCoordinates(ProcessEnt.Owner, Vector2.Zero));
dpEnt = new Entity<DeltaPressureComponent>(uid, SEntMan.GetComponent<DeltaPressureComponent>(uid));
Assert.That(SAtmos.IsDeltaPressureEntityInList(ProcessEnt.Owner, dpEnt), "Entity was not in processing list when it should have been added!");
var indices = Transform.GetGridOrMapTilePosition(dpEnt);
var tiles = ProcessEnt.Comp1.Tiles;
// There was a nasty bug where the indexing for comparisons was off and diagonals were
// being compared against instead of cardinals. This test basically
// stamps that out, so hopefully something silly doesn't happen again
// smile
SetTilePressure(tiles[indices.Offset(AtmosDirection.North)], 12_000f);
SetTilePressure(tiles[indices.Offset(AtmosDirection.South)], 100f);
SetTilePressure(tiles[indices.Offset(AtmosDirection.East)], 12_000f);
SetTilePressure(tiles[indices.Offset(AtmosDirection.West)], 100f);
});
await Server.WaitPost(delegate
{
SAtmos.RunProcessingFull(ProcessEnt, ProcessEnt.Owner, SAtmos.AtmosTickRate);
});
await Server.WaitRunTicks(30);
await Server.WaitAssertion(delegate
{
Assert.That(SEntMan.Deleted(dpEnt), $"{dpEnt} should take damage when opposing cardinals have threshold pressure differences.");
foreach (var mix in SAtmos.GetAllMixtures(ProcessEnt))
{
mix.Clear();
}
});
}
}
+26 -14
View File
@@ -4,8 +4,6 @@ using System.Numerics;
using Content.IntegrationTests.Fixtures;
using Content.Server.Cargo.Components;
using Content.Server.Cargo.Systems;
using Content.Server.Nutrition.Components;
using Content.Server.Nutrition.EntitySystems;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.Mobs.Components;
using Content.Shared.Prototypes;
@@ -14,6 +12,8 @@ using Content.Shared.Whitelist;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Content.Shared.Storage;
using Content.Shared.Tools.Components;
namespace Content.IntegrationTests.Tests;
@@ -151,7 +151,6 @@ public sealed class CargoTest : GameTest
var componentFactory = server.ResolveDependency<IComponentFactory>();
var whitelist = entManager.System<EntityWhitelistSystem>();
var cargo = entManager.System<CargoSystem>();
var sliceableSys = entManager.System<SliceableFoodSystem>();
var bounties = protoManager.EnumeratePrototypes<CargoBountyPrototype>().ToList();
@@ -164,14 +163,14 @@ public sealed class CargoTest : GameTest
var sliceableEntityProtos = protoManager.EnumeratePrototypes<EntityPrototype>()
.Where(p => !p.Abstract)
.Where(p => !pair.IsTestPrototype(p))
.Where(p => p.TryGetComponent<SliceableFoodComponent>(out _, componentFactory))
.Where(p => p.TryGetComponent<ToolRefinableComponent>(out _, componentFactory))
.Select(p => p.ID)
.ToList();
foreach (var proto in sliceableEntityProtos)
{
var ent = entManager.SpawnEntity(proto, coord);
var sliceable = entManager.GetComponent<SliceableFoodComponent>(ent);
var sliceable = entManager.GetComponent<ToolRefinableComponent>(ent);
// Check each bounty
foreach (var bounty in bounties)
@@ -184,19 +183,32 @@ public sealed class CargoTest : GameTest
continue;
// Spawn a slice
var slice = entManager.SpawnEntity(sliceable.Slice, coord);
// See if the slice also counts for this bounty entry
if (!cargo.IsValidBountyEntry(slice, entry))
var sliceCountByProtoId = EntitySpawnCollection.GetSpawns(sliceable.RefineResult)
.GroupBy(x => x)
.ToDictionary(x => x.Key, x => x.Count());
foreach (var (sliceProtoId, sliceCount) in sliceCountByProtoId)
{
var slice = entManager.SpawnEntity(sliceProtoId, coord);
// See if the slice also counts for this bounty entry
if (!cargo.IsValidBountyEntry(slice, entry))
{
entManager.DeleteEntity(slice);
continue;
}
entManager.DeleteEntity(slice);
continue;
// If for some reason it can only make one slice, that's okay, I guess
Assert.That(
sliceCount,
Is.EqualTo(1),
$"{proto} counts as part of cargo bounty {bounty.ID} "
+ $"and slices into {sliceCount} slices which count for the same bounty!"
);
}
entManager.DeleteEntity(slice);
// If for some reason it can only make one slice, that's okay, I guess
Assert.That(sliceable.TotalCount, Is.EqualTo(1), $"{proto} counts as part of cargo bounty {bounty.ID} and slices into {sliceable.TotalCount} slices which count for the same bounty!");
}
}
@@ -21,7 +21,7 @@ public sealed class ObjectiveCommandsTest : GameTest
components:
- type: Objective
difficulty: 1
issuer: objective-issuer-syndicate
issuer: TheSyndicate
icon:
sprite: error.rsi
state: error
@@ -0,0 +1,79 @@
using System.Linq;
using Content.IntegrationTests.Fixtures;
using Content.IntegrationTests.Fixtures.Attributes;
using Content.Server.Destructible;
using Content.Server.Destructible.Thresholds.Behaviors;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Damage.Systems;
using Content.Shared.Destructible;
using Robust.Shared.GameObjects;
using static Content.IntegrationTests.Tests.Destructible.DestructibleTestPrototypes;
namespace Content.IntegrationTests.Tests.Destructible;
/// <summary>
/// Tests ensuring the correct operation of <see cref="SharedDestructibleSystem"/>.
/// </summary>
public sealed class DestructibleOverkillTest : GameTest
{
[SidedDependency(Side.Server)] private DamageableSystem _sDamageableSystem = default!;
[SidedDependency(Side.Server)] private TestDestructibleListenerSystem _sDestructibleListenerSystem = default!;
/// <summary>
/// Test that an entity with consequences is destroyed cleanly when overkilled.
/// </summary>
[Test]
[TestOf(typeof(DestructibleSystem))]
[Description("Test that an entity with consequences is destroyed cleanly when overkilled.")]
public async Task EnsureOverkill()
{
var testMap = await Pair.CreateTestMap();
// Entity count prior to spawning and destroying
var baseEntityCount = SEntMan.EntityCount;
EntityUid sDestructibleEntity = default;
// Spawn our test entity and threshold listener
await Server.WaitPost(() =>
{
sDestructibleEntity = SSpawnAtPosition(DestructibleDestructionEntityId, testMap.GridCoords);
_sDestructibleListenerSystem.ThresholdsReached.Clear();
});
await Server.WaitAssertion(() =>
{
var bruteDamageGroup = SProtoMan.Index<DamageGroupPrototype>(TestBruteDamageGroupId);
var bruteDamage = new DamageSpecifier(bruteDamageGroup, 200);
// Hit the destructible with enough damage to overkill
Assert.DoesNotThrow(() =>
{
_sDamageableSystem.TryChangeDamage(sDestructibleEntity, bruteDamage, true);
});
// We now verify that our component has the properties we expect
// Our first threshold should be the overkill destruction
var threshold = _sDestructibleListenerSystem.ThresholdsReached[0].Threshold;
// Ensure that the threshold triggered and only has one behavior
using (Assert.EnterMultipleScope())
{
Assert.That(threshold.Triggered, Is.True);
Assert.That(threshold.Behaviors, Has.Count.EqualTo(1));
}
var doActsBehavior = (DoActsBehavior)threshold.Behaviors.Single(b => b is DoActsBehavior);
// Ensure that the one act in this behavior is destruction
Assert.That(doActsBehavior.HasAct(ThresholdActs.Destruction));
});
await Server.WaitRunTicks(1); // Wait for predicted delete
Assert.That(SEntMan.EntityCount,
Is.EqualTo(baseEntityCount),
$"Overkill destructible test produced excess entities. Overkill did not behave as intended.");
}
}
@@ -2,7 +2,6 @@
using System.Linq;
using System.Numerics;
using Content.IntegrationTests.Fixtures;
using Content.Server.Disposal.Unit;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Disposal.Components;
@@ -22,26 +21,14 @@ namespace Content.IntegrationTests.Tests.Disposal
[Reflect(false)]
private sealed class DisposalUnitTestSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DoInsertDisposalUnitEvent>(ev =>
{
var (_, toInsert, unit) = ev;
var insertTransform = Comp<TransformComponent>(toInsert);
// Not in a tube yet
Assert.That(insertTransform.ParentUid, Is.EqualTo(unit));
}, after: new[] { typeof(SharedDisposalUnitSystem) });
}
}
private static void UnitInsert(EntityUid uid, DisposalUnitComponent unit, bool result, DisposalUnitSystem disposalSystem, params EntityUid[] entities)
private static void UnitInsert(EntityUid uid, DisposalUnitComponent unit, bool result, SharedDisposalUnitSystem disposalSystem, params EntityUid[] entities)
{
foreach (var entity in entities)
{
Assert.That(disposalSystem.CanInsert(uid, unit, entity), Is.EqualTo(result));
disposalSystem.TryInsert(uid, entity, null);
Assert.That(disposalSystem.TryInsert((uid, unit), entity, null), Is.EqualTo(result));
}
}
@@ -53,20 +40,20 @@ namespace Content.IntegrationTests.Tests.Disposal
}
}
private static void UnitInsertContains(EntityUid uid, DisposalUnitComponent unit, bool result, DisposalUnitSystem disposalSystem, params EntityUid[] entities)
private static void UnitInsertContains(EntityUid uid, DisposalUnitComponent unit, bool result, SharedDisposalUnitSystem disposalSystem, params EntityUid[] entities)
{
UnitInsert(uid, unit, result, disposalSystem, entities);
UnitContains(unit, result, entities);
}
private static void Flush(EntityUid unitEntity, DisposalUnitComponent unit, bool result, DisposalUnitSystem disposalSystem, params EntityUid[] entities)
private static void Flush(EntityUid unitEntity, DisposalUnitComponent unit, bool result, SharedDisposalUnitSystem disposalSystem, params EntityUid[] entities)
{
Assert.Multiple(() =>
{
Assert.That(unit.Container.ContainedEntities, Is.SupersetOf(entities));
Assert.That(entities, Has.Length.EqualTo(unit.Container.ContainedEntities.Count));
Assert.That(result, Is.EqualTo(disposalSystem.TryFlush(unitEntity, unit)));
Assert.That(result, Is.EqualTo(disposalSystem.TryFlush((unitEntity, unit))));
Assert.That(result || entities.Length == 0, Is.EqualTo(unit.Container.ContainedEntities.Count == 0));
});
}
@@ -123,6 +110,10 @@ namespace Content.IntegrationTests.Tests.Disposal
entryDelay: 0
draggedEntryDelay: 0
flushTime: 0
whitelist:
components:
- Item
- Body
- type: Anchorable
- type: ApcPowerReceiver
- type: Physics
@@ -162,7 +153,7 @@ namespace Content.IntegrationTests.Tests.Disposal
var entityManager = server.ResolveDependency<IEntityManager>();
var xformSystem = entityManager.System<SharedTransformSystem>();
var disposalSystem = entityManager.System<DisposalUnitSystem>();
var disposalSystem = entityManager.System<SharedDisposalUnitSystem>();
var power = entityManager.System<PowerReceiverSystem>();
await server.WaitAssertion(() =>
@@ -13,6 +13,7 @@ using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager.Attributes;
using Robust.Shared.Spawners;
namespace Content.IntegrationTests.Tests
{
@@ -290,6 +291,8 @@ namespace Content.IntegrationTests.Tests
// If the entity deleted itself, check that it didn't spawn other entities
if (!server.EntMan.EntityExists(uid))
{
await CleanupTransientEntities(pair, serverEntities);
Assert.That(Count(server.EntMan), Is.EqualTo(count), $"Server prototype {protoId} failed on deleting itself\n" +
BuildDiffString(serverEntities, Entities(server.EntMan), server.EntMan));
Assert.That(Count(client.EntMan), Is.EqualTo(clientCount), $"Client prototype {protoId} failed on deleting itself\n" +
@@ -309,6 +312,7 @@ namespace Content.IntegrationTests.Tests
await server.WaitPost(() => server.EntMan.DeleteEntity(uid));
await pair.RunTicksSync(3);
await CleanupTransientEntities(pair, serverEntities);
// Check that the number of entities has gone back to the original value.
Assert.That(Count(server.EntMan), Is.EqualTo(count), $"Server prototype {protoId} failed on deletion: count didn't reset properly\n" +
@@ -321,6 +325,33 @@ namespace Content.IntegrationTests.Tests
});
}
/// <summary>
/// Deletes any entities with <see cref="TimedDespawnComponent"/> that were not present in the baseline snapshot.
/// Some entities spawn transient side-effects on deletion (e.g. explosion visuals). These side-effect entities
/// use TimedDespawn and would persist across test iterations, corrupting baseline entity counts and causing
/// cascading assertion failures.
/// </summary>
private static async Task CleanupTransientEntities(Pair.TestPair pair, HashSet<EntityUid> baselineEntities)
{
var server = pair.Server;
await server.WaitPost(() =>
{
var toRemove = new List<EntityUid>();
var query = server.EntMan.AllEntityQueryEnumerator<TimedDespawnComponent>();
while (query.MoveNext(out var uid, out _))
{
if (!baselineEntities.Contains(uid))
toRemove.Add(uid);
}
foreach (var uid in toRemove)
{
server.EntMan.DeleteEntity(uid);
}
});
await pair.RunTicksSync(3);
}
private static string BuildDiffString(IEnumerable<EntityUid> oldEnts, IEnumerable<EntityUid> newEnts, IEntityManager entMan)
{
var sb = new StringBuilder();
@@ -20,7 +20,7 @@ public sealed partial class SuperBonkComponent : Component
/// How often should we bonk.
/// </summary>
[DataField]
public TimeSpan BonkCooldown = TimeSpan.FromMilliseconds(100);
public TimeSpan BonkCooldown = TimeSpan.FromMilliseconds(1500);
/// <summary>
/// Next time when we will bonk.
@@ -1,7 +1,6 @@
using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Server.Administration.UI;
using Content.Server.Disposal.Tube;
using Content.Server.EUI;
using Content.Server.Ghost.Roles;
using Content.Server.Mind;
@@ -14,6 +13,7 @@ using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Configurable;
using Content.Shared.Database;
using Content.Shared.Disposal.Tube;
using Content.Shared.Examine;
using Content.Shared.GameTicking;
using Content.Shared.Inventory;
@@ -543,7 +543,7 @@ namespace Content.Server.Administration.Systems
Text = Loc.GetString("tube-direction-verb-get-data-text"),
Category = VerbCategory.Debug,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/information.svg.192dpi.png")),
Act = () => _disposalTubes.PopupDirections(args.Target, tube, args.User)
Act = () => _disposalTubes.PopupDirections((args.Target, tube), args.User)
};
args.Verbs.Add(verb);
}
@@ -1,5 +1,6 @@
using System.Buffers;
using System.Diagnostics;
using System.Numerics.Tensors;
using Content.Server.NodeContainer.NodeGroups;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
@@ -366,10 +367,9 @@ public partial class AtmosphereSystem
mixtMoles[i] = mixture.TotalMoles;
}
// TODO NumericsHelpers need a method that substitutes NaNs with zeros. AVX-512 has one iirc but for 256/128 we need to do some masking bs
NumericsHelpers.Multiply(mixtMoles, Atmospherics.R);
NumericsHelpers.Multiply(mixtMoles, mixtTemp);
NumericsHelpers.Divide(mixtMoles, mixtVol, pressures);
TensorPrimitives.Multiply(mixtMoles, Atmospherics.R, mixtMoles);
TensorPrimitives.Multiply(mixtMoles, mixtTemp, mixtMoles);
TensorPrimitives.Divide(mixtMoles, mixtVol, pressures);
}
finally
{
@@ -1,5 +1,6 @@
using System.Buffers;
using System.Collections.Concurrent;
using System.Numerics.Tensors;
using Content.Server.Atmos.Components;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
@@ -141,9 +142,13 @@ public sealed partial class AtmosphereSystem
/*
In order to perform SIMD ops we load the values into opposing pairs, where:
groupA: North, East, South, West
groupB: South, West, North, East
groupA: North, East
groupB: South, West
That way NumericsHelpers can just do vectorized operations on them super easily.
In a sense this means that the arrays, per index, would look like:
0, 1 - 0, 2
0, 1 - 1, 3
*/
for (var i = 0; i < len; i++)
{
@@ -151,15 +156,16 @@ public sealed partial class AtmosphereSystem
var pairBase = i * DeltaPressurePairCount;
for (var j = 0; j < DeltaPressurePairCount; j++)
{
groupA[pairBase + j] = pressures[presBase + j];
groupB[pairBase + j] = pressures[presBase + j + DeltaPressurePairCount];
var pressurePairBase = DeltaPressurePairCount * j;
groupA[pairBase + j] = pressures[presBase + pressurePairBase];
groupB[pairBase + j] = pressures[presBase + pressurePairBase + 1];
}
}
// Time to get crankin
NumericsHelpers.Max(groupA, groupB, groupMax);
NumericsHelpers.Sub(groupA, groupB);
NumericsHelpers.Abs(groupA);
TensorPrimitives.Max(groupA, groupB, groupMax);
TensorPrimitives.Subtract(groupA, groupB, groupA);
TensorPrimitives.Abs(groupA, groupA);
// Now go through each entity and determine their max pressure & delta pressure.
// Queue for damage if necessary.
@@ -1,4 +1,5 @@
using System.Linq;
using System.Numerics.Tensors;
using System.Runtime.CompilerServices;
using Content.Server.Atmos.Reactions;
using Content.Shared.Atmos;
@@ -36,40 +37,40 @@ namespace Content.Server.Atmos.EntitySystems
public override float GetMass(float[] moles)
{
Span<float> tmp = stackalloc float[moles.Length];
NumericsHelpers.Multiply(moles, GasMolarMasses, tmp);
TensorPrimitives.Multiply(moles, GasMolarMasses, tmp);
// Conversion of grams to kilograms.
return NumericsHelpers.HorizontalAdd(tmp) * Atmospherics.gToKg;
return TensorPrimitives.Sum(tmp) * Atmospherics.gToKg;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
protected override float GetHeatCapacityCalculation(float[] moles, bool space)
{
// Little hack to make space gas mixtures have heat capacity, therefore allowing them to cool down rooms.
if (space && MathHelper.CloseTo(NumericsHelpers.HorizontalAdd(moles), 0f))
if (space && MathHelper.CloseTo(TensorPrimitives.Sum(moles), 0f))
{
return Atmospherics.SpaceHeatCapacity;
}
Span<float> tmp = stackalloc float[moles.Length];
NumericsHelpers.Multiply(moles, GasMolarHeatCapacities, tmp);
TensorPrimitives.Multiply(moles, GasMolarHeatCapacities, tmp);
// Adjust heat capacity by speedup, because this is primarily what
// determines how quickly gases heat up/cool.
return MathF.Max(NumericsHelpers.HorizontalAdd(tmp), Atmospherics.MinimumHeatCapacity);
return MathF.Max(TensorPrimitives.Sum(tmp), Atmospherics.MinimumHeatCapacity);
}
public override bool IsMixtureFuel(GasMixture mixture, float epsilon = Atmospherics.Epsilon)
{
Span<float> tmp = stackalloc float[Atmospherics.AdjustedNumberOfGases];
NumericsHelpers.Multiply(mixture.Moles, GasFuelMask, tmp);
return NumericsHelpers.HorizontalAdd(tmp) > epsilon;
TensorPrimitives.Multiply(mixture.Moles, GasFuelMask, tmp);
return TensorPrimitives.Sum(tmp) > epsilon;
}
public override bool IsMixtureOxidizer(GasMixture mixture, float epsilon = Atmospherics.Epsilon)
{
Span<float> tmp = stackalloc float[Atmospherics.AdjustedNumberOfGases];
NumericsHelpers.Multiply(mixture.Moles, GasOxidizerMask, tmp);
return NumericsHelpers.HorizontalAdd(tmp) > epsilon;
TensorPrimitives.Multiply(mixture.Moles, GasOxidizerMask, tmp);
return TensorPrimitives.Sum(tmp) > epsilon;
}
/// <summary>
@@ -131,8 +132,8 @@ namespace Content.Server.Atmos.EntitySystems
}
// transfer moles
NumericsHelpers.Multiply(source.Moles, fraction, buffer);
NumericsHelpers.Add(receiver.Moles, buffer);
TensorPrimitives.Multiply(source.Moles, fraction, buffer);
TensorPrimitives.Add(receiver.Moles, buffer, receiver.Moles);
}
}
@@ -325,13 +326,8 @@ namespace Content.Server.Atmos.EntitySystems
[PublicAPI]
public static void AddMolsToMixture(GasMixture mixture, ReadOnlySpan<float> molsToAdd)
{
// Span length should be as long as the length of the gas array.
// Technically this is a redundant check because NumericsHelpers will do the same thing,
// but eh.
ArgumentOutOfRangeException.ThrowIfNotEqual(mixture.Moles.Length, molsToAdd.Length, nameof(mixture.Moles.Length));
NumericsHelpers.Add(mixture.Moles, molsToAdd);
NumericsHelpers.Max(mixture.Moles, 0f);
TensorPrimitives.Add(mixture.Moles, molsToAdd, mixture.Moles);
TensorPrimitives.Max(mixture.Moles, 0f, mixture.Moles);
}
public enum GasCompareResult
@@ -50,7 +50,7 @@ public sealed partial class GasTankSystem : SharedGasTankSystem
entity.Comp.CheckUser = false;
if (Transform(entity).ParentUid != entity.Comp.User)
{
DisconnectFromInternals(entity);
DisconnectFromInternals(entity, forced: true);
}
}
+16 -14
View File
@@ -1,16 +1,15 @@
using System.Numerics;
using Content.Server.Beam.Components;
using Content.Shared.Beam;
using Content.Shared.Beam.Components;
using Content.Shared.Physics;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Prototypes;
namespace Content.Server.Beam;
@@ -22,6 +21,8 @@ public sealed partial class BeamSystem : SharedBeamSystem
[Dependency] private SharedBroadphaseSystem _broadphase = default!;
[Dependency] private SharedPhysicsSystem _physics = default!;
private static readonly EntProtoId VirtualBeamEntityControllerId = "VirtualBeamEntityController";
public override void Initialize()
{
base.Initialize();
@@ -77,8 +78,8 @@ public sealed partial class BeamSystem : SharedBeamSystem
string shader = "unshaded")
{
var beamSpawnPos = beamStartPos;
var ent = Spawn(prototype, beamSpawnPos);
var shape = new EdgeShape(distanceCorrection, new Vector2(0,0));
var ent = Spawn(prototype, beamSpawnPos, rotation: userAngle);
var shape = new EdgeShape(distanceCorrection, new Vector2(0, 0));
if (!TryComp<PhysicsComponent>(ent, out var physics) || !TryComp<BeamComponent>(ent, out var beam))
return;
@@ -108,7 +109,7 @@ public sealed partial class BeamSystem : SharedBeamSystem
else
{
var controllerEnt = Spawn("VirtualBeamEntityController", beamSpawnPos);
var controllerEnt = Spawn(VirtualBeamEntityControllerId, beamSpawnPos);
beam.VirtualBeamController = controllerEnt;
_audio.PlayPvs(beam.Sound, ent);
@@ -118,10 +119,11 @@ public sealed partial class BeamSystem : SharedBeamSystem
}
//Create the rest of the beam, sprites handled through the BeamVisualizerEvent
for (var i = 0; i < distanceLength-1; i++)
for (var i = 0; i < distanceLength - 1; i++)
{
beamSpawnPos = beamSpawnPos.Offset(calculatedDistance.Normalized());
var newEnt = Spawn(prototype, beamSpawnPos);
_transform.SetWorldRotation(newEnt, userAngle);
var ev = new BeamVisualizerEvent(GetNetEntity(newEnt), distanceLength, userAngle, bodyState, shader);
RaiseNetworkEvent(ev);
@@ -140,7 +142,7 @@ public sealed partial class BeamSystem : SharedBeamSystem
/// <param name="bodyPrototype">The prototype spawned when this beam is created</param>
/// <param name="bodyState">Optional sprite state for the <see cref="bodyPrototype"/> if a default one is not given</param>
/// <param name="shader">Optional shader for the <see cref="bodyPrototype"/> if a default one is not given</param>
/// <param name="controller"></param>
/// <param name="controller">The virtual beam controller entity to use. If null one will be spawned.</param>
public void TryCreateBeam(EntityUid user, EntityUid target, string bodyPrototype, string? bodyState = null, string shader = "unshaded", EntityUid? controller = null)
{
if (Deleted(user) || Deleted(target))
@@ -149,21 +151,21 @@ public sealed partial class BeamSystem : SharedBeamSystem
var userMapPos = _transform.GetMapCoordinates(user);
var targetMapPos = _transform.GetMapCoordinates(target);
//The distance between the target and the user.
var calculatedDistance = targetMapPos.Position - userMapPos.Position;
var userAngle = calculatedDistance.ToWorldAngle();
if (userMapPos.MapId != targetMapPos.MapId)
return;
//Where the start of the beam will spawn
var beamStartPos = userMapPos.Offset(calculatedDistance.Normalized());
//The distance between the target and the user.
var calculatedDistance = targetMapPos.Position - userMapPos.Position;
var userAngle = calculatedDistance.ToWorldAngle();
//Don't divide by zero
if (calculatedDistance.Length() == 0)
return;
if (controller != null && TryComp<BeamComponent>(controller, out var controllerBeamComp))
//Where the start of the beam will spawn
var beamStartPos = userMapPos.Offset(calculatedDistance.Normalized());
if (TryComp<BeamComponent>(controller, out var controllerBeamComp))
{
controllerBeamComp.HitTargets.Add(user);
controllerBeamComp.HitTargets.Add(target);
@@ -1,8 +0,0 @@
using Content.Shared.Beam.Components;
namespace Content.Server.Beam.Components;
[RegisterComponent]
public sealed partial class BeamComponent : SharedBeamComponent
{
}
@@ -1,17 +0,0 @@
using Content.Server.Botany.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Botany.Components;
// TODO: This should probably be merged with SliceableFood somehow or made into a more generic Choppable.
// Yeah this is pretty trash. also consolidating this type of behavior will avoid future transform parenting bugs (see #6090).
[RegisterComponent]
[Access(typeof(LogSystem))]
public sealed partial class LogComponent : Component
{
[DataField("spawnedPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string SpawnedPrototype = "MaterialWoodPlank1";
[DataField("spawnCount")] public int SpawnCount = 2;
}
@@ -1,6 +1,8 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.Tools;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Server.Botany.Components;
@@ -138,4 +140,10 @@ public sealed partial class PlantHolderComponent : Component
[ViewVariables]
public Entity<SolutionComponent>? SoilSolution = null;
/// <summary>
/// Tool quality that required if plant should be harvested with specified tool.
/// </summary>
[DataField]
public ProtoId<ToolQualityPrototype>? HarvestToolQuality = "Sawing";
}
+1 -1
View File
@@ -193,7 +193,7 @@ public partial class SeedData
[DataField] public bool Viable = true;
/// <summary>
/// If true, a sharp tool is required to harvest this plant.
/// If true, a tool with sliceable quality is required to harvest this plant.
/// </summary>
[DataField] public bool Ligneous;
@@ -15,7 +15,8 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Kitchen.Components;
using Content.Shared.Tools.Systems;
using Content.Shared.Tools;
namespace Content.Server.Botany.Systems;
@@ -30,6 +31,9 @@ public sealed partial class BotanySystem : EntitySystem
[Dependency] private MetaDataSystem _metaData = default!;
[Dependency] private RandomHelperSystem _randomHelper = default!;
[Dependency] private ISharedAdminLogManager _adminLogger = default!;
[Dependency] private SharedToolSystem _tools = default!;
private static readonly ProtoId<ToolQualityPrototype> HarvestTool = "Slicing";
public override void Initialize()
{
@@ -194,7 +198,7 @@ public sealed partial class BotanySystem : EntitySystem
public bool CanHarvest(SeedData proto, EntityUid? held = null)
{
return !proto.Ligneous || proto.Ligneous && held != null && HasComp<SharpComponent>(held);
return !proto.Ligneous || proto.Ligneous && held != null && _tools.HasQuality(held.Value, HarvestTool);
}
#endregion
@@ -1,50 +0,0 @@
using Content.Server.Botany.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Kitchen.Components;
using Content.Shared.Random;
using Robust.Shared.Containers;
namespace Content.Server.Botany.Systems;
public sealed partial class LogSystem : EntitySystem
{
[Dependency] private SharedHandsSystem _handsSystem = default!;
[Dependency] private SharedContainerSystem _containerSystem = default!;
[Dependency] private RandomHelperSystem _randomHelper = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<LogComponent, InteractUsingEvent>(OnInteractUsing);
}
private void OnInteractUsing(EntityUid uid, LogComponent component, InteractUsingEvent args)
{
if (!HasComp<SharpComponent>(args.Used))
return;
// if in some container, try pick up, else just drop to world
var inContainer = _containerSystem.IsEntityInContainer(uid);
var pos = Transform(uid).Coordinates;
for (var i = 0; i < component.SpawnCount; i++)
{
var plank = Spawn(component.SpawnedPrototype, pos);
if (inContainer)
_handsSystem.PickupOrDrop(args.User, plank);
else
{
var xform = Transform(plank);
_containerSystem.AttachParentToContainerOrGrid((plank, xform));
xform.LocalRotation = 0;
_randomHelper.RandomOffset(plank, 0.25f);
}
}
QueueDel(uid);
args.Handled = true;
}
}
@@ -11,7 +11,6 @@ using Content.Shared.Chemistry.Reagent;
using Content.Shared.Coordinates.Helpers;
using Content.Shared.Examine;
using Content.Shared.FixedPoint;
using Content.Shared.Hands.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Popups;
@@ -23,12 +22,11 @@ using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Content.Shared.Chemistry.Reaction;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Database;
using Content.Shared.EntityEffects;
using Content.Shared.Kitchen.Components;
using Content.Shared.Labels.Components;
using Content.Shared.Tools.Systems;
namespace Content.Server.Botany.Systems;
@@ -50,6 +48,7 @@ public sealed partial class PlantHolderSystem : EntitySystem
[Dependency] private ItemSlotsSystem _itemSlots = default!;
[Dependency] private ISharedAdminLogManager _adminLogger = default!;
[Dependency] private SharedEntityEffectsSystem _entityEffects = default!;
[Dependency] private SharedToolSystem _tool = default!;
public const float HydroponicsSpeedMultiplier = 1f;
public const float HydroponicsConsumptionMultiplier = 2f;
@@ -323,7 +322,8 @@ public sealed partial class PlantHolderSystem : EntitySystem
return;
}
if (HasComp<SharpComponent>(args.Used))
var harvestToolQuality = entity.Comp.HarvestToolQuality;
if (harvestToolQuality.HasValue && _tool.HasQuality(args.Used, harvestToolQuality.Value))
{
args.Handled = true;
DoHarvest(uid, args.User, component);
@@ -5,6 +5,7 @@ using Content.Shared.CharacterInfo;
using Content.Shared.Objectives;
using Content.Shared.Objectives.Components;
using Content.Shared.Objectives.Systems;
using Robust.Shared.Prototypes;
namespace Content.Server.CharacterInfo;
@@ -14,6 +15,7 @@ public sealed partial class CharacterInfoSystem : EntitySystem
[Dependency] private MindSystem _minds = default!;
[Dependency] private RoleSystem _roles = default!;
[Dependency] private SharedObjectivesSystem _objectives = default!;
[Dependency] private IPrototypeManager _protoMan = default!;
public override void Initialize()
{
@@ -42,8 +44,14 @@ public sealed partial class CharacterInfoSystem : EntitySystem
if (info == null)
continue;
if (!_protoMan.TryIndex(Comp<ObjectiveComponent>(objective).Issuer, out var issuerProto))
{
Log.Error($"Found incorrect objective issuer {issuerProto} when generating character info for objective {MetaData(objective).EntityPrototype}.");
continue;
}
// group objectives by their issuer
var issuer = Comp<ObjectiveComponent>(objective).LocIssuer;
var issuer = issuerProto.LocalizedName;
if (!objectives.ContainsKey(issuer))
objectives[issuer] = new List<ObjectiveInfo>();
objectives[issuer].Add(info.Value);
@@ -0,0 +1,86 @@
using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Station.Components;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Utility;
namespace Content.Server.Chat.Systems;
public sealed partial class ChatSystem
{
/// <inheritdoc />
public override void DispatchGlobalAnnouncement(
string message,
string? sender = null,
bool playSound = true,
SoundSpecifier? announcementSound = null,
Color? colorOverride = null
)
{
sender ??= Loc.GetString("chat-manager-sender-announcement");
var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message)));
_chatManager.ChatMessageToAll(ChatChannel.Radio, message, wrappedMessage, default, false, true, colorOverride);
if (playSound)
{
_audio.PlayGlobal(announcementSound ?? DefaultAnnouncementSound, Filter.Broadcast(), true, AudioParams.Default.WithVolume(-2f));
}
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Global station announcement from {sender}: {message}");
}
/// <inheritdoc />
public override void DispatchFilteredAnnouncement(
Filter filter,
string message,
EntityUid? source = null,
string? sender = null,
bool playSound = true,
SoundSpecifier? announcementSound = null,
Color? colorOverride = null)
{
sender ??= Loc.GetString("chat-manager-sender-announcement");
var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message)));
_chatManager.ChatMessageToManyFiltered(filter, ChatChannel.Radio, message, wrappedMessage, source ?? default, false, true, colorOverride);
if (playSound)
{
_audio.PlayGlobal(announcementSound ?? DefaultAnnouncementSound, filter, true, AudioParams.Default.WithVolume(-2f));
}
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Station Announcement from {sender}: {message}");
}
/// <inheritdoc />
public override void DispatchStationAnnouncement(
EntityUid source,
string message,
string? sender = null,
bool playDefaultSound = true,
SoundSpecifier? announcementSound = null,
Color? colorOverride = null)
{
sender ??= Loc.GetString("chat-manager-sender-announcement");
var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message)));
var station = _stationSystem.GetOwningStation(source);
if (station == null)
{
// you can't make a station announcement without a station
return;
}
if (!TryComp<StationDataComponent>(station, out var stationDataComp)) return;
var filter = _stationSystem.GetInStation(stationDataComp);
_chatManager.ChatMessageToManyFiltered(filter, ChatChannel.Radio, message, wrappedMessage, source, false, true, colorOverride);
if (playDefaultSound)
{
_audio.PlayGlobal(announcementSound ?? DefaultAnnouncementSound, filter, true, AudioParams.Default.WithVolume(-2f));
}
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Station Announcement on {station} from {sender}: {message}");
}
}
@@ -0,0 +1,259 @@
using System.Linq;
using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.IdentityManagement;
using Content.Shared.Radio;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Chat.Systems;
public sealed partial class ChatSystem
{
private void SendEntitySpeak(
EntityUid source,
string originalMessage,
ChatTransmitRange range,
string? nameOverride,
bool hideLog = false,
bool ignoreActionBlocker = false
)
{
if (!_actionBlocker.CanSpeak(source) && !ignoreActionBlocker)
return;
var message = TransformSpeech(source, originalMessage);
if (message.Length == 0)
return;
var speech = GetSpeechVerb(source, message);
// get the entity's apparent name (if no override provided).
string name;
if (nameOverride != null)
{
name = nameOverride;
}
else
{
var nameEv = new TransformSpeakerNameEvent(source, Name(source));
RaiseLocalEvent(source, nameEv);
name = nameEv.VoiceName;
// Check for a speech verb override
if (nameEv.SpeechVerb != null && _prototypeManager.Resolve(nameEv.SpeechVerb, out var proto))
speech = proto;
}
name = FormattedMessage.EscapeText(name);
var wrappedMessage = Loc.GetString(speech.Bold ? "chat-manager-entity-say-bold-wrap-message" : "chat-manager-entity-say-wrap-message",
("entityName", name),
("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))),
("fontType", speech.FontId),
("fontSize", speech.FontSize),
("message", FormattedMessage.EscapeText(message)));
SendInVoiceRange(ChatChannel.Local, message, wrappedMessage, source, range);
var ev = new EntitySpokeEvent(source, message, null, null);
RaiseLocalEvent(source, ev, true);
// To avoid logging any messages sent by entities that are not players, like vendors, cloning, etc.
// Also doesn't log if hideLog is true.
if (!HasComp<ActorComponent>(source) || hideLog)
return;
if (originalMessage == message)
{
if (name != Name(source))
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {source} as {name}: {originalMessage}.");
else
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {source}: {originalMessage}.");
}
else
{
if (name != Name(source))
_adminLogger.Add(LogType.Chat, LogImpact.Low,
$"Say from {source} as {name}, original: {originalMessage}, transformed: {message}.");
else
_adminLogger.Add(LogType.Chat, LogImpact.Low,
$"Say from {source}, original: {originalMessage}, transformed: {message}.");
}
}
private void SendEntityWhisper(
EntityUid source,
string originalMessage,
ChatTransmitRange range,
RadioChannelPrototype? channel,
string? nameOverride,
bool hideLog = false,
bool ignoreActionBlocker = false
)
{
if (!_actionBlocker.CanSpeak(source) && !ignoreActionBlocker)
return;
var message = TransformSpeech(source, FormattedMessage.RemoveMarkupOrThrow(originalMessage));
if (message.Length == 0)
return;
var obfuscatedMessage = ObfuscateMessageReadability(message, 0.2f);
// get the entity's name by visual identity (if no override provided).
string nameIdentity = FormattedMessage.EscapeText(nameOverride ?? Identity.Name(source, EntityManager));
// get the entity's name by voice (if no override provided).
string name;
if (nameOverride != null)
{
name = nameOverride;
}
else
{
var nameEv = new TransformSpeakerNameEvent(source, Name(source));
RaiseLocalEvent(source, nameEv);
name = nameEv.VoiceName;
}
name = FormattedMessage.EscapeText(name);
var wrappedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message",
("entityName", name), ("message", FormattedMessage.EscapeText(message)));
var wrappedobfuscatedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message",
("entityName", nameIdentity), ("message", FormattedMessage.EscapeText(obfuscatedMessage)));
var wrappedUnknownMessage = Loc.GetString("chat-manager-entity-whisper-unknown-wrap-message",
("message", FormattedMessage.EscapeText(obfuscatedMessage)));
foreach (var (session, data) in GetRecipients(source, WhisperMuffledRange))
{
EntityUid listener;
if (session.AttachedEntity is not { Valid: true } playerEntity)
continue;
listener = session.AttachedEntity.Value;
if (MessageRangeCheck(session, data, range) != MessageRangeCheckResult.Full)
continue; // Won't get logged to chat, and ghosts are too far away to see the pop-up, so we just won't send it to them.
if (data.Range <= WhisperClearRange || data.Observer)
_chatManager.ChatMessageToOne(ChatChannel.Whisper, message, wrappedMessage, source, false, session.Channel);
//If listener is too far, they only hear fragments of the message
else if (_examineSystem.InRangeUnOccluded(source, listener, WhisperMuffledRange))
_chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedobfuscatedMessage, source, false, session.Channel);
//If listener is too far and has no line of sight, they can't identify the whisperer's identity
else
_chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedUnknownMessage, source, false, session.Channel);
}
_replay.RecordServerMessage(new ChatMessage(ChatChannel.Whisper, message, wrappedMessage, GetNetEntity(source), null, MessageRangeHideChatForReplay(range)));
var ev = new EntitySpokeEvent(source, message, channel, obfuscatedMessage);
RaiseLocalEvent(source, ev, true);
if (!hideLog)
if (originalMessage == message)
{
if (name != Name(source))
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {source} as {name}: {originalMessage}.");
else
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {source}: {originalMessage}.");
}
else
{
if (name != Name(source))
_adminLogger.Add(LogType.Chat, LogImpact.Low,
$"Whisper from {source} as {name}, original: {originalMessage}, transformed: {message}.");
else
_adminLogger.Add(LogType.Chat, LogImpact.Low,
$"Whisper from {source}, original: {originalMessage}, transformed: {message}.");
}
}
protected override void SendEntityEmote(
EntityUid source,
string action,
ChatTransmitRange range,
string? nameOverride,
bool hideLog = false,
bool checkEmote = true,
bool ignoreActionBlocker = false,
NetUserId? author = null
)
{
if (!_actionBlocker.CanEmote(source) && !ignoreActionBlocker)
return;
// get the entity's apparent name (if no override provided).
var ent = Identity.Entity(source, EntityManager);
string name = FormattedMessage.EscapeText(nameOverride ?? Name(ent));
// Emotes use Identity.Name, since it doesn't actually involve your voice at all.
var wrappedMessage = Loc.GetString("chat-manager-entity-me-wrap-message",
("entityName", name),
("entity", ent),
("message", FormattedMessage.RemoveMarkupOrThrow(action)));
if (checkEmote &&
!TryEmoteChatInput(source, action))
return;
SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, range, author);
if (!hideLog)
if (name != Name(source))
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source} as {name}: {action}");
else
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source}: {action}");
}
// ReSharper disable once InconsistentNaming
private void SendLOOC(EntityUid source, ICommonSession player, string message, bool hideChat)
{
var name = FormattedMessage.EscapeText(Identity.Name(source, EntityManager));
if (_adminManager.IsAdmin(player))
{
if (!_adminLoocEnabled) return;
}
else if (!_loocEnabled) return;
// If crit player LOOC is disabled, don't send the message at all.
if (!_critLoocEnabled && _mobStateSystem.IsCritical(source))
return;
var wrappedMessage = Loc.GetString("chat-manager-entity-looc-wrap-message",
("entityName", name),
("message", FormattedMessage.EscapeText(message)));
SendInVoiceRange(ChatChannel.LOOC, message, wrappedMessage, source, hideChat ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal, player.UserId);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"LOOC from {source}: {message}");
}
private void SendDeadChat(EntityUid source, ICommonSession player, string message, bool hideChat)
{
var clients = GetDeadChatClients();
var playerName = Name(source);
string wrappedMessage;
if (_adminManager.IsAdmin(player))
{
wrappedMessage = Loc.GetString("chat-manager-send-admin-dead-chat-wrap-message",
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")),
("userName", player.Channel.UserName),
("message", FormattedMessage.EscapeText(message)));
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Admin dead chat from {source}: {message}");
}
else
{
wrappedMessage = Loc.GetString("chat-manager-send-dead-chat-wrap-message",
("deadChannelName", Loc.GetString("chat-manager-dead-channel-name")),
("playerName", (playerName)),
("message", FormattedMessage.EscapeText(message)));
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Dead chat from {source}: {message}");
}
_chatManager.ChatMessageToMany(ChatChannel.Dead, message, wrappedMessage, source, hideChat, true, clients.ToList(), author: player.UserId);
}
}
@@ -0,0 +1,259 @@
using System.Linq;
using System.Text;
using Content.Server.Speech.Prototypes;
using Content.Shared.Chat;
using Content.Shared.Ghost;
using Content.Shared.Players;
using Robust.Shared.Console;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Chat.Systems;
public sealed partial class ChatSystem
{
private enum MessageRangeCheckResult
{
Disallowed,
HideChat,
Full
}
/// <summary>
/// If hideChat should be set as far as replays are concerned.
/// </summary>
private bool MessageRangeHideChatForReplay(ChatTransmitRange range)
{
return range == ChatTransmitRange.HideChat;
}
/// <summary>
/// Checks if a target as returned from GetRecipients should receive the message.
/// Keep in mind data.Range is -1 for out of range observers.
/// </summary>
private MessageRangeCheckResult MessageRangeCheck(ICommonSession session, ICChatRecipientData data, ChatTransmitRange range)
{
var initialResult = MessageRangeCheckResult.Full;
switch (range)
{
case ChatTransmitRange.Normal:
initialResult = MessageRangeCheckResult.Full;
break;
case ChatTransmitRange.GhostRangeLimit:
initialResult = (data.Observer && data.Range < 0 && !_adminManager.IsAdmin(session)) ? MessageRangeCheckResult.HideChat : MessageRangeCheckResult.Full;
break;
case ChatTransmitRange.HideChat:
initialResult = MessageRangeCheckResult.HideChat;
break;
case ChatTransmitRange.NoGhosts:
initialResult = (data.Observer && !_adminManager.IsAdmin(session)) ? MessageRangeCheckResult.Disallowed : MessageRangeCheckResult.Full;
break;
}
var insistHideChat = data.HideChatOverride ?? false;
var insistNoHideChat = !(data.HideChatOverride ?? true);
if (insistHideChat && initialResult == MessageRangeCheckResult.Full)
return MessageRangeCheckResult.HideChat;
if (insistNoHideChat && initialResult == MessageRangeCheckResult.HideChat)
return MessageRangeCheckResult.Full;
return initialResult;
}
/// <summary>
/// Sends a chat message to the given players in range of the source entity.
/// </summary>
private void SendInVoiceRange(ChatChannel channel, string message, string wrappedMessage, EntityUid source, ChatTransmitRange range, NetUserId? author = null)
{
foreach (var (session, data) in GetRecipients(source, VoiceRange))
{
var entRange = MessageRangeCheck(session, data, range);
if (entRange == MessageRangeCheckResult.Disallowed)
continue;
var entHideChat = entRange == MessageRangeCheckResult.HideChat;
_chatManager.ChatMessageToOne(channel, message, wrappedMessage, source, entHideChat, session.Channel, author: author);
}
_replay.RecordServerMessage(new ChatMessage(channel, message, wrappedMessage, GetNetEntity(source), null, MessageRangeHideChatForReplay(range)));
}
/// <summary>
/// Returns true if the given player is 'allowed' to send the given message, false otherwise.
/// </summary>
private bool CanSendInGame(string message, IConsoleShell? shell = null, ICommonSession? player = null)
{
// Non-players don't have to worry about these restrictions.
if (player == null)
return true;
var mindContainerComponent = player.ContentData()?.Mind;
if (mindContainerComponent == null)
{
shell?.WriteError("You don't have a mind!");
return false;
}
if (player.AttachedEntity is not { Valid: true } _)
{
shell?.WriteError("You don't have an entity!");
return false;
}
return !_chatManager.MessageCharacterLimit(player, message);
}
// ReSharper disable once InconsistentNaming
private string SanitizeInGameICMessage(EntityUid source, string message, out string? emoteStr, bool capitalize = true, bool punctuate = false, bool capitalizeTheWordI = true)
{
var newMessage = SanitizeMessageReplaceWords(message.Trim());
GetRadioKeycodePrefix(source, newMessage, out newMessage, out var prefix);
// Sanitize it first as it might change the word order
_sanitizer.TrySanitizeEmoteShorthands(newMessage, source, out newMessage, out emoteStr);
if (capitalize)
newMessage = SanitizeMessageCapital(newMessage);
if (capitalizeTheWordI)
newMessage = SanitizeMessageCapitalizeTheWordI(newMessage, "i");
if (punctuate)
newMessage = SanitizeMessagePeriod(newMessage);
return prefix + newMessage;
}
private string SanitizeInGameOOCMessage(string message)
{
var newMessage = message.Trim();
newMessage = FormattedMessage.EscapeText(newMessage);
return newMessage;
}
public string TransformSpeech(EntityUid sender, string message)
{
var ev = new TransformSpeechEvent(sender, message);
RaiseLocalEvent(sender, ev, true);
return ev.Message;
}
public bool CheckIgnoreSpeechBlocker(EntityUid sender, bool ignoreBlocker)
{
if (ignoreBlocker)
return ignoreBlocker;
var ev = new CheckIgnoreSpeechBlockerEvent(sender, ignoreBlocker);
RaiseLocalEvent(sender, ev, true);
return ev.IgnoreBlocker;
}
private IEnumerable<INetChannel> GetDeadChatClients()
{
return Filter.Empty()
.AddWhereAttachedEntity(HasComp<GhostComponent>)
.Recipients
.Union(_adminManager.ActiveAdmins)
.Select(p => p.Channel);
}
private string SanitizeMessagePeriod(string message)
{
if (string.IsNullOrEmpty(message))
return message;
// Adds a period if the last character is a letter.
if (char.IsLetter(message[^1]))
message += ".";
return message;
}
public static readonly ProtoId<ReplacementAccentPrototype> ChatSanitize_Accent = "chatsanitize";
public string SanitizeMessageReplaceWords(string message)
{
if (string.IsNullOrEmpty(message)) return message;
var msg = message;
msg = _wordreplacement.ApplyReplacements(msg, ChatSanitize_Accent);
return msg;
}
/// <summary>
/// Returns list of players and ranges for all players withing some range. Also returns observers with a range of -1.
/// </summary>
private Dictionary<ICommonSession, ICChatRecipientData> GetRecipients(EntityUid source, float voiceGetRange)
{
// TODO proper speech occlusion
var recipients = new Dictionary<ICommonSession, ICChatRecipientData>();
var transformSource = Transform(source);
var sourceMapId = transformSource.MapID;
var sourceCoords = transformSource.Coordinates;
foreach (var player in _playerManager.Sessions)
{
if (player.AttachedEntity is not { Valid: true } playerEntity)
continue;
var transformEntity = Transform(playerEntity);
if (transformEntity.MapID != sourceMapId)
continue;
var observer = _ghostHearingQuery.HasComponent(playerEntity);
// even if they are a ghost hearer, in some situations we still need the range
if (sourceCoords.TryDistance(EntityManager, transformEntity.Coordinates, out var distance) && distance < voiceGetRange)
{
recipients.Add(player, new ICChatRecipientData(distance, observer));
continue;
}
if (observer)
recipients.Add(player, new ICChatRecipientData(-1, true));
}
RaiseLocalEvent(new ExpandICChatRecipientsEvent(source, voiceGetRange, recipients));
return recipients;
}
public readonly record struct ICChatRecipientData(float Range, bool Observer, bool? HideChatOverride = null)
{
}
private string ObfuscateMessageReadability(string message, float chance)
{
var modifiedMessage = new StringBuilder(message);
for (var i = 0; i < message.Length; i++)
{
if (char.IsWhiteSpace((modifiedMessage[i])))
{
continue;
}
if (_random.Prob(1 - chance))
{
modifiedMessage[i] = '~';
}
}
return modifiedMessage.ToString();
}
public string BuildGibberishString(IReadOnlyList<char> charOptions, int length)
{
var sb = new StringBuilder();
for (var i = 0; i < length; i++)
{
sb.Append(_random.Pick(charOptions));
}
return sb.ToString();
}
}
-585
View File
@@ -1,37 +1,26 @@
using System.Globalization;
using System.Linq;
using System.Text;
using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Server.Speech.EntitySystems;
using Content.Server.Speech.Prototypes;
using Content.Server.Station.Systems;
using Content.Shared.ActionBlocker;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.Ghost;
using Content.Shared.IdentityManagement;
using Content.Shared.Mobs.Systems;
using Content.Shared.Players;
using Content.Shared.Players.RateLimiting;
using Content.Shared.Radio;
using Content.Shared.Station.Components;
using Robust.Server.Player;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Replays;
using Robust.Shared.Utility;
namespace Content.Server.Chat.Systems;
@@ -294,580 +283,6 @@ public sealed partial class ChatSystem : SharedChatSystem
break;
}
}
#region Announcements
/// <inheritdoc />
public override void DispatchGlobalAnnouncement(
string message,
string? sender = null,
bool playSound = true,
SoundSpecifier? announcementSound = null,
Color? colorOverride = null
)
{
sender ??= Loc.GetString("chat-manager-sender-announcement");
var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message)));
_chatManager.ChatMessageToAll(ChatChannel.Radio, message, wrappedMessage, default, false, true, colorOverride);
if (playSound)
{
if (sender == Loc.GetString("admin-announce-announcer-default")) announcementSound = new SoundPathSpecifier(CentComAnnouncementSound); // Corvax-Announcements: Support custom alert sound from admin panel
_audio.PlayGlobal(announcementSound ?? DefaultAnnouncementSound, Filter.Broadcast(), true, AudioParams.Default.WithVolume(-2f));
}
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Global station announcement from {sender}: {message}");
}
/// <inheritdoc />
public override void DispatchFilteredAnnouncement(
Filter filter,
string message,
EntityUid? source = null,
string? sender = null,
bool playSound = true,
SoundSpecifier? announcementSound = null,
Color? colorOverride = null)
{
sender ??= Loc.GetString("chat-manager-sender-announcement");
var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message)));
_chatManager.ChatMessageToManyFiltered(filter, ChatChannel.Radio, message, wrappedMessage, source ?? default, false, true, colorOverride);
if (playSound)
{
_audio.PlayGlobal(announcementSound ?? DefaultAnnouncementSound, filter, true, AudioParams.Default.WithVolume(-2f));
}
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Station Announcement from {sender}: {message}");
}
/// <inheritdoc />
public override void DispatchStationAnnouncement(
EntityUid source,
string message,
string? sender = null,
bool playDefaultSound = true,
SoundSpecifier? announcementSound = null,
Color? colorOverride = null)
{
sender ??= Loc.GetString("chat-manager-sender-announcement");
var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message)));
var station = _stationSystem.GetOwningStation(source);
if (station == null)
{
// you can't make a station announcement without a station
return;
}
if (!TryComp<StationDataComponent>(station, out var stationDataComp)) return;
var filter = _stationSystem.GetInStation(stationDataComp);
_chatManager.ChatMessageToManyFiltered(filter, ChatChannel.Radio, message, wrappedMessage, source, false, true, colorOverride);
if (playDefaultSound)
{
_audio.PlayGlobal(announcementSound ?? DefaultAnnouncementSound, filter, true, AudioParams.Default.WithVolume(-2f));
}
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Station Announcement on {station} from {sender}: {message}");
}
#endregion
#region Private API
private void SendEntitySpeak(
EntityUid source,
string originalMessage,
ChatTransmitRange range,
string? nameOverride,
bool hideLog = false,
bool ignoreActionBlocker = false
)
{
if (!_actionBlocker.CanSpeak(source) && !ignoreActionBlocker)
return;
var message = TransformSpeech(source, originalMessage);
if (message.Length == 0)
return;
var speech = GetSpeechVerb(source, message);
// get the entity's apparent name (if no override provided).
string name;
if (nameOverride != null)
{
name = nameOverride;
}
else
{
var nameEv = new TransformSpeakerNameEvent(source, Name(source));
RaiseLocalEvent(source, nameEv);
name = nameEv.VoiceName;
// Check for a speech verb override
if (nameEv.SpeechVerb != null && _prototypeManager.Resolve(nameEv.SpeechVerb, out var proto))
speech = proto;
}
name = FormattedMessage.EscapeText(name);
var wrappedMessage = Loc.GetString(speech.Bold ? "chat-manager-entity-say-bold-wrap-message" : "chat-manager-entity-say-wrap-message",
("entityName", name),
("verb", Loc.GetString(_random.Pick(speech.SpeechVerbStrings))),
("fontType", speech.FontId),
("fontSize", speech.FontSize),
("message", FormattedMessage.EscapeText(message)));
SendInVoiceRange(ChatChannel.Local, message, wrappedMessage, source, range);
var ev = new EntitySpokeEvent(source, message, originalMessage, null, null);
RaiseLocalEvent(source, ev, true);
// To avoid logging any messages sent by entities that are not players, like vendors, cloning, etc.
// Also doesn't log if hideLog is true.
if (!HasComp<ActorComponent>(source) || hideLog)
return;
if (originalMessage == message)
{
if (name != Name(source))
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {source} as {name}: {originalMessage}.");
else
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Say from {source}: {originalMessage}.");
}
else
{
if (name != Name(source))
_adminLogger.Add(LogType.Chat, LogImpact.Low,
$"Say from {source} as {name}, original: {originalMessage}, transformed: {message}.");
else
_adminLogger.Add(LogType.Chat, LogImpact.Low,
$"Say from {source}, original: {originalMessage}, transformed: {message}.");
}
}
private void SendEntityWhisper(
EntityUid source,
string originalMessage,
ChatTransmitRange range,
RadioChannelPrototype? channel,
string? nameOverride,
bool hideLog = false,
bool ignoreActionBlocker = false
)
{
if (!_actionBlocker.CanSpeak(source) && !ignoreActionBlocker)
return;
var message = TransformSpeech(source, FormattedMessage.RemoveMarkupOrThrow(originalMessage));
if (message.Length == 0)
return;
var obfuscatedMessage = ObfuscateMessageReadability(message, 0.2f);
// get the entity's name by visual identity (if no override provided).
string nameIdentity = FormattedMessage.EscapeText(nameOverride ?? Identity.Name(source, EntityManager));
// get the entity's name by voice (if no override provided).
string name;
if (nameOverride != null)
{
name = nameOverride;
}
else
{
var nameEv = new TransformSpeakerNameEvent(source, Name(source));
RaiseLocalEvent(source, nameEv);
name = nameEv.VoiceName;
}
name = FormattedMessage.EscapeText(name);
var wrappedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message",
("entityName", name), ("message", FormattedMessage.EscapeText(message)));
var wrappedobfuscatedMessage = Loc.GetString("chat-manager-entity-whisper-wrap-message",
("entityName", nameIdentity), ("message", FormattedMessage.EscapeText(obfuscatedMessage)));
var wrappedUnknownMessage = Loc.GetString("chat-manager-entity-whisper-unknown-wrap-message",
("message", FormattedMessage.EscapeText(obfuscatedMessage)));
foreach (var (session, data) in GetRecipients(source, WhisperMuffledRange))
{
EntityUid listener;
if (session.AttachedEntity is not { Valid: true } playerEntity)
continue;
listener = session.AttachedEntity.Value;
if (MessageRangeCheck(session, data, range) != MessageRangeCheckResult.Full)
continue; // Won't get logged to chat, and ghosts are too far away to see the pop-up, so we just won't send it to them.
if (data.Range <= WhisperClearRange || data.Observer)
_chatManager.ChatMessageToOne(ChatChannel.Whisper, message, wrappedMessage, source, false, session.Channel);
//If listener is too far, they only hear fragments of the message
else if (_examineSystem.InRangeUnOccluded(source, listener, WhisperMuffledRange))
_chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedobfuscatedMessage, source, false, session.Channel);
//If listener is too far and has no line of sight, they can't identify the whisperer's identity
else
_chatManager.ChatMessageToOne(ChatChannel.Whisper, obfuscatedMessage, wrappedUnknownMessage, source, false, session.Channel);
}
_replay.RecordServerMessage(new ChatMessage(ChatChannel.Whisper, message, wrappedMessage, GetNetEntity(source), null, MessageRangeHideChatForReplay(range)));
var ev = new EntitySpokeEvent(source, message, originalMessage, channel, obfuscatedMessage);
RaiseLocalEvent(source, ev, true);
if (!hideLog)
if (originalMessage == message)
{
if (name != Name(source))
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {source} as {name}: {originalMessage}.");
else
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Whisper from {source}: {originalMessage}.");
}
else
{
if (name != Name(source))
_adminLogger.Add(LogType.Chat, LogImpact.Low,
$"Whisper from {source} as {name}, original: {originalMessage}, transformed: {message}.");
else
_adminLogger.Add(LogType.Chat, LogImpact.Low,
$"Whisper from {source}, original: {originalMessage}, transformed: {message}.");
}
}
protected override void SendEntityEmote(
EntityUid source,
string action,
ChatTransmitRange range,
string? nameOverride,
bool hideLog = false,
bool checkEmote = true,
bool ignoreActionBlocker = false,
NetUserId? author = null
)
{
if (!_actionBlocker.CanEmote(source) && !ignoreActionBlocker)
return;
// get the entity's apparent name (if no override provided).
var ent = Identity.Entity(source, EntityManager);
string name = FormattedMessage.EscapeText(nameOverride ?? Name(ent));
// Emotes use Identity.Name, since it doesn't actually involve your voice at all.
var wrappedMessage = Loc.GetString("chat-manager-entity-me-wrap-message",
("entityName", name),
("entity", ent),
("message", FormattedMessage.RemoveMarkupOrThrow(action)));
if (checkEmote &&
!TryEmoteChatInput(source, action))
return;
SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, range, author);
if (!hideLog)
if (name != Name(source))
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source} as {name}: {action}");
else
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Emote from {source}: {action}");
}
// ReSharper disable once InconsistentNaming
private void SendLOOC(EntityUid source, ICommonSession player, string message, bool hideChat)
{
var name = FormattedMessage.EscapeText(Identity.Name(source, EntityManager));
if (_adminManager.IsAdmin(player))
{
if (!_adminLoocEnabled) return;
}
else if (!_loocEnabled) return;
// If crit player LOOC is disabled, don't send the message at all.
if (!_critLoocEnabled && _mobStateSystem.IsCritical(source))
return;
var wrappedMessage = Loc.GetString("chat-manager-entity-looc-wrap-message",
("entityName", name),
("message", FormattedMessage.EscapeText(message)));
SendInVoiceRange(ChatChannel.LOOC, message, wrappedMessage, source, hideChat ? ChatTransmitRange.HideChat : ChatTransmitRange.Normal, player.UserId);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"LOOC from {source}: {message}");
}
private void SendDeadChat(EntityUid source, ICommonSession player, string message, bool hideChat)
{
var clients = GetDeadChatClients();
var playerName = Name(source);
string wrappedMessage;
if (_adminManager.IsAdmin(player))
{
wrappedMessage = Loc.GetString("chat-manager-send-admin-dead-chat-wrap-message",
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")),
("userName", player.Channel.UserName),
("message", FormattedMessage.EscapeText(message)));
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Admin dead chat from {source}: {message}");
}
else
{
wrappedMessage = Loc.GetString("chat-manager-send-dead-chat-wrap-message",
("deadChannelName", Loc.GetString("chat-manager-dead-channel-name")),
("playerName", (playerName)),
("message", FormattedMessage.EscapeText(message)));
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Dead chat from {source}: {message}");
}
_chatManager.ChatMessageToMany(ChatChannel.Dead, message, wrappedMessage, source, hideChat, true, clients.ToList(), author: player.UserId);
}
#endregion
#region Utility
private enum MessageRangeCheckResult
{
Disallowed,
HideChat,
Full
}
/// <summary>
/// If hideChat should be set as far as replays are concerned.
/// </summary>
private bool MessageRangeHideChatForReplay(ChatTransmitRange range)
{
return range == ChatTransmitRange.HideChat;
}
/// <summary>
/// Checks if a target as returned from GetRecipients should receive the message.
/// Keep in mind data.Range is -1 for out of range observers.
/// </summary>
private MessageRangeCheckResult MessageRangeCheck(ICommonSession session, ICChatRecipientData data, ChatTransmitRange range)
{
var initialResult = MessageRangeCheckResult.Full;
switch (range)
{
case ChatTransmitRange.Normal:
initialResult = MessageRangeCheckResult.Full;
break;
case ChatTransmitRange.GhostRangeLimit:
initialResult = (data.Observer && data.Range < 0 && !_adminManager.IsAdmin(session)) ? MessageRangeCheckResult.HideChat : MessageRangeCheckResult.Full;
break;
case ChatTransmitRange.HideChat:
initialResult = MessageRangeCheckResult.HideChat;
break;
case ChatTransmitRange.NoGhosts:
initialResult = (data.Observer && !_adminManager.IsAdmin(session)) ? MessageRangeCheckResult.Disallowed : MessageRangeCheckResult.Full;
break;
}
var insistHideChat = data.HideChatOverride ?? false;
var insistNoHideChat = !(data.HideChatOverride ?? true);
if (insistHideChat && initialResult == MessageRangeCheckResult.Full)
return MessageRangeCheckResult.HideChat;
if (insistNoHideChat && initialResult == MessageRangeCheckResult.HideChat)
return MessageRangeCheckResult.Full;
return initialResult;
}
/// <summary>
/// Sends a chat message to the given players in range of the source entity.
/// </summary>
private void SendInVoiceRange(ChatChannel channel, string message, string wrappedMessage, EntityUid source, ChatTransmitRange range, NetUserId? author = null)
{
foreach (var (session, data) in GetRecipients(source, VoiceRange))
{
var entRange = MessageRangeCheck(session, data, range);
if (entRange == MessageRangeCheckResult.Disallowed)
continue;
var entHideChat = entRange == MessageRangeCheckResult.HideChat;
_chatManager.ChatMessageToOne(channel, message, wrappedMessage, source, entHideChat, session.Channel, author: author);
}
_replay.RecordServerMessage(new ChatMessage(channel, message, wrappedMessage, GetNetEntity(source), null, MessageRangeHideChatForReplay(range)));
}
/// <summary>
/// Returns true if the given player is 'allowed' to send the given message, false otherwise.
/// </summary>
private bool CanSendInGame(string message, IConsoleShell? shell = null, ICommonSession? player = null)
{
// Non-players don't have to worry about these restrictions.
if (player == null)
return true;
var mindContainerComponent = player.ContentData()?.Mind;
if (mindContainerComponent == null)
{
shell?.WriteError("You don't have a mind!");
return false;
}
if (player.AttachedEntity is not { Valid: true } _)
{
shell?.WriteError("You don't have an entity!");
return false;
}
return !_chatManager.MessageCharacterLimit(player, message);
}
// ReSharper disable once InconsistentNaming
private string SanitizeInGameICMessage(EntityUid source, string message, out string? emoteStr, bool capitalize = true, bool punctuate = false, bool capitalizeTheWordI = true)
{
var newMessage = SanitizeMessageReplaceWords(message.Trim());
GetRadioKeycodePrefix(source, newMessage, out newMessage, out var prefix);
// Sanitize it first as it might change the word order
_sanitizer.TrySanitizeEmoteShorthands(newMessage, source, out newMessage, out emoteStr);
if (capitalize)
newMessage = SanitizeMessageCapital(newMessage);
if (capitalizeTheWordI)
newMessage = SanitizeMessageCapitalizeTheWordI(newMessage, "i");
if (punctuate)
newMessage = SanitizeMessagePeriod(newMessage);
return prefix + newMessage;
}
private string SanitizeInGameOOCMessage(string message)
{
var newMessage = message.Trim();
newMessage = FormattedMessage.EscapeText(newMessage);
return newMessage;
}
public string TransformSpeech(EntityUid sender, string message)
{
var ev = new TransformSpeechEvent(sender, message);
RaiseLocalEvent(sender, ev, true);
return ev.Message;
}
public bool CheckIgnoreSpeechBlocker(EntityUid sender, bool ignoreBlocker)
{
if (ignoreBlocker)
return ignoreBlocker;
var ev = new CheckIgnoreSpeechBlockerEvent(sender, ignoreBlocker);
RaiseLocalEvent(sender, ev, true);
return ev.IgnoreBlocker;
}
private IEnumerable<INetChannel> GetDeadChatClients()
{
return Filter.Empty()
.AddWhereAttachedEntity(HasComp<GhostComponent>)
.Recipients
.Union(_adminManager.ActiveAdmins)
.Select(p => p.Channel);
}
private string SanitizeMessagePeriod(string message)
{
if (string.IsNullOrEmpty(message))
return message;
// Adds a period if the last character is a letter.
if (char.IsLetter(message[^1]))
message += ".";
return message;
}
public static readonly ProtoId<ReplacementAccentPrototype> ChatSanitize_Accent = "chatsanitize";
public string SanitizeMessageReplaceWords(string message)
{
if (string.IsNullOrEmpty(message)) return message;
var msg = message;
msg = _wordreplacement.ApplyReplacements(msg, ChatSanitize_Accent);
return msg;
}
/// <summary>
/// Returns list of players and ranges for all players withing some range. Also returns observers with a range of -1.
/// </summary>
private Dictionary<ICommonSession, ICChatRecipientData> GetRecipients(EntityUid source, float voiceGetRange)
{
// TODO proper speech occlusion
var recipients = new Dictionary<ICommonSession, ICChatRecipientData>();
var transformSource = Transform(source);
var sourceMapId = transformSource.MapID;
var sourceCoords = transformSource.Coordinates;
foreach (var player in _playerManager.Sessions)
{
if (player.AttachedEntity is not { Valid: true } playerEntity)
continue;
var transformEntity = Transform(playerEntity);
if (transformEntity.MapID != sourceMapId)
continue;
var observer = _ghostHearingQuery.HasComponent(playerEntity);
// even if they are a ghost hearer, in some situations we still need the range
if (sourceCoords.TryDistance(EntityManager, transformEntity.Coordinates, out var distance) && distance < voiceGetRange)
{
recipients.Add(player, new ICChatRecipientData(distance, observer));
continue;
}
if (observer)
recipients.Add(player, new ICChatRecipientData(-1, true));
}
RaiseLocalEvent(new ExpandICChatRecipientsEvent(source, voiceGetRange, recipients));
return recipients;
}
public readonly record struct ICChatRecipientData(float Range, bool Observer, bool? HideChatOverride = null)
{
}
private string ObfuscateMessageReadability(string message, float chance)
{
var modifiedMessage = new StringBuilder(message);
for (var i = 0; i < message.Length; i++)
{
if (char.IsWhiteSpace((modifiedMessage[i])))
{
continue;
}
if (_random.Prob(1 - chance))
{
modifiedMessage[i] = '~';
}
}
return modifiedMessage.ToString();
}
public string BuildGibberishString(IReadOnlyList<char> charOptions, int length)
{
var sb = new StringBuilder();
for (var i = 0; i < length; i++)
{
sb.Append(_random.Pick(charOptions));
}
return sb.ToString();
}
#endregion
}
/// <summary>
@@ -1,25 +1,31 @@
using Content.Server.Destructible.Thresholds;
namespace Content.Server.Destructible
namespace Content.Server.Destructible;
/// <summary>
/// When attached to an <see cref="Robust.Shared.GameObjects.EntityUid"/>, allows it to take damage
/// and triggers thresholds when reached.
/// </summary>
[RegisterComponent]
public sealed partial class DestructibleComponent : Component
{
/// <summary>
/// When attached to an <see cref="Robust.Shared.GameObjects.EntityUid"/>, allows it to take damage
/// and triggers thresholds when reached.
/// A list of damage thresholds for the entity;
/// includes their triggers and resultant behaviors.
/// </summary>
[RegisterComponent]
public sealed partial class DestructibleComponent : Component
{
/// <summary>
/// A list of damage thresholds for the entity;
/// includes their triggers and resultant behaviors
/// </summary>
[DataField]
public List<DamageThreshold> Thresholds = new();
[DataField]
public List<DamageThreshold> Thresholds = new();
/// <summary>
/// Specifies whether the entity has passed a damage threshold that causes it to break
/// </summary>
[DataField]
public bool IsBroken = false;
}
/// <summary>
/// Specifies whether the entity has passed a damage threshold that causes it to break.
/// </summary>
[DataField]
public bool IsBroken = false;
/// <summary>
/// Specifies if the entity should be silently destroyed when receiving damage significantly in excess of
/// its normal destructible threshold.
/// </summary>
[DataField(readOnly: true)]
public bool GenerateOverkillThreshold = true;
}
+258 -168
View File
@@ -9,7 +9,6 @@ using Content.Server.Explosion.EntitySystems;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Stack;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Damage;
using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.Destructible;
@@ -19,210 +18,301 @@ using Content.Shared.Gibbing;
using Content.Shared.Humanoid;
using Content.Shared.Trigger.Systems;
using JetBrains.Annotations;
using Robust.Server.Audio;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Destructible
namespace Content.Server.Destructible;
[UsedImplicitly]
public sealed partial class DestructibleSystem : SharedDestructibleSystem
{
[UsedImplicitly]
public sealed partial class DestructibleSystem : SharedDestructibleSystem
[Dependency] public IAdminLogManager AdminLogger = default!;
[Dependency] public IPrototypeManager PrototypeManager = default!;
[Dependency] public IRobustRandom Random = default!;
public new IEntityManager EntityManager => base.EntityManager;
[Dependency] public AtmosphereSystem AtmosphereSystem = default!;
[Dependency] public ConstructionSystem ConstructionSystem = default!;
[Dependency] public ExplosionSystem ExplosionSystem = default!;
[Dependency] public GibbingSystem Gibbing = default!;
[Dependency] public PuddleSystem PuddleSystem = default!;
[Dependency] public SharedContainerSystem ContainerSystem = default!;
[Dependency] public SharedSolutionContainerSystem SolutionContainerSystem = default!;
[Dependency] public StackSystem StackSystem = default!;
[Dependency] public TriggerSystem TriggerSystem = default!;
/// <summary>
/// Minimum damage to invoke overkill behavior.
/// </summary>
private const int MinimumOverkill = 100;
/// <summary>
/// Multiplier over normal damage to invoke overkill.
/// </summary>
private const double OverkillMultiplier = 2.0;
public override void Initialize()
{
[Dependency] public IRobustRandom Random = default!;
public new IEntityManager EntityManager => base.EntityManager;
base.Initialize();
SubscribeLocalEvent<DestructibleComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<DestructibleComponent, DamageChangedEvent>(OnDamageChanged);
}
[Dependency] public AtmosphereSystem AtmosphereSystem = default!;
[Dependency] public AudioSystem AudioSystem = default!;
[Dependency] public GibbingSystem Gibbing = default!;
[Dependency] public ConstructionSystem ConstructionSystem = default!;
[Dependency] public ExplosionSystem ExplosionSystem = default!;
[Dependency] public StackSystem StackSystem = default!;
[Dependency] public TriggerSystem TriggerSystem = default!;
[Dependency] public SharedSolutionContainerSystem SolutionContainerSystem = default!;
[Dependency] public PuddleSystem PuddleSystem = default!;
[Dependency] public SharedContainerSystem ContainerSystem = default!;
[Dependency] public IPrototypeManager PrototypeManager = default!;
[Dependency] public IAdminLogManager AdminLogger = default!;
/// <summary>
/// Map Initialization function for <see cref="DestructibleComponent"/>, adding automatic overkill threshold.
/// </summary>
/// <param name="entity">The uid, component tuple.</param>
/// <param name="args">The event arguments.</param>
private void OnMapInit(Entity<DestructibleComponent> entity, ref MapInitEvent args)
{
AddOverkillThreshold(entity);
}
public override void Initialize()
/// <summary>
/// Check if any thresholds were reached. if they were, execute them.
/// </summary>
private void OnDamageChanged(Entity<DestructibleComponent> entity, ref DamageChangedEvent args)
{
var (uid, comp) = entity;
comp.IsBroken = false;
foreach (var threshold in comp.Thresholds)
{
base.Initialize();
SubscribeLocalEvent<DestructibleComponent, DamageChangedEvent>(OnDamageChanged);
}
/// <summary>
/// Check if any thresholds were reached. if they were, execute them.
/// </summary>
private void OnDamageChanged(EntityUid uid, DestructibleComponent component, DamageChangedEvent args)
{
component.IsBroken = false;
foreach (var threshold in component.Thresholds)
if (Triggered(threshold, (uid, args.Damageable)))
{
if (Triggered(threshold, (uid, args.Damageable)))
RaiseLocalEvent(uid, new DamageThresholdReached(comp, threshold), true);
var logImpact = LogImpact.Low;
// Convert behaviors into string for logs
var triggeredBehaviors = string.Join(", ", threshold.Behaviors.Select(behavior =>
{
RaiseLocalEvent(uid, new DamageThresholdReached(component, threshold), true);
var logImpact = LogImpact.Low;
// Convert behaviors into string for logs
var triggeredBehaviors = string.Join(", ", threshold.Behaviors.Select(b =>
if (logImpact <= behavior.Impact)
logImpact = behavior.Impact;
if (behavior is DoActsBehavior doActsBehavior)
{
if (logImpact <= b.Impact)
logImpact = b.Impact;
if (b is DoActsBehavior doActsBehavior)
{
return $"{b.GetType().Name}:{doActsBehavior.Acts.ToString()}";
}
return b.GetType().Name;
}));
// If it doesn't have a humanoid component, it's probably not particularly notable?
if (logImpact > LogImpact.Medium && !HasComp<HumanoidProfileComponent>(uid))
logImpact = LogImpact.Medium;
if (args.Origin != null)
{
AdminLogger.Add(LogType.Damaged,
logImpact,
$"{ToPrettyString(args.Origin.Value):actor} caused {ToPrettyString(uid):subject} to trigger [{triggeredBehaviors}]");
}
else
{
AdminLogger.Add(LogType.Damaged,
logImpact,
$"Unknown damage source caused {ToPrettyString(uid):subject} to trigger [{triggeredBehaviors}]");
return $"{behavior.GetType().Name}:{doActsBehavior.Acts.ToString()}";
}
return behavior.GetType().Name;
}));
Execute(threshold, uid, args.Origin);
// If it doesn't have a humanoid component, it's probably not particularly notable?
if (logImpact > LogImpact.Medium && !HasComp<HumanoidProfileComponent>(uid))
logImpact = LogImpact.Medium;
if (args.Origin != null)
{
AdminLogger.Add(LogType.Damaged,
logImpact,
$"{ToPrettyString(args.Origin.Value):actor} caused {ToPrettyString(uid):subject} to trigger [{triggeredBehaviors}]");
}
else
{
AdminLogger.Add(LogType.Damaged,
logImpact,
$"Unknown damage source caused {ToPrettyString(uid):subject} to trigger [{triggeredBehaviors}]");
}
if (threshold.OldTriggered)
{
component.IsBroken |= threshold.Behaviors.Any(b => b is DoActsBehavior doActsBehavior &&
(doActsBehavior.HasAct(ThresholdActs.Breakage) || doActsBehavior.HasAct(ThresholdActs.Destruction)));
}
// if destruction behavior (or some other deletion effect) occurred, don't run other triggers.
if (EntityManager.IsQueuedForDeletion(uid) || Deleted(uid))
return;
Execute(threshold, uid, args.Origin);
}
}
/// <summary>
/// Check if the given threshold should trigger.
/// </summary>
public bool Triggered(DamageThreshold threshold, Entity<Shared.Damage.Components.DamageableComponent> owner)
{
if (threshold.Trigger == null)
return false;
if (threshold.Triggered && threshold.TriggersOnce)
return false;
if (threshold.OldTriggered)
{
threshold.OldTriggered = threshold.Trigger.Reached(owner, this);
return false;
comp.IsBroken |= threshold.Behaviors.Any(b => b is DoActsBehavior doActsBehavior &&
(doActsBehavior.HasAct(ThresholdActs.Breakage) || doActsBehavior.HasAct(ThresholdActs.Destruction)));
}
if (!threshold.Trigger.Reached(owner, this))
return false;
// if destruction behavior (or some other deletion effect) occurred, don't run other triggers.
if (EntityManager.IsQueuedForDeletion(uid) || Deleted(uid))
return;
}
}
threshold.OldTriggered = true;
return true;
/// <summary>
/// Check if the given threshold should trigger.
/// </summary>
public bool Triggered(DamageThreshold threshold, Entity<Shared.Damage.Components.DamageableComponent> owner)
{
if (threshold.Trigger == null)
return false;
if (threshold.Triggered && threshold.TriggersOnce)
return false;
if (threshold.OldTriggered)
{
threshold.OldTriggered = threshold.Trigger.Reached(owner, this);
return false;
}
/// <summary>
/// Check if the conditions for the given threshold are currently true.
/// </summary>
public bool Reached(DamageThreshold threshold, Entity<Shared.Damage.Components.DamageableComponent> owner)
{
if (threshold.Trigger == null)
return false;
if (!threshold.Trigger.Reached(owner, this))
return false;
return threshold.Trigger.Reached(owner, this);
threshold.OldTriggered = true;
return true;
}
/// <summary>
/// Check if the conditions for the given threshold are currently true.
/// </summary>
public bool Reached(DamageThreshold threshold, Entity<Shared.Damage.Components.DamageableComponent> owner)
{
if (threshold.Trigger == null)
return false;
return threshold.Trigger.Reached(owner, this);
}
/// <summary>
/// Triggers this threshold.
/// </summary>
/// <param name="threshold">The threshold to execute.</param>
/// <param name="owner">The entity that owns this threshold.</param>
/// <param name="cause">The entity that caused this threshold to trigger.</param>
public void Execute(DamageThreshold threshold, EntityUid owner, EntityUid? cause = null)
{
threshold.Triggered = true;
foreach (var behavior in threshold.Behaviors)
{
// The owner has been deleted. We stop execution of behaviors here.
if (!Exists(owner))
return;
// TODO: Replace with EntityEffects.
behavior.Execute(owner, this, cause);
}
}
/// <summary>
/// Triggers this threshold.
/// </summary>
/// <param name="owner">The entity that owns this threshold.</param>
/// <param name="cause">The entity that caused this threshold to trigger.</param>
public void Execute(DamageThreshold threshold, EntityUid owner, EntityUid? cause = null)
/// <summary>
/// Adds a threshold to the threshold list. If the entity does not have a destructible component, one will be added.
/// </summary>
/// <param name="entity">The entity, component tuple to target.</param>
/// <param name="threshold">The threshold to add.</param>
/// <param name="index">The index at which to insert the threshold.</param>
public void AddThreshold(Entity<DestructibleComponent?> entity, DamageThreshold threshold, Index? index)
{
if (!Resolve(entity.Owner, ref entity.Comp, false))
entity.Comp = AddComp<DestructibleComponent>(entity.Owner);
if (index is not null)
{
threshold.Triggered = true;
var threshIndex = index.Value.GetOffset(entity.Comp.Thresholds.Count);
entity.Comp.Thresholds.Insert(threshIndex, threshold);
}
else
{
entity.Comp.Thresholds.Add(threshold);
}
}
/// <summary>
/// Adds an overkill threshold if one does not exist.
/// </summary>
/// <remarks>
/// An overkill threshold is a top priority threshold that will destroy the entity without triggering any other
/// behaviors applied to the entity.
/// </remarks>
/// <param name="entity">The entity, component tuple to target.</param>
private void AddOverkillThreshold(Entity<DestructibleComponent> entity)
{
if (!entity.Comp.GenerateOverkillThreshold)
return;
var maxTrigger = FixedPoint2.Zero;
foreach (var threshold in entity.Comp.Thresholds)
{
if (threshold.Trigger is not DamageTrigger trigger)
continue;
foreach (var behavior in threshold.Behaviors)
{
// The owner has been deleted. We stop execution of behaviors here.
if (!Exists(owner))
return;
// TODO: Replace with EntityEffects.
behavior.Execute(owner, this, cause);
}
}
public bool TryGetDestroyedAt(Entity<DestructibleComponent?> ent, [NotNullWhen(true)] out FixedPoint2? destroyedAt)
{
destroyedAt = null;
if (!Resolve(ent, ref ent.Comp, false))
return false;
destroyedAt = DestroyedAt(ent, ent.Comp);
return true;
}
// FFS this shouldn't be this hard. Maybe this should just be a field of the destructible component. Its not
// like there is currently any entity that is NOT just destroyed upon reaching a total-damage value.
/// <summary>
/// Figure out how much damage an entity needs to have in order to be destroyed.
/// </summary>
/// <remarks>
/// This assumes that this entity has some sort of destruction or breakage behavior triggered by a
/// total-damage threshold.
/// </remarks>
public FixedPoint2 DestroyedAt(EntityUid uid, DestructibleComponent? destructible = null)
{
if (!Resolve(uid, ref destructible, logMissing: false))
return FixedPoint2.MaxValue;
// We have nested for loops here, but the vast majority of components only have one threshold with 1-3 behaviors.
// Really, this should probably just be a property of the damageable component.
var damageNeeded = FixedPoint2.MaxValue;
foreach (var threshold in destructible.Thresholds)
{
if (threshold.Trigger is not DamageTrigger trigger)
// Not a destruction behavior
if (behavior is not DoActsBehavior actBehavior || !actBehavior.HasAct(ThresholdActs.Destruction))
continue;
foreach (var behavior in threshold.Behaviors)
// Already has a pure destruction behavior
if (threshold.Behaviors.Count == 1)
return;
maxTrigger = FixedPoint2.Max(maxTrigger, trigger.Damage);
}
}
// No destruction behavior
if (FixedPoint2.Zero == maxTrigger)
return;
var autoThreshold = new DamageThreshold
{
Trigger = new DamageTrigger { Damage = FixedPoint2.Max(MinimumOverkill, OverkillMultiplier * maxTrigger) },
Behaviors = { new DoActsBehavior { Acts = ThresholdActs.Destruction } },
};
// Thresholds are evaluated in order, so overkill must be first to avoid triggering effects
AddThreshold(entity.AsNullable(), autoThreshold, 0);
}
public bool TryGetDestroyedAt(Entity<DestructibleComponent?> ent, [NotNullWhen(true)] out FixedPoint2? destroyedAt)
{
destroyedAt = null;
if (!Resolve(ent, ref ent.Comp, false))
return false;
destroyedAt = DestroyedAt(ent, ent.Comp);
return true;
}
// FFS this shouldn't be this hard. Maybe this should just be a field of the destructible component. Its not
// like there is currently any entity that is NOT just destroyed upon reaching a total-damage value.
/// <summary>
/// Figure out how much damage an entity needs to have in order to be destroyed.
/// </summary>
/// <remarks>
/// This assumes that this entity has some sort of destruction or breakage behavior triggered by a
/// total-damage threshold.
/// </remarks>
public FixedPoint2 DestroyedAt(EntityUid uid, DestructibleComponent? destructible = null)
{
if (!Resolve(uid, ref destructible, logMissing: false))
return FixedPoint2.MaxValue;
// We have nested for loops here, but the vast majority of components only have one threshold with 1-3 behaviors.
// Really, this should probably just be a property of the damageable component.
var damageNeeded = FixedPoint2.MaxValue;
foreach (var threshold in destructible.Thresholds)
{
if (threshold.Trigger is not DamageTrigger trigger)
continue;
foreach (var behavior in threshold.Behaviors)
{
if (behavior is DoActsBehavior actBehavior &&
actBehavior.HasAct(ThresholdActs.Destruction | ThresholdActs.Breakage))
{
if (behavior is DoActsBehavior actBehavior &&
actBehavior.HasAct(ThresholdActs.Destruction | ThresholdActs.Breakage))
{
damageNeeded = FixedPoint2.Min(damageNeeded, trigger.Damage);
}
damageNeeded = FixedPoint2.Min(damageNeeded, trigger.Damage);
}
}
return damageNeeded;
}
}
// Currently only used for destructible integration tests. Unless other uses are found for this, maybe this should just be removed and the tests redone.
/// <summary>
/// Event raised when a <see cref="DamageThreshold"/> is reached.
/// </summary>
public sealed class DamageThresholdReached : EntityEventArgs
{
public readonly DestructibleComponent Parent;
public readonly DamageThreshold Threshold;
public DamageThresholdReached(DestructibleComponent parent, DamageThreshold threshold)
{
Parent = parent;
Threshold = threshold;
}
return damageNeeded;
}
}
// Currently only used for destructible integration tests. Unless other uses are found for this, maybe this should just be removed and the tests redone.
/// <summary>
/// Event raised when a <see cref="DamageThreshold"/> is reached.
/// </summary>
public sealed class DamageThresholdReached : EntityEventArgs
{
public readonly DestructibleComponent Parent;
public readonly DamageThreshold Threshold;
public DamageThresholdReached(DestructibleComponent parent, DamageThreshold threshold)
{
Parent = parent;
Threshold = threshold;
}
}
@@ -0,0 +1,179 @@
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Disposal.Components;
using Content.Shared.Disposal.Holder;
using Content.Shared.Disposal.Tube;
using Content.Shared.Disposal.Unit;
using Content.Shared.Maps;
using Content.Shared.Stunnable;
using Content.Shared.Throwing;
using Robust.Shared.Containers;
using Robust.Shared.Map.Components;
using Robust.Shared.Network;
using Robust.Shared.Random;
namespace Content.Server.Disposal.Holder;
/// <inheritdoc/>
public sealed partial class DisposalHolderSystem : SharedDisposalHolderSystem
{
[Dependency] private AtmosphereSystem _atmos = default!;
[Dependency] private SharedTransformSystem _xform = default!;
[Dependency] private IRobustRandom _random = default!;
[Dependency] private ThrowingSystem _throwing = default!;
[Dependency] private SharedDisposalUnitSystem _disposalUnit = default!;
[Dependency] private SharedContainerSystem _container = default!;
[Dependency] private SharedMapSystem _maps = default!;
[Dependency] private INetManager _net = default!;
[Dependency] private SharedStunSystem _stun = default!;
[Dependency] private TileSystem _tile = default!;
private EntityQuery<DisposalUnitComponent> _disposalUnitQuery;
private EntityQuery<MetaDataComponent> _metaQuery;
private EntityQuery<TransformComponent> _xformQuery;
public override void Initialize()
{
base.Initialize();
_disposalUnitQuery = GetEntityQuery<DisposalUnitComponent>();
_metaQuery = GetEntityQuery<MetaDataComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
}
/// <inheritdoc/>
public override void TransferAtmos(Entity<DisposalHolderComponent> ent, Entity<DisposalUnitComponent> unit)
{
_atmos.Merge(ent.Comp.Air, unit.Comp.Air);
unit.Comp.Air.Clear();
}
/// <inheritdoc/>
protected override void ExpelAtmos(Entity<DisposalHolderComponent> ent)
{
if (_atmos.GetContainingMixture(ent.Owner, false, true) is { } environment)
{
_atmos.Merge(environment, ent.Comp.Air);
ent.Comp.Air.Clear();
}
}
/// <inheritdoc/>
public override void Exit(Entity<DisposalHolderComponent> ent)
{
if (Terminating(ent))
return;
if (ent.Comp.IsExiting)
return;
ent.Comp.IsExiting = true;
Dirty(ent);
// Get the holder and grid transforms
var xform = _xformQuery.GetComponent(ent);
var gridUid = xform.GridUid;
_xformQuery.TryGetComponent(gridUid, out var gridXform);
// Determine the exit angle of the ejected entities
var exitDirection = ent.Comp.CurrentDirection;
Angle? exitAngle = exitDirection != Direction.Invalid ? exitDirection.ToAngle() : null;
// Check for a disposal unit to throw them into and then eject them from it.
// *This ejection also makes the target not collide with the unit.*
// *This is on purpose.*
Entity<DisposalUnitComponent>? unit = null;
if (TryComp<MapGridComponent>(gridUid, out var grid))
{
foreach (var contentUid in _maps.GetLocal(gridUid.Value, grid, xform.Coordinates))
{
if (_disposalUnitQuery.TryGetComponent(contentUid, out var disposalUnit))
{
unit = new(contentUid, disposalUnit);
break;
}
}
// If no disposal unit was found, this exit will be a little messy
if (unit == null && _net.IsServer)
{
// Pry up the tile that the pipe was under
var tileRef = _maps.GetTileRef((gridUid.Value, grid), xform.Coordinates);
_tile.PryTile(tileRef);
// Also pry up the tile infront of the pipe
if (exitAngle != null)
{
tileRef = _maps.GetTileRef((gridUid.Value, grid), xform.Coordinates.Offset(exitAngle.Value.ToWorldVec()));
_tile.PryTile(tileRef);
}
}
}
// Update the exit angle here to account for the grid's rotation
if (exitAngle != null && gridXform != null)
{
exitAngle += _xform.GetWorldRotation(gridXform);
}
// We're purposely iterating over all the holder's children
// because the holder might have something teleported into it,
// outside the usual container insertion logic.
var children = xform.ChildEnumerator;
while (children.MoveNext(out var held))
{
DetachEntity(held);
var heldMeta = _metaQuery.GetComponent(held);
var heldXform = _xformQuery.GetComponent(held);
// Insert the child into the found disposal unit, then pop them out
if (unit != null && unit.Value.Comp.Container != null && _container.Insert((held, heldXform, heldMeta), unit.Value.Comp.Container))
{
_disposalUnit.Remove(unit.Value, held);
}
else
{
// Otherwise remove the child from the holder and prepare to throw it
if (ent.Comp.Container != null && ent.Comp.Container.Contains(held))
{
_container.Remove((held, null, heldMeta), ent.Comp.Container, force: true);
}
_xform.AttachToGridOrMap(held, heldXform);
// Knockdown the entity
_stun.TryKnockdown(held, ent.Comp.ExitStunDuration, force: true);
// Throw the entity
if (exitAngle != null && heldXform.ParentUid.IsValid())
{
_throwing.TryThrow(held, exitAngle.Value.ToWorldVec() * ent.Comp.ExitDistanceMultiplier, ent.Comp.TraversalSpeed * ent.Comp.ExitSpeedMultiplier);
}
}
}
ExpelAtmos(ent);
Del(ent.Owner);
}
/// <inheritdoc/>
protected override bool TryEscaping(Entity<DisposalHolderComponent> ent, Entity<DisposalTubeComponent> tube)
{
// Check if the entity should have a chance to escape yet
if (ent.Comp.DirectionChangeCount < ent.Comp.DirectionChangeThreshold)
return false;
// Check if the holder escaped
if (_random.NextFloat() > ent.Comp.EscapeChance)
return false;
// Unanchor the tube and exit
var xform = Transform(tube);
_xform.Unanchor(tube, xform);
Exit(ent);
return true;
}
}
@@ -2,7 +2,4 @@ using Content.Shared.Disposal.Mailing;
namespace Content.Server.Disposal.Mailing;
public sealed class MailingUnitSystem : SharedMailingUnitSystem
{
}
public sealed class MailingUnitSystem : SharedMailingUnitSystem;
@@ -1,7 +0,0 @@
namespace Content.Server.Disposal.Tube;
[RegisterComponent]
[Access(typeof(DisposalTubeSystem))]
public sealed partial class DisposalBendComponent : Component
{
}
@@ -1,12 +0,0 @@
namespace Content.Server.Disposal.Tube;
[RegisterComponent]
[Access(typeof(DisposalTubeSystem))]
[Virtual]
public partial class DisposalJunctionComponent : Component
{
/// <summary>
/// The angles to connect to.
/// </summary>
[DataField("degrees")] public List<Angle> Degrees = new();
}
@@ -1,15 +0,0 @@
using Robust.Shared.Audio;
namespace Content.Server.Disposal.Tube
{
[RegisterComponent]
[Access(typeof(DisposalTubeSystem))]
public sealed partial class DisposalRouterComponent : DisposalJunctionComponent
{
[DataField("tags")]
public HashSet<string> Tags = new();
[DataField("clickSound")]
public SoundSpecifier ClickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
}
}
@@ -1,50 +0,0 @@
using Content.Server.DeviceLinking.Systems;
using Content.Shared.DeviceLinking.Events;
namespace Content.Server.Disposal.Tube;
/// <summary>
/// Handles signals and the routing get next direction event.
/// </summary>
public sealed partial class DisposalSignalRouterSystem : EntitySystem
{
[Dependency] private DeviceLinkSystem _deviceLink = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DisposalSignalRouterComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<DisposalSignalRouterComponent, SignalReceivedEvent>(OnSignalReceived);
SubscribeLocalEvent<DisposalSignalRouterComponent, GetDisposalsNextDirectionEvent>(OnGetNextDirection, after: new[] { typeof(DisposalTubeSystem) });
}
private void OnInit(EntityUid uid, DisposalSignalRouterComponent comp, ComponentInit args)
{
_deviceLink.EnsureSinkPorts(uid, comp.OnPort, comp.OffPort, comp.TogglePort);
}
private void OnSignalReceived(EntityUid uid, DisposalSignalRouterComponent comp, ref SignalReceivedEvent args)
{
// TogglePort flips it
// OnPort sets it to true
// OffPort sets it to false
comp.Routing = args.Port == comp.TogglePort
? !comp.Routing
: args.Port == comp.OnPort;
}
private void OnGetNextDirection(EntityUid uid, DisposalSignalRouterComponent comp, ref GetDisposalsNextDirectionEvent args)
{
if (!comp.Routing)
{
args.Next = Transform(uid).LocalRotation.GetDir();
return;
}
// use the junction side direction when a tag matches
var ev = new GetDisposalsConnectableDirectionsEvent();
RaiseLocalEvent(uid, ref ev);
args.Next = ev.Connectable[1];
}
}
@@ -1,14 +0,0 @@
using Content.Shared.DeviceLinking;
using Robust.Shared.Prototypes;
namespace Content.Server.Disposal.Tube;
/// <summary>
/// Disposal pipes with this component can be linked with devices to send a signal every time an item goes through the pipe
/// </summary>
[RegisterComponent, Access(typeof(DisposalSignallerSystem))]
public sealed partial class DisposalSignallerComponent : Component
{
[DataField]
public ProtoId<SourcePortPrototype> Port = "ItemDetected";
}
@@ -1,25 +0,0 @@
using Content.Server.DeviceLinking.Systems;
namespace Content.Server.Disposal.Tube;
public sealed partial class DisposalSignallerSystem : EntitySystem
{
[Dependency] private DeviceLinkSystem _link = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DisposalSignallerComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<DisposalSignallerComponent, GetDisposalsNextDirectionEvent>(OnGetNextDirection, after: new[] { typeof(DisposalTubeSystem) });
}
private void OnInit(EntityUid uid, DisposalSignallerComponent comp, ComponentInit args)
{
_link.EnsureSourcePorts(uid, comp.Port);
}
private void OnGetNextDirection(EntityUid uid, DisposalSignallerComponent comp, ref GetDisposalsNextDirectionEvent args)
{
_link.InvokePort(uid, comp.Port);
}
}
@@ -1,15 +0,0 @@
using Robust.Shared.Audio;
namespace Content.Server.Disposal.Tube
{
[RegisterComponent]
public sealed partial class DisposalTaggerComponent : DisposalTransitComponent
{
[ViewVariables(VVAccess.ReadWrite)]
[DataField("tag")]
public string Tag = "";
[DataField("clickSound")]
public SoundSpecifier ClickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
}
}
@@ -1,10 +0,0 @@
namespace Content.Server.Disposal.Tube
{
// TODO: Different types of tubes eject in random direction with no exit point
[RegisterComponent]
[Access(typeof(DisposalTubeSystem))]
[Virtual]
public partial class DisposalTransitComponent : Component
{
}
}
@@ -1,38 +0,0 @@
using Content.Server.Disposal.Unit;
using Content.Shared.Damage;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
namespace Content.Server.Disposal.Tube;
[RegisterComponent]
[Access(typeof(DisposalTubeSystem), typeof(DisposableSystem))]
public sealed partial class DisposalTubeComponent : Component
{
[DataField]
public string ContainerId = "DisposalTube";
[ViewVariables]
public bool Connected;
[DataField]
public SoundSpecifier ClangSound = new SoundPathSpecifier("/Audio/Effects/clang.ogg", AudioParams.Default.WithVolume(-5f));
/// <summary>
/// Container of entities that are currently inside this tube
/// </summary>
[ViewVariables]
public Container Contents = default!;
/// <summary>
/// Damage dealt to containing entities on every turn
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public DamageSpecifier DamageOnTurn = new()
{
DamageDict = new()
{
{ "Blunt", 0.0 },
}
};
}
@@ -1,443 +0,0 @@
using System.Linq;
using System.Text;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Construction.Completions;
using Content.Server.Disposal.Unit;
using Content.Server.Popups;
using Content.Shared.Destructible;
using Content.Shared.Disposal.Components;
using Content.Shared.Disposal.Tube;
using Content.Shared.Disposal.Unit;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Random;
namespace Content.Server.Disposal.Tube
{
public sealed partial class DisposalTubeSystem : SharedDisposalTubeSystem
{
[Dependency] private IRobustRandom _random = default!;
[Dependency] private SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private PopupSystem _popups = default!;
[Dependency] private UserInterfaceSystem _uiSystem = default!;
[Dependency] private SharedAudioSystem _audioSystem = default!;
[Dependency] private DisposableSystem _disposableSystem = default!;
[Dependency] private SharedContainerSystem _containerSystem = default!;
[Dependency] private AtmosphereSystem _atmosSystem = default!;
[Dependency] private TransformSystem _transform = default!;
[Dependency] private SharedMapSystem _map = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DisposalTubeComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<DisposalTubeComponent, ComponentRemove>(OnComponentRemove);
SubscribeLocalEvent<DisposalTubeComponent, AnchorStateChangedEvent>(OnAnchorChange);
SubscribeLocalEvent<DisposalTubeComponent, BreakageEventArgs>(OnBreak);
SubscribeLocalEvent<DisposalTubeComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<DisposalTubeComponent, ConstructionBeforeDeleteEvent>(OnDeconstruct);
SubscribeLocalEvent<DisposalBendComponent, GetDisposalsConnectableDirectionsEvent>(OnGetBendConnectableDirections);
SubscribeLocalEvent<DisposalBendComponent, GetDisposalsNextDirectionEvent>(OnGetBendNextDirection);
SubscribeLocalEvent<Shared.Disposal.Tube.DisposalEntryComponent, GetDisposalsConnectableDirectionsEvent>(OnGetEntryConnectableDirections);
SubscribeLocalEvent<Shared.Disposal.Tube.DisposalEntryComponent, GetDisposalsNextDirectionEvent>(OnGetEntryNextDirection);
SubscribeLocalEvent<DisposalJunctionComponent, GetDisposalsConnectableDirectionsEvent>(OnGetJunctionConnectableDirections);
SubscribeLocalEvent<DisposalJunctionComponent, GetDisposalsNextDirectionEvent>(OnGetJunctionNextDirection);
SubscribeLocalEvent<DisposalRouterComponent, GetDisposalsConnectableDirectionsEvent>(OnGetRouterConnectableDirections);
SubscribeLocalEvent<DisposalRouterComponent, GetDisposalsNextDirectionEvent>(OnGetRouterNextDirection);
SubscribeLocalEvent<DisposalTransitComponent, GetDisposalsConnectableDirectionsEvent>(OnGetTransitConnectableDirections);
SubscribeLocalEvent<DisposalTransitComponent, GetDisposalsNextDirectionEvent>(OnGetTransitNextDirection);
SubscribeLocalEvent<DisposalTaggerComponent, GetDisposalsConnectableDirectionsEvent>(OnGetTaggerConnectableDirections);
SubscribeLocalEvent<DisposalTaggerComponent, GetDisposalsNextDirectionEvent>(OnGetTaggerNextDirection);
Subs.BuiEvents<DisposalRouterComponent>(SharedDisposalRouterComponent.DisposalRouterUiKey.Key, subs =>
{
subs.Event<BoundUIOpenedEvent>(OnOpenRouterUI);
subs.Event<SharedDisposalRouterComponent.UiActionMessage>(OnUiAction);
});
Subs.BuiEvents<DisposalTaggerComponent>(SharedDisposalTaggerComponent.DisposalTaggerUiKey.Key, subs =>
{
subs.Event<BoundUIOpenedEvent>(OnOpenTaggerUI);
subs.Event<SharedDisposalTaggerComponent.UiActionMessage>(OnUiAction);
});
}
/// <summary>
/// Handles ui messages from the client. For things such as button presses
/// which interact with the world and require server action.
/// </summary>
/// <param name="msg">A user interface message from the client.</param>
private void OnUiAction(EntityUid uid, DisposalTaggerComponent tagger, SharedDisposalTaggerComponent.UiActionMessage msg)
{
if (TryComp<PhysicsComponent>(uid, out var physBody) && physBody.BodyType != BodyType.Static)
return;
//Check for correct message and ignore maleformed strings
if (msg.Action == SharedDisposalTaggerComponent.UiAction.Ok && SharedDisposalTaggerComponent.TagRegex.IsMatch(msg.Tag))
{
tagger.Tag = msg.Tag.Trim();
_audioSystem.PlayPvs(tagger.ClickSound, uid, AudioParams.Default.WithVolume(-2f));
}
}
/// <summary>
/// Handles ui messages from the client. For things such as button presses
/// which interact with the world and require server action.
/// </summary>
/// <param name="msg">A user interface message from the client.</param>
private void OnUiAction(EntityUid uid, DisposalRouterComponent router, SharedDisposalRouterComponent.UiActionMessage msg)
{
if (!Exists(msg.Actor))
return;
if (TryComp<PhysicsComponent>(uid, out var physBody) && physBody.BodyType != BodyType.Static)
return;
//Check for correct message and ignore maleformed strings
if (msg.Action == SharedDisposalRouterComponent.UiAction.Ok && SharedDisposalRouterComponent.TagRegex.IsMatch(msg.Tags))
{
router.Tags.Clear();
foreach (var tag in msg.Tags.Split(',', StringSplitOptions.RemoveEmptyEntries))
{
var trimmed = tag.Trim();
if (trimmed == "")
continue;
router.Tags.Add(trimmed);
}
_audioSystem.PlayPvs(router.ClickSound, uid, AudioParams.Default.WithVolume(-2f));
}
}
private void OnComponentInit(EntityUid uid, DisposalTubeComponent tube, ComponentInit args)
{
tube.Contents = _containerSystem.EnsureContainer<Container>(uid, tube.ContainerId);
}
private void OnComponentRemove(EntityUid uid, DisposalTubeComponent tube, ComponentRemove args)
{
DisconnectTube(uid, tube);
}
private void OnGetBendConnectableDirections(EntityUid uid, DisposalBendComponent component, ref GetDisposalsConnectableDirectionsEvent args)
{
var direction = Transform(uid).LocalRotation;
var side = new Angle(MathHelper.DegreesToRadians(direction.Degrees - 90));
args.Connectable = new[] { direction.GetDir(), side.GetDir() };
}
private void OnGetBendNextDirection(EntityUid uid, DisposalBendComponent component, ref GetDisposalsNextDirectionEvent args)
{
var ev = new GetDisposalsConnectableDirectionsEvent();
RaiseLocalEvent(uid, ref ev);
var previousDF = args.Holder.PreviousDirectionFrom;
if (previousDF == Direction.Invalid)
{
args.Next = ev.Connectable[0];
return;
}
args.Next = previousDF == ev.Connectable[0] ? ev.Connectable[1] : ev.Connectable[0];
}
private void OnGetEntryConnectableDirections(EntityUid uid, Shared.Disposal.Tube.DisposalEntryComponent component, ref GetDisposalsConnectableDirectionsEvent args)
{
args.Connectable = new[] { Transform(uid).LocalRotation.GetDir() };
}
private void OnGetEntryNextDirection(EntityUid uid, Shared.Disposal.Tube.DisposalEntryComponent component, ref GetDisposalsNextDirectionEvent args)
{
// Ejects contents when they come from the same direction the entry is facing.
if (args.Holder.PreviousDirectionFrom != Direction.Invalid)
{
args.Next = Direction.Invalid;
return;
}
var ev = new GetDisposalsConnectableDirectionsEvent();
RaiseLocalEvent(uid, ref ev);
args.Next = ev.Connectable[0];
}
private void OnGetJunctionConnectableDirections(EntityUid uid, DisposalJunctionComponent component, ref GetDisposalsConnectableDirectionsEvent args)
{
var direction = Transform(uid).LocalRotation;
args.Connectable = component.Degrees
.Select(degree => new Angle(degree.Theta + direction.Theta).GetDir())
.ToArray();
}
private void OnGetJunctionNextDirection(EntityUid uid, DisposalJunctionComponent component, ref GetDisposalsNextDirectionEvent args)
{
var next = Transform(uid).LocalRotation.GetDir();
var ev = new GetDisposalsConnectableDirectionsEvent();
RaiseLocalEvent(uid, ref ev);
var directions = ev.Connectable.Skip(1).ToArray();
if (args.Holder.PreviousDirectionFrom == Direction.Invalid ||
args.Holder.PreviousDirectionFrom == next)
{
args.Next = _random.Pick(directions);
return;
}
args.Next = next;
}
private void OnGetRouterConnectableDirections(EntityUid uid, DisposalRouterComponent component, ref GetDisposalsConnectableDirectionsEvent args)
{
OnGetJunctionConnectableDirections(uid, component, ref args);
}
private void OnGetRouterNextDirection(EntityUid uid, DisposalRouterComponent component, ref GetDisposalsNextDirectionEvent args)
{
var ev = new GetDisposalsConnectableDirectionsEvent();
RaiseLocalEvent(uid, ref ev);
if (args.Holder.Tags.Overlaps(component.Tags))
{
args.Next = ev.Connectable[1];
return;
}
args.Next = Transform(uid).LocalRotation.GetDir();
}
private void OnGetTransitConnectableDirections(EntityUid uid, DisposalTransitComponent component, ref GetDisposalsConnectableDirectionsEvent args)
{
var rotation = Transform(uid).LocalRotation;
var opposite = new Angle(rotation.Theta + Math.PI);
args.Connectable = new[] { rotation.GetDir(), opposite.GetDir() };
}
private void OnGetTransitNextDirection(EntityUid uid, DisposalTransitComponent component, ref GetDisposalsNextDirectionEvent args)
{
var ev = new GetDisposalsConnectableDirectionsEvent();
RaiseLocalEvent(uid, ref ev);
var previousDF = args.Holder.PreviousDirectionFrom;
var forward = ev.Connectable[0];
if (previousDF == Direction.Invalid)
{
args.Next = forward;
return;
}
var backward = ev.Connectable[1];
args.Next = previousDF == forward ? backward : forward;
}
private void OnGetTaggerConnectableDirections(EntityUid uid, DisposalTaggerComponent component, ref GetDisposalsConnectableDirectionsEvent args)
{
OnGetTransitConnectableDirections(uid, component, ref args);
}
private void OnGetTaggerNextDirection(EntityUid uid, DisposalTaggerComponent component, ref GetDisposalsNextDirectionEvent args)
{
args.Holder.Tags.Add(component.Tag);
OnGetTransitNextDirection(uid, component, ref args);
}
private void OnDeconstruct(EntityUid uid, DisposalTubeComponent component, ConstructionBeforeDeleteEvent args)
{
DisconnectTube(uid, component);
}
private void OnStartup(EntityUid uid, DisposalTubeComponent component, ComponentStartup args)
{
UpdateAnchored(uid, component, Transform(uid).Anchored);
}
private void OnBreak(EntityUid uid, DisposalTubeComponent component, BreakageEventArgs args)
{
DisconnectTube(uid, component);
}
private void OnOpenRouterUI(EntityUid uid, DisposalRouterComponent router, BoundUIOpenedEvent args)
{
UpdateRouterUserInterface(uid, router);
}
private void OnOpenTaggerUI(EntityUid uid, DisposalTaggerComponent tagger, BoundUIOpenedEvent args)
{
if (_uiSystem.HasUi(uid, SharedDisposalTaggerComponent.DisposalTaggerUiKey.Key))
{
_uiSystem.SetUiState(uid, SharedDisposalTaggerComponent.DisposalTaggerUiKey.Key,
new SharedDisposalTaggerComponent.DisposalTaggerUserInterfaceState(tagger.Tag));
}
}
/// <summary>
/// Gets component data to be used to update the user interface client-side.
/// </summary>
/// <returns>Returns a <see cref="SharedDisposalRouterComponent.DisposalRouterUserInterfaceState"/></returns>
private void UpdateRouterUserInterface(EntityUid uid, DisposalRouterComponent router)
{
if (router.Tags.Count <= 0)
{
_uiSystem.SetUiState(uid, SharedDisposalRouterComponent.DisposalRouterUiKey.Key, new SharedDisposalRouterComponent.DisposalRouterUserInterfaceState(""));
return;
}
var taglist = new StringBuilder();
foreach (var tag in router.Tags)
{
taglist.Append(tag);
taglist.Append(", ");
}
taglist.Remove(taglist.Length - 2, 2);
_uiSystem.SetUiState(uid, SharedDisposalRouterComponent.DisposalRouterUiKey.Key, new SharedDisposalRouterComponent.DisposalRouterUserInterfaceState(taglist.ToString()));
}
private void OnAnchorChange(EntityUid uid, DisposalTubeComponent component, ref AnchorStateChangedEvent args)
{
UpdateAnchored(uid, component, args.Anchored);
}
private void UpdateAnchored(EntityUid uid, DisposalTubeComponent component, bool anchored)
{
if (anchored)
{
ConnectTube(uid, component);
// TODO this visual data should just generalized into some anchored-visuals system/comp, this has nothing to do with disposal tubes.
_appearanceSystem.SetData(uid, DisposalTubeVisuals.VisualState, DisposalTubeVisualState.Anchored);
}
else
{
DisconnectTube(uid, component);
_appearanceSystem.SetData(uid, DisposalTubeVisuals.VisualState, DisposalTubeVisualState.Free);
}
}
public EntityUid? NextTubeFor(EntityUid target, Direction nextDirection, DisposalTubeComponent? targetTube = null)
{
if (!Resolve(target, ref targetTube))
return null;
var oppositeDirection = nextDirection.GetOpposite();
var xform = Transform(target);
if (!TryComp<MapGridComponent>(xform.GridUid, out var grid))
return null;
var position = xform.Coordinates;
foreach (var entity in _map.GetInDir(xform.GridUid.Value, grid, position, nextDirection))
{
if (!TryComp(entity, out DisposalTubeComponent? tube))
{
continue;
}
if (!CanConnect(entity, tube, oppositeDirection))
{
continue;
}
if (!CanConnect(target, targetTube, nextDirection))
{
continue;
}
return entity;
}
return null;
}
public static void ConnectTube(EntityUid _, DisposalTubeComponent tube)
{
if (tube.Connected)
{
return;
}
tube.Connected = true;
}
public void DisconnectTube(EntityUid _, DisposalTubeComponent tube)
{
if (!tube.Connected)
{
return;
}
tube.Connected = false;
var query = GetEntityQuery<DisposalHolderComponent>();
foreach (var entity in tube.Contents.ContainedEntities.ToArray())
{
if (query.TryGetComponent(entity, out var holder))
_disposableSystem.ExitDisposals(entity, holder);
}
}
public bool CanConnect(EntityUid tubeId, DisposalTubeComponent tube, Direction direction)
{
if (!tube.Connected)
{
return false;
}
var ev = new GetDisposalsConnectableDirectionsEvent();
RaiseLocalEvent(tubeId, ref ev);
return ev.Connectable.Contains(direction);
}
public void PopupDirections(EntityUid tubeId, DisposalTubeComponent _, EntityUid recipient)
{
var ev = new GetDisposalsConnectableDirectionsEvent();
RaiseLocalEvent(tubeId, ref ev);
var directions = string.Join(", ", ev.Connectable);
_popups.PopupEntity(Loc.GetString("disposal-tube-component-popup-directions-text", ("directions", directions)), tubeId, recipient);
}
public override bool TryInsert(EntityUid uid, DisposalUnitComponent from, IEnumerable<string>? tags = default, DisposalEntryComponent? entry = null)
{
if (!Resolve(uid, ref entry))
return false;
var xform = Transform(uid);
var holder = Spawn(entry.HolderPrototypeId, _transform.GetMapCoordinates(uid, xform: xform));
var holderComponent = Comp<DisposalHolderComponent>(holder);
foreach (var entity in from.Container.ContainedEntities.ToArray())
{
_containerSystem.Insert(entity, holderComponent.Container);
}
_atmosSystem.Merge(holderComponent.Air, from.Air);
from.Air.Clear();
if (tags != null)
holderComponent.Tags.UnionWith(tags);
return _disposableSystem.EnterTube(holder, uid, holderComponent);
}
}
}
@@ -1,7 +0,0 @@
namespace Content.Server.Disposal.Tube;
[ByRefEvent]
public record struct GetDisposalsConnectableDirectionsEvent
{
public Direction[] Connectable;
}
@@ -1,9 +0,0 @@
using Content.Server.Disposal.Unit;
namespace Content.Server.Disposal.Tube;
[ByRefEvent]
public record struct GetDisposalsNextDirectionEvent(DisposalHolderComponent Holder)
{
public Direction Next;
}
@@ -1,6 +1,6 @@
using Content.Server.Administration;
using Content.Server.Disposal.Tube;
using Content.Shared.Administration;
using Content.Shared.Disposal.Tube;
using Robust.Shared.Console;
namespace Content.Server.Disposal
@@ -54,7 +54,7 @@ namespace Content.Server.Disposal
return;
}
_entities.System<DisposalTubeSystem>().PopupDirections(id.Value, tube, player.AttachedEntity.Value);
_entities.System<DisposalTubeSystem>().PopupDirections((id.Value, tube), player.AttachedEntity.Value);
}
}
}
@@ -1,10 +1,11 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Systems;
using Content.Shared.Atmos;
using Content.Shared.Disposal.Unit;
namespace Content.Server.Disposal.Unit;
public sealed class BeingDisposedSystem : EntitySystem
/// <inheritdoc/>
public sealed class BeingDisposedSystem : SharedBeingDisposedSystem
{
public override void Initialize()
{
@@ -15,26 +16,26 @@ public sealed class BeingDisposedSystem : EntitySystem
SubscribeLocalEvent<BeingDisposedComponent, AtmosExposedGetAirEvent>(OnGetAir);
}
private void OnGetAir(EntityUid uid, BeingDisposedComponent component, ref AtmosExposedGetAirEvent args)
private void OnGetAir(Entity<BeingDisposedComponent> ent, ref AtmosExposedGetAirEvent args)
{
if (TryComp<DisposalHolderComponent>(component.Holder, out var holder))
if (TryComp<DisposalHolderComponent>(ent.Comp.Holder, out var holder))
{
args.Gas = holder.Air;
args.Handled = true;
}
}
private void OnInhaleLocation(EntityUid uid, BeingDisposedComponent component, InhaleLocationEvent args)
private void OnInhaleLocation(Entity<BeingDisposedComponent> ent, ref InhaleLocationEvent args)
{
if (TryComp<DisposalHolderComponent>(component.Holder, out var holder))
if (TryComp<DisposalHolderComponent>(ent.Comp.Holder, out var holder))
{
args.Gas = holder.Air;
}
}
private void OnExhaleLocation(EntityUid uid, BeingDisposedComponent component, ExhaleLocationEvent args)
private void OnExhaleLocation(Entity<BeingDisposedComponent> ent, ref ExhaleLocationEvent args)
{
if (TryComp<DisposalHolderComponent>(component.Holder, out var holder))
if (TryComp<DisposalHolderComponent>(ent.Comp.Holder, out var holder))
{
args.Gas = holder.Air;
}
@@ -1,269 +0,0 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Disposal.Tube;
using Content.Shared.Body;
using Content.Shared.Damage.Systems;
using Content.Shared.Disposal.Components;
using Content.Shared.Item;
using Content.Shared.Throwing;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
namespace Content.Server.Disposal.Unit
{
public sealed partial class DisposableSystem : EntitySystem
{
[Dependency] private ThrowingSystem _throwing = default!;
[Dependency] private AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private DamageableSystem _damageable = default!;
[Dependency] private DisposalUnitSystem _disposalUnitSystem = default!;
[Dependency] private DisposalTubeSystem _disposalTubeSystem = default!;
[Dependency] private SharedAudioSystem _audio = default!;
[Dependency] private SharedContainerSystem _containerSystem = default!;
[Dependency] private SharedMapSystem _maps = default!;
[Dependency] private SharedPhysicsSystem _physicsSystem = default!;
[Dependency] private SharedTransformSystem _xformSystem = default!;
private EntityQuery<DisposalTubeComponent> _disposalTubeQuery;
private EntityQuery<DisposalUnitComponent> _disposalUnitQuery;
private EntityQuery<MetaDataComponent> _metaQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<TransformComponent> _xformQuery;
public override void Initialize()
{
base.Initialize();
_disposalTubeQuery = GetEntityQuery<DisposalTubeComponent>();
_disposalUnitQuery = GetEntityQuery<DisposalUnitComponent>();
_metaQuery = GetEntityQuery<MetaDataComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
SubscribeLocalEvent<DisposalHolderComponent, ComponentStartup>(OnComponentStartup);
SubscribeLocalEvent<DisposalHolderComponent, ContainerIsInsertingAttemptEvent>(CanInsert);
SubscribeLocalEvent<DisposalHolderComponent, EntInsertedIntoContainerMessage>(OnInsert);
}
private void OnComponentStartup(EntityUid uid, DisposalHolderComponent holder, ComponentStartup args)
{
holder.Container = _containerSystem.EnsureContainer<Container>(uid, nameof(DisposalHolderComponent));
}
private void CanInsert(Entity<DisposalHolderComponent> ent, ref ContainerIsInsertingAttemptEvent args)
{
if (!HasComp<ItemComponent>(args.EntityUid) && !HasComp<BodyComponent>(args.EntityUid))
args.Cancel();
}
private void OnInsert(Entity<DisposalHolderComponent> ent, ref EntInsertedIntoContainerMessage args)
{
if (_physicsQuery.TryGetComponent(args.Entity, out var physBody))
_physicsSystem.SetCanCollide(args.Entity, false, body: physBody);
}
public void ExitDisposals(EntityUid uid, DisposalHolderComponent? holder = null, TransformComponent? holderTransform = null)
{
if (Terminating(uid))
return;
if (!Resolve(uid, ref holder, ref holderTransform))
return;
if (holder.IsExitingDisposals)
{
Log.Error("Tried exiting disposals twice. This should never happen.");
return;
}
holder.IsExitingDisposals = true;
// Check for a disposal unit to throw them into and then eject them from it.
// *This ejection also makes the target not collide with the unit.*
// *This is on purpose.*
EntityUid? disposalId = null;
DisposalUnitComponent? duc = null;
var gridUid = holderTransform.GridUid;
if (TryComp<MapGridComponent>(gridUid, out var grid))
{
foreach (var contentUid in _maps.GetLocal(gridUid.Value, grid, holderTransform.Coordinates))
{
if (_disposalUnitQuery.TryGetComponent(contentUid, out duc))
{
disposalId = contentUid;
break;
}
}
}
// We're purposely iterating over all the holder's children
// because the holder might have something teleported into it,
// outside the usual container insertion logic.
var children = holderTransform.ChildEnumerator;
while (children.MoveNext(out var entity))
{
RemComp<BeingDisposedComponent>(entity);
var meta = _metaQuery.GetComponent(entity);
if (holder.Container.Contains(entity))
_containerSystem.Remove((entity, null, meta), holder.Container, reparent: false, force: true);
var xform = _xformQuery.GetComponent(entity);
if (xform.ParentUid != uid)
continue;
if (duc != null)
_containerSystem.Insert((entity, xform, meta), duc.Container);
else
{
_xformSystem.AttachToGridOrMap(entity, xform);
var direction = holder.CurrentDirection == Direction.Invalid ? holder.PreviousDirection : holder.CurrentDirection;
if (direction != Direction.Invalid && _xformQuery.TryGetComponent(gridUid, out var gridXform))
{
var directionAngle = direction.ToAngle();
directionAngle += _xformSystem.GetWorldRotation(gridXform);
_throwing.TryThrow(entity, directionAngle.ToWorldVec() * 3f, 10f);
}
}
}
if (disposalId != null && duc != null)
{
_disposalUnitSystem.TryEjectContents(disposalId.Value, duc);
}
if (_atmosphereSystem.GetContainingMixture(uid, false, true) is { } environment)
{
_atmosphereSystem.Merge(environment, holder.Air);
holder.Air.Clear();
}
Del(uid);
}
// Note: This function will cause an ExitDisposals on any failure that does not make an ExitDisposals impossible.
public bool EnterTube(EntityUid holderUid, EntityUid toUid, DisposalHolderComponent? holder = null, TransformComponent? holderTransform = null, DisposalTubeComponent? to = null, TransformComponent? toTransform = null)
{
if (!Resolve(holderUid, ref holder, ref holderTransform))
return false;
if (holder.IsExitingDisposals)
{
Log.Error("Tried entering tube after exiting disposals. This should never happen.");
return false;
}
if (!Resolve(toUid, ref to, ref toTransform))
{
ExitDisposals(holderUid, holder, holderTransform);
return false;
}
foreach (var ent in holder.Container.ContainedEntities)
{
var comp = EnsureComp<BeingDisposedComponent>(ent);
comp.Holder = holderUid;
}
// Insert into next tube
if (!_containerSystem.Insert(holderUid, to.Contents))
{
ExitDisposals(holderUid, holder, holderTransform);
return false;
}
if (holder.CurrentTube != null)
{
holder.PreviousTube = holder.CurrentTube;
holder.PreviousDirection = holder.CurrentDirection;
}
holder.CurrentTube = toUid;
var ev = new GetDisposalsNextDirectionEvent(holder);
RaiseLocalEvent(toUid, ref ev);
holder.CurrentDirection = ev.Next;
holder.StartingTime = 0.1f;
holder.TimeLeft = 0.1f;
// Logger.InfoS("c.s.disposal.holder", $"Disposals dir {holder.CurrentDirection}");
// Invalid direction = exit now!
if (holder.CurrentDirection == Direction.Invalid)
{
ExitDisposals(holderUid, holder, holderTransform);
return false;
}
// damage entities on turns and play sound
if (holder.CurrentDirection != holder.PreviousDirection)
{
foreach (var ent in holder.Container.ContainedEntities)
{
_damageable.TryChangeDamage(ent, to.DamageOnTurn);
}
_audio.PlayPvs(to.ClangSound, toUid);
}
return true;
}
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<DisposalHolderComponent>();
while (query.MoveNext(out var uid, out var holder))
{
UpdateComp(uid, holder, frameTime);
}
}
private void UpdateComp(EntityUid uid, DisposalHolderComponent holder, float frameTime)
{
while (frameTime > 0)
{
var time = frameTime;
if (time > holder.TimeLeft)
{
time = holder.TimeLeft;
}
holder.TimeLeft -= time;
frameTime -= time;
if (!Exists(holder.CurrentTube))
{
ExitDisposals(uid, holder);
break;
}
var currentTube = holder.CurrentTube!.Value;
if (holder.TimeLeft > 0)
{
var progress = 1 - holder.TimeLeft / holder.StartingTime;
var origin = _xformQuery.GetComponent(currentTube).Coordinates;
var destination = holder.CurrentDirection.ToVec();
var newPosition = destination * progress;
// This is some supreme shit code.
_xformSystem.SetCoordinates(uid, _xformSystem.WithEntityId(origin.Offset(newPosition), currentTube));
continue;
}
// Past this point, we are performing inter-tube transfer!
// Remove current tube content
_containerSystem.Remove(uid, _disposalTubeQuery.GetComponent(currentTube).Contents, reparent: false, force: true);
// Find next tube
var nextTube = _disposalTubeSystem.NextTubeFor(currentTube, holder.CurrentDirection);
if (!Exists(nextTube))
{
ExitDisposals(uid, holder);
break;
}
// Perform remainder of entry process
if (!EnterTube(uid, nextTube!.Value, holder))
{
break;
}
}
}
}
}
@@ -1,54 +0,0 @@
using Content.Server.Atmos;
using Content.Shared.Atmos;
using Robust.Shared.Containers;
namespace Content.Server.Disposal.Unit
{
[RegisterComponent]
public sealed partial class DisposalHolderComponent : Component, IGasMixtureHolder
{
public Container Container = null!;
/// <summary>
/// The total amount of time that it will take for this entity to
/// be pushed to the next tube
/// </summary>
[ViewVariables]
public float StartingTime { get; set; }
/// <summary>
/// Time left until the entity is pushed to the next tube
/// </summary>
[ViewVariables]
public float TimeLeft { get; set; }
[ViewVariables]
public EntityUid? PreviousTube { get; set; }
[ViewVariables]
public Direction PreviousDirection { get; set; } = Direction.Invalid;
[ViewVariables]
public Direction PreviousDirectionFrom => (PreviousDirection == Direction.Invalid) ? Direction.Invalid : PreviousDirection.GetOpposite();
[ViewVariables]
public EntityUid? CurrentTube { get; set; }
// CurrentDirection is not null when CurrentTube isn't null.
[ViewVariables]
public Direction CurrentDirection { get; set; } = Direction.Invalid;
/// <summary>Mistake prevention</summary>
[ViewVariables]
public bool IsExitingDisposals { get; set; } = false;
/// <summary>
/// A list of tags attached to the content, used for sorting
/// </summary>
[ViewVariables]
public HashSet<string> Tags { get; set; } = new();
[DataField("air")]
public GasMixture Air { get; set; } = new(70);
}
}
@@ -1,44 +1,27 @@
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Destructible;
using Content.Shared.Disposal.Components;
using Content.Shared.Disposal.Unit;
using Content.Shared.Explosion;
namespace Content.Server.Disposal.Unit;
/// <inheritdoc/>
public sealed partial class DisposalUnitSystem : SharedDisposalUnitSystem
{
[Dependency] private AtmosphereSystem _atmosSystem = default!;
[Dependency] private SharedTransformSystem _xform = default!;
[Dependency] private AtmosphereSystem _atmos = default!;
public override void Initialize()
/// <inheritdoc/>
protected override void IntakeAir(Entity<DisposalUnitComponent> ent, TransformComponent xform)
{
base.Initialize();
var air = ent.Comp.Air;
var indices = _xform.GetGridTilePositionOrDefault((ent, xform));
SubscribeLocalEvent<DisposalUnitComponent, DestructionEventArgs>(OnDestruction);
SubscribeLocalEvent<DisposalUnitComponent, BeforeExplodeEvent>(OnExploded);
}
protected override void HandleAir(EntityUid uid, DisposalUnitComponent component, TransformComponent xform)
{
var air = component.Air;
var indices = TransformSystem.GetGridTilePositionOrDefault((uid, xform));
if (_atmosSystem.GetTileMixture(xform.GridUid, xform.MapUid, indices, true) is { Temperature: > 0f } environment)
if (_atmos.GetTileMixture(xform.GridUid, xform.MapUid, indices, true) is { Temperature: > 0f } environment)
{
var transferMoles = 0.1f * (0.25f * Atmospherics.OneAtmosphere * 1.01f - air.Pressure) * air.Volume / (environment.Temperature * Atmospherics.R);
component.Air = environment.Remove(transferMoles);
ent.Comp.Air = environment.Remove(transferMoles);
}
}
private void OnDestruction(EntityUid uid, DisposalUnitComponent component, DestructionEventArgs args)
{
TryEjectContents(uid, component);
}
private void OnExploded(Entity<DisposalUnitComponent> ent, ref BeforeExplodeEvent args)
{
args.Contents.AddRange(ent.Comp.Container.ContainedEntities);
}
}
@@ -1,4 +0,0 @@
namespace Content.Server.Disposal.Unit
{
public record DoInsertDisposalUnitEvent(EntityUid? User, EntityUid ToInsert, EntityUid Unit);
}
@@ -1,4 +1,3 @@
using System.Linq;
using System.Runtime.InteropServices;
using Content.Server.Atmos.Components;
using Content.Server.Explosion.Components;
@@ -214,12 +213,16 @@ public sealed partial class ExplosionSystem
// that this will result in a non-airtight entity.Entities that ONLY break via construction graph node changes
// are currently effectively "invincible" as far as this is concerned. This really should be done more rigorously.
var totalDamageTarget = FixedPoint2.MaxValue;
if (_destructibleQuery.TryGetComponent(uid, out var destructible))
if (_destructibleQuery.TryComp(uid, out var destructible))
{
totalDamageTarget = _destructibleSystem.DestroyedAt(uid, destructible);
}
if (totalDamageTarget == FixedPoint2.MaxValue || !_injurableQuery.TryGetComponent(uid, out var injurable))
// We are assuming airtight entities don't need to relay since they shouldn't have inventories.
var modifiers = _damageableSystem.GetDamageModifierSet(uid);
var explosionComp = _explosionResistanceQuery.CompOrNull(uid);
if (totalDamageTarget == FixedPoint2.MaxValue || !_injurableQuery.TryComp(uid, out var injurable))
{
for (var i = 0; i < explosionTolerance.Length; i++)
{
@@ -238,35 +241,94 @@ public sealed partial class ExplosionSystem
{
// TODO EXPLOSION SYSTEM
// cache explosion type damage.
if (!_prototypeManager.Resolve(id, out ExplosionPrototype? explosionType))
if (!_prototypeManager.Resolve(id, out var explosionType))
continue;
// evaluate the damage that this damage type would do to this entity
var damagePerIntensity = FixedPoint2.Zero;
// Create a dictionary of intensity thresholds which dictates when damagePerIntensity increases!
var damageThresholds = new SortedDictionary<FixedPoint2, FixedPoint2>();
foreach (var (type, value) in explosionType.DamagePerIntensity.DamageDict)
{
if (!_damageableSystem.CanBeDamagedBy((uid, injurable), type))
continue;
// TODO EXPLOSION SYSTEM
// add a variant of the event that gets raised once, instead of once per prototype.
// Or better yet, just calculate this manually w/o the event.
// The event mainly exists for indirect resistances via things like inventory & clothing
// But this shouldn't matter for airtight entities.
var ev = new GetExplosionResistanceEvent(explosionType.ID);
RaiseLocalEvent(uid, ref ev);
var modifier = mod;
if (explosionComp != null)
{
modifier *= explosionComp.DamageCoefficient;
if (explosionComp.Modifiers.TryGetValue(explosionType.ID, out var typeMod))
modifier *= typeMod;
}
damagePerIntensity += value * mod * Math.Max(0, ev.DamageCoefficient);
if (modifiers != null)
{
if (modifiers.Coefficients.TryGetValue(type, out var armorMod))
modifier *= armorMod;
if (modifiers.FlatReduction.TryGetValue(type, out var flat))
{
if (flat > 0)
{
// If the flat modifier is reducing damage, we cache the extra damage per intensity for later!
var intensity = flat / value;
var damage = damageThresholds.GetValueOrDefault(intensity);
damageThresholds[intensity] = value * Math.Max(0, modifier) + damage;
continue;
}
}
}
damagePerIntensity += value * Math.Max(0, modifier);
}
var toleranceValue = damagePerIntensity > 0
? (float) ((totalDamageTarget - _damageableSystem.GetTotalDamage(uid)) / damagePerIntensity)
: ToleranceValues.Invulnerable;
explosionTolerance[index] = toleranceValue;
explosionTolerance[index] = GetExplosionTolerance(uid, totalDamageTarget, damagePerIntensity, damageThresholds);
}
}
private FixedPoint2 GetExplosionTolerance(EntityUid uid,
FixedPoint2 totalDamageTarget,
FixedPoint2 damagePerIntensity,
SortedDictionary<FixedPoint2, FixedPoint2> damageThresholds)
{
return GetExplosionTolerance(totalDamageTarget - _damageableSystem.GetTotalDamage(uid),
damagePerIntensity,
damageThresholds);
}
private FixedPoint2 GetExplosionTolerance(FixedPoint2 damageTarget,
FixedPoint2 damagePerIntensity,
SortedDictionary<FixedPoint2, FixedPoint2> damageThresholds)
{
var tolerance = damagePerIntensity > 0 ? damageTarget / damagePerIntensity : ToleranceValues.Invulnerable;
var prevIntensity = FixedPoint2.Zero;
/*
* Calculated through a pretty simple equation which relies on this dictionary being sorted.
* We precalculate the intensity at which an explosion's damage type exceeds the flat reduction of an entity's armor
* That is done above and stored in our `damageThresholds` SortedDictionary. If you can find a more mem efficient way to do this be my guest,
* but these values *have* to be sorted.
*/
foreach (var (intensity, damage) in damageThresholds)
{
// Check if the object would break before hitting this threshold, if so, return the current tolerance value
if (intensity > tolerance)
return tolerance;
/*
* If the object breaks after this threshold, reduce the HP left by the amount of HP lost between the last flat reduction and this one
* Then adjust our damagePerIntensity and new tolerance values accordingly.
* Lastly store this intensity value so we can calculate the delta next loop.
*/
damageTarget -= (intensity - prevIntensity) * damagePerIntensity;
damagePerIntensity += damage;
tolerance = intensity + damageTarget / damagePerIntensity;
prevIntensity = intensity;
}
return tolerance;
}
private void OnAirtightGridRemoved(EntityUid entity)
{
if (!TryComp(entity, out ExplosionAirtightGridComponent? airtightGrid))
@@ -1,24 +1,20 @@
using Content.Shared.CCVar;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.Explosion;
using Content.Shared.Explosion.Components;
using Content.Shared.Maps;
using Content.Shared.Physics;
using Content.Shared.Projectiles;
using Content.Shared.Tag;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Numerics;
using Content.Shared.Damage.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using TimedDespawnComponent = Robust.Shared.Spawners.TimedDespawnComponent;
namespace Content.Server.Explosion.EntitySystems;
@@ -228,34 +224,25 @@ public sealed partial class ExplosionSystem
ProcessEntity(uid, epicenter, damage, throwForce, id, xform, fireStacks, cause);
}
// process anchored entities
var tileBlocked = false;
_anchored.Clear();
_map.GetAnchoredEntities(grid, tile, _anchored);
foreach (var entity in _anchored)
{
processed.Add(entity);
ProcessEntity(entity, epicenter, damage, throwForce, id, null, fireStacks, cause);
}
// heat the atmosphere
if (temperature != null)
{
_atmosphere.HotspotExpose(grid.Owner, tile, temperature.Value, currentIntensity, cause, true);
}
// We process anchored entities last, these should've been caught by the lookups earlier.
// Walls and reinforced walls will break into girders. These girders will also be considered turf-blocking for
// the purposes of destroying floors. Again, ideally the process of damaging an entity should somehow return
// information about the entities that were spawned as a result, but without that information we just have to
// re-check for new anchored entities. Compared to entity spawning & deleting, this should still be relatively minor.
var tileBlocked = false;
_map.GetAnchoredEntities(grid, tile, _anchored);
if (_anchored.Count > 0)
{
_anchored.Clear();
_map.GetAnchoredEntities(grid, tile, _anchored);
foreach (var entity in _anchored)
{
tileBlocked |= IsBlockingTurf(entity);
}
_anchored.Clear();
}
// Next, we get the intersecting entities AGAIN, but purely for throwing. This way, glass shards spawned from
@@ -441,7 +428,7 @@ public sealed partial class ExplosionSystem
DamageSpecifier? originalDamage,
float throwForce,
string id,
TransformComponent? xform,
TransformComponent xform,
float? fireStacksOnIgnite,
EntityUid? cause)
{
@@ -454,7 +441,7 @@ public sealed partial class ExplosionSystem
continue;
// TODO EXPLOSIONS turn explosions into entities, and pass the the entity in as the damage origin.
_damageableSystem.TryChangeDamage((entity, damageable), damage, ignoreResistances: true, ignoreGlobalModifiers: true);
_damageableSystem.ChangeDamage((entity, damageable), damage);
if (_actorQuery.HasComp(entity))
{
@@ -478,8 +465,7 @@ public sealed partial class ExplosionSystem
}
// throw
if (xform != null // null implies anchored or in a container
&& !xform.Anchored
if (!xform.Anchored
&& throwForce > 0
&& !EntityManager.IsQueuedForDeletion(uid)
&& _physicsQuery.TryGetComponent(uid, out var physics)
@@ -704,7 +690,7 @@ sealed class Explosion
private readonly IEntityManager _entMan;
private readonly ExplosionSystem _system;
private readonly SharedMapSystem _mapSystem;
private readonly Shared.Damage.Systems.DamageableSystem _damageable;
private readonly DamageableSystem _damageable;
public readonly EntityUid VisualEnt;
@@ -6,6 +6,7 @@ using Content.Server.Atmos.EntitySystems;
using Content.Server.Destructible;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NPC.Pathfinding;
using Content.Shared.Armor;
using Content.Shared.Atmos.Components;
using Content.Shared.Camera;
using Content.Shared.CCVar;
@@ -64,6 +65,7 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
[Dependency] private EntityQuery<ActorComponent> _actorQuery = default!;
[Dependency] private EntityQuery<DestructibleComponent> _destructibleQuery = default!;
[Dependency] private EntityQuery<DamageableComponent> _damageableQuery = default!;
[Dependency] private EntityQuery<ExplosionResistanceComponent> _explosionResistanceQuery = default!;
[Dependency] private EntityQuery<InjurableComponent> _injurableQuery = default!;
[Dependency] private EntityQuery<AirtightComponent> _airtightQuery = default!;
[Dependency] private EntityQuery<TileHistoryComponent> _tileHistoryQuery = default!;
@@ -62,33 +62,27 @@ namespace Content.Server.GameTicking.Commands
shell.WriteLine("Round has not started.");
return;
}
else if (ticker.RunLevel == GameRunLevel.InRound)
var id = args[0];
if (!int.TryParse(args[1], out var sid))
{
string id = args[0];
shell.WriteError(Loc.GetString("shell-argument-must-be-number"));
}
if (!int.TryParse(args[1], out var sid))
{
shell.WriteError(Loc.GetString("shell-argument-must-be-number"));
}
var station = _entManager.GetEntity(new NetEntity(sid));
var jobPrototype = _prototypeManager.Index<JobPrototype>(id);
if(stationJobs.TryGetJobSlot(station, jobPrototype, out var slots) == false || slots == 0)
{
shell.WriteLine($"{jobPrototype.LocalizedName} has no available slots.");
return;
}
if (_adminManager.IsAdmin(player) && _cfg.GetCVar(CCVars.AdminDeadminOnJoin))
{
_adminManager.DeAdmin(player);
}
ticker.MakeJoinGame(player, station, id);
var station = _entManager.GetEntity(new NetEntity(sid));
var jobPrototype = _prototypeManager.Index<JobPrototype>(id);
if (stationJobs.TryGetJobSlot(station, jobPrototype, out var slots) == false || slots == 0)
{
shell.WriteLine($"{jobPrototype.LocalizedName} has no available slots.");
return;
}
ticker.MakeJoinGame(player, EntityUid.Invalid);
if (_adminManager.IsAdmin(player) && _cfg.GetCVar(CCVars.AdminDeadminOnJoin))
{
_adminManager.DeAdmin(player);
}
ticker.MakeJoinGame(player, station, id);
}
}
}
@@ -209,7 +209,12 @@ namespace Content.Server.GameTicking
speciesId = weights.Pick(_robustRandom);
}
character = HumanoidCharacterProfile.RandomWithSpecies(speciesId);
// The random profile must retain the job priorities set by the player
var jobs = character.JobPriorities;
character = HumanoidCharacterProfile.RandomWithSpecies(speciesId).WithJobPriorities(jobs);
// This does not utilize overflow job slots, so if the character profile
// had no available job priorities (ie Captain on Dev) set, then the player will spawn as a ghost
// Corvax-Sponsors-Start
var sponsorPrototypes = _sponsors != null && _sponsors.TryGetServerPrototypes(player.UserId, out var prototypes) ? prototypes.ToArray() : [];
character.Appearance = HumanoidCharacterAppearance.EnsureValid(character.Appearance, character.Species, character.Sex, sponsorPrototypes);
+1 -134
View File
@@ -1,138 +1,5 @@
using System.Linq;
using Content.Server.Popups;
using Content.Shared.DoAfter;
using Content.Shared.IdentityManagement;
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Robust.Shared.Containers;
namespace Content.Server.Implants;
public sealed partial class ImplanterSystem : SharedImplanterSystem
{
[Dependency] private PopupSystem _popup = default!;
[Dependency] private SharedDoAfterSystem _doAfter = default!;
[Dependency] private SharedContainerSystem _container = default!;
public override void Initialize()
{
base.Initialize();
InitializeImplanted();
SubscribeLocalEvent<ImplanterComponent, AfterInteractEvent>(OnImplanterAfterInteract);
SubscribeLocalEvent<ImplanterComponent, ImplantEvent>(OnImplant);
SubscribeLocalEvent<ImplanterComponent, DrawEvent>(OnDraw);
}
// TODO: This all needs to be moved to shared and predicted.
private void OnImplanterAfterInteract(EntityUid uid, ImplanterComponent component, AfterInteractEvent args)
{
if (args.Target == null || !args.CanReach || args.Handled)
return;
var target = args.Target.Value;
if (!CheckTarget(target, component.Whitelist, component.Blacklist))
return;
//TODO: Rework when surgery is in for implant cases
if (component.CurrentMode == ImplanterToggleMode.Draw && !component.ImplantOnly)
{
TryDraw(component, args.User, target, uid);
}
else
{
if (!CanImplant(args.User, target, uid, component, out var implant, out _))
{
// no popup if implant doesn't exist
if (implant == null)
return;
// show popup to the user saying implant failed
var name = Identity.Name(target, EntityManager, args.User);
var msg = Loc.GetString("implanter-component-implant-failed", ("implant", implant), ("target", name));
_popup.PopupEntity(msg, target, args.User);
// prevent further interaction since popup was shown
args.Handled = true;
return;
}
//Implant self instantly, otherwise try to inject the target.
if (args.User == target)
Implant(target, target, uid, component);
else
TryImplant(component, args.User, target, uid);
}
args.Handled = true;
}
/// <summary>
/// Attempt to implant someone else.
/// </summary>
/// <param name="component">Implanter component</param>
/// <param name="user">The entity using the implanter</param>
/// <param name="target">The entity being implanted</param>
/// <param name="implanter">The implanter being used</param>
public void TryImplant(ImplanterComponent component, EntityUid user, EntityUid target, EntityUid implanter)
{
var args = new DoAfterArgs(EntityManager, user, component.ImplantTime, new ImplantEvent(), implanter, target: target, used: implanter)
{
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = true,
};
if (!_doAfter.TryStartDoAfter(args))
return;
_popup.PopupEntity(Loc.GetString("injector-component-needle-injecting-user"), target, user);
var userName = Identity.Entity(user, EntityManager);
_popup.PopupEntity(Loc.GetString("implanter-component-implanting-target", ("user", userName)), user, target, PopupType.LargeCaution);
}
/// <summary>
/// Try to remove an implant and store it in an implanter
/// </summary>
/// <param name="component">Implanter component</param>
/// <param name="user">The entity using the implanter</param>
/// <param name="target">The entity getting their implant removed</param>
/// <param name="implanter">The implanter being used</param>
//TODO: Remove when surgery is in
public void TryDraw(ImplanterComponent component, EntityUid user, EntityUid target, EntityUid implanter)
{
var args = new DoAfterArgs(EntityManager, user, component.DrawTime, new DrawEvent(), implanter, target: target, used: implanter)
{
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = true,
};
if (_doAfter.TryStartDoAfter(args))
_popup.PopupEntity(Loc.GetString("injector-component-needle-injecting-user"), target, user);
}
private void OnImplant(EntityUid uid, ImplanterComponent component, ImplantEvent args)
{
if (args.Cancelled || args.Handled || args.Target == null || args.Used == null)
return;
Implant(args.User, args.Target.Value, args.Used.Value, component);
args.Handled = true;
}
private void OnDraw(EntityUid uid, ImplanterComponent component, DrawEvent args)
{
if (args.Cancelled || args.Handled || args.Used == null || args.Target == null)
return;
Draw(args.Used.Value, args.User, args.Target.Value, component);
args.Handled = true;
}
}
public sealed partial class ImplanterSystem : SharedImplanterSystem;
@@ -1,203 +0,0 @@
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Destructible;
using Content.Shared.DoAfter;
using Content.Shared.Gibbing;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Kitchen;
using Content.Shared.Kitchen.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Popups;
using Content.Shared.Storage;
using Content.Shared.Verbs;
using Robust.Server.Containers;
using Robust.Server.GameObjects;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Kitchen.EntitySystems;
public sealed partial class SharpSystem : EntitySystem
{
[Dependency] private GibbingSystem _gibbing = default!;
[Dependency] private SharedDestructibleSystem _destructibleSystem = default!;
[Dependency] private SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private SharedPopupSystem _popupSystem = default!;
[Dependency] private ContainerSystem _containerSystem = default!;
[Dependency] private MobStateSystem _mobStateSystem = default!;
[Dependency] private TransformSystem _transform = default!;
[Dependency] private IRobustRandom _robustRandom = default!;
[Dependency] private ISharedAdminLogManager _adminLogger = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SharpComponent, AfterInteractEvent>(OnAfterInteract, before: [typeof(IngestionSystem)]);
SubscribeLocalEvent<SharpComponent, SharpDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<ButcherableComponent, GetVerbsEvent<InteractionVerb>>(OnGetInteractionVerbs);
}
private void OnAfterInteract(EntityUid uid, SharpComponent component, AfterInteractEvent args)
{
if (args.Handled || args.Target is null || !args.CanReach)
return;
if (TryStartButcherDoafter(uid, args.Target.Value, args.User))
args.Handled = true;
}
private bool TryStartButcherDoafter(EntityUid knife, EntityUid target, EntityUid user)
{
if (!TryComp<ButcherableComponent>(target, out var butcher))
return false;
if (!TryComp<SharpComponent>(knife, out var sharp))
return false;
if (TryComp<MobStateComponent>(target, out var mobState) && !_mobStateSystem.IsDead(target, mobState))
return false;
if (butcher.Type != ButcheringType.Knife && target != user)
{
_popupSystem.PopupEntity(Loc.GetString("butcherable-different-tool", ("target", target)), knife, user);
return false;
}
if (!sharp.Butchering.Add(target))
return false;
// if the user isn't the entity with the sharp component,
// they will need to be holding something with their hands, so we set needHand to true
// so that the doafter can be interrupted if they drop the item in their hands
var needHand = user != knife;
var doAfter =
new DoAfterArgs(EntityManager, user, sharp.ButcherDelayModifier * butcher.ButcherDelay, new SharpDoAfterEvent(), knife, target: target, used: knife)
{
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = needHand,
};
_doAfterSystem.TryStartDoAfter(doAfter);
return true;
}
private void OnDoAfter(EntityUid uid, SharpComponent component, DoAfterEvent args)
{
if (args.Handled || !TryComp<ButcherableComponent>(args.Args.Target, out var butcher))
return;
if (args.Cancelled)
{
component.Butchering.Remove(args.Args.Target.Value);
return;
}
component.Butchering.Remove(args.Args.Target.Value);
var spawnEntities = EntitySpawnCollection.GetSpawns(butcher.SpawnedEntities, _robustRandom);
var coords = _transform.GetMapCoordinates(args.Args.Target.Value);
EntityUid popupEnt = default!;
if (_containerSystem.TryGetContainingContainer(args.Args.Target.Value, out var container))
{
foreach (var proto in spawnEntities)
{
// distribute the spawned items randomly in a small radius around the origin
popupEnt = SpawnInContainerOrDrop(proto, container.Owner, container.ID);
}
}
else
{
foreach (var proto in spawnEntities)
{
// distribute the spawned items randomly in a small radius around the origin
popupEnt = Spawn(proto, coords.Offset(_robustRandom.NextVector2(0.25f)));
}
}
// only show a big popup when butchering living things.
// Meant to differentiate cutting up clothes and cutting up your boss.
var popupType = HasComp<MobStateComponent>(args.Args.Target.Value)
? PopupType.LargeCaution
: PopupType.Small;
_popupSystem.PopupEntity(Loc.GetString("butcherable-knife-butchered-success", ("target", args.Args.Target.Value), ("knife", Identity.Entity(uid, EntityManager))),
popupEnt,
args.Args.User,
popupType);
_gibbing.Gib(args.Args.Target.Value); // does nothing if ent can't be gibbed
_destructibleSystem.DestroyEntity(args.Args.Target.Value);
args.Handled = true;
_adminLogger.Add(LogType.Gib,
$"{ToPrettyString(args.User):user} " +
$"has butchered {ToPrettyString(args.Target):target} " +
$"with {ToPrettyString(args.Used):knife}");
}
private void OnGetInteractionVerbs(EntityUid uid, ButcherableComponent component, GetVerbsEvent<InteractionVerb> args)
{
if (component.Type != ButcheringType.Knife || !args.CanAccess || !args.CanInteract)
return;
// if the user has no hands, don't show them the verb if they have no SharpComponent either
if (!TryComp<SharpComponent>(args.User, out var userSharpComp) && args.Hands == null)
return;
var disabled = false;
string? message = null;
// if the held item doesn't have SharpComponent
// and the user doesn't have SharpComponent
// disable the verb
if (!TryComp<SharpComponent>(args.Using, out var usingSharpComp) && userSharpComp == null)
{
disabled = true;
message = Loc.GetString("butcherable-need-knife",
("target", uid));
}
else if (_containerSystem.IsEntityInContainer(uid))
{
disabled = true;
message = Loc.GetString("butcherable-not-in-container",
("target", uid));
}
else if (TryComp<MobStateComponent>(uid, out var state) && !_mobStateSystem.IsDead(uid, state))
{
disabled = true;
message = Loc.GetString("butcherable-mob-isnt-dead");
}
// set the object doing the butchering to the item in the user's hands or to the user themselves
// if either has the SharpComponent
EntityUid sharpObject = default;
if (usingSharpComp != null)
sharpObject = args.Using!.Value;
else if (userSharpComp != null)
sharpObject = args.User;
InteractionVerb verb = new()
{
Act = () =>
{
if (!disabled)
TryStartButcherDoafter(sharpObject, args.Target, args.User);
},
Message = message,
Disabled = disabled,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/cutlery.svg.192dpi.png")),
Text = Loc.GetString("butcherable-verb-name"),
};
args.Verbs.Add(verb);
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
using System.Linq;
using Content.Server.Beam;
using Content.Server.Beam.Components;
using Content.Shared.Beam.Components;
using Content.Server.Lightning.Components;
using Content.Shared.Lightning;
using Robust.Server.GameObjects;
@@ -82,7 +82,7 @@ public sealed partial class LightningSystem : SharedLightningSystem
int shootedCount = 0;
int count = -1;
while(shootedCount < boltCount)
while (shootedCount < boltCount)
{
count++;
@@ -3,19 +3,17 @@ using Content.Server.Fluids.EntitySystems;
using Content.Server.Ghost;
using Content.Server.Popups;
using Content.Server.Stack;
using Content.Server.Wires;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Database;
using Content.Shared.Destructible;
using Content.Shared.Emag.Components;
using Content.Shared.Gibbing;
using Content.Shared.Humanoid;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Materials;
using Content.Shared.Mind;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Power;
using Content.Shared.Repairable;
using Content.Shared.Stacks;
@@ -23,9 +21,6 @@ using Robust.Server.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using System.Linq;
using Content.Shared.Gibbing;
using Content.Shared.Humanoid;
namespace Content.Server.Materials;
@@ -36,7 +31,6 @@ public sealed partial class MaterialReclaimerSystem : SharedMaterialReclaimerSys
[Dependency] private AppearanceSystem _appearance = default!;
[Dependency] private GhostSystem _ghostSystem = default!;
[Dependency] private MaterialStorageSystem _materialStorage = default!;
[Dependency] private OpenableSystem _openable = default!;
[Dependency] private PopupSystem _popup = default!;
[Dependency] private SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private GibbingSystem _gibbing = default!;
@@ -51,8 +45,6 @@ public sealed partial class MaterialReclaimerSystem : SharedMaterialReclaimerSys
base.Initialize();
SubscribeLocalEvent<MaterialReclaimerComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<MaterialReclaimerComponent, InteractUsingEvent>(OnInteractUsing,
before: [typeof(WiresSystem), typeof(SolutionTransferSystem)]);
SubscribeLocalEvent<MaterialReclaimerComponent, SuicideByEnvironmentEvent>(OnSuicideByEnvironment);
SubscribeLocalEvent<ActiveMaterialReclaimerComponent, PowerChangedEvent>(OnActivePowerChanged);
@@ -67,28 +59,6 @@ public sealed partial class MaterialReclaimerSystem : SharedMaterialReclaimerSys
Dirty(entity);
}
private void OnInteractUsing(Entity<MaterialReclaimerComponent> entity, ref InteractUsingEvent args)
{
if (args.Handled || entity.Comp.SolutionContainerId == null)
return;
// if we're trying to get a solution out of the reclaimer, don't destroy it
if (_solutionContainer.TryGetSolution(entity.Owner, entity.Comp.SolutionContainerId, out _, out var outputSolution) && outputSolution.Contents.Any())
{
if (_solutionContainer.EnumerateSolutions(args.Used).Any(s => s.Solution.Comp.Solution.AvailableVolume > 0))
{
if (_openable.IsClosed(args.Used))
return;
if (TryComp<SolutionTransferComponent>(args.Used, out var transfer) &&
transfer.CanSend)
return;
}
}
args.Handled = TryStartProcessItem(entity.Owner, args.Used, entity.Comp, args.User);
}
private void OnSuicideByEnvironment(Entity<MaterialReclaimerComponent> entity, ref SuicideByEnvironmentEvent args)
{
if (args.Handled)

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