mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-06-09 12:06:35 +02:00
merge remote wizden/master
This commit is contained in:
@@ -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,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
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+16
-8
@@ -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;
|
||||
}
|
||||
}
|
||||
+15
-9
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user