Files
space-station-14/Content.Client/SubFloor/TrayScannerSystem.cs
T
chromiumboy 2ca5d9d81a Disposals refactor (#40540)
* Initial commit

* Added pods

* Transit tubes should no longer connect with disposal pipes

* Updated the transit tube sprites

* Revisited how the transit tubes work

* Fixing merge conflict

* Updated sprites

* Simplified disposal component collections and updated yaml

* Moved the majority of the disposals code to Shared

* Move disposed ents via physics

* Disposal unit now use generic visualizers

* Followers of disposed entities have predicted movement

* Made BeingDisposedSystem shared

* Added documentation for some components

* Code clean up

* Removed unneeded container entries, tubes use directions instead of angles

* Many EntityUid/Comp -> Entity<Comp> changes

* Updated DisposableSystem

* Review of DisposalTubeSystem

* Review of DisposalUnitSystem

* Review of DisposalSignalRouterSystem

* Unit updates

* Fixing merge conflict

* Merge conflict resolved

* Re-organization of systems and components

* Allow trapped entities to escape endless loops

* Fix for item ejection

* UI updates

* Minor tweak

* Fixes for transit tubes

* Removed placeholder sprite

* Removed transit tubes (for now)

* Moved exit stun time to DisposalHolderComponent

* Added a limit on the amount of damage that can sustained from disposals travel

* Fix for PVS-related sound issues

* Added a limit on the number of entities that can be inserted into units

* Clean up

* Undid file scoping

* Fixed test fail

* Fixed test fail

* Increased min default capacity of disposal units

* Fixed audio spam occurring when dumping multiple items into a disposal unit

* Tile prying now occurs whenever exiting an open pipe

* Prevent attacking and interactions while traveling through disposals

* Added generic verbs for entering/exiting disposal units

* Fixed UI prediction bug

* Bug fixes

* Small improvements

* Moved the disposal holder prototype from the disposal entry to the unit

* Minor clean up

* Added support for a disposal holder despawn effect

* Added prediction guard

* Test fail fix

* Re-added new escape behavior

* Removed outdated PVS code

* Clean up

* More escape behavior changes

* More clean up

* Updated exit selection logic

* Bug fix

* More clean up

* Split up disposal unit code

* Cleaned up disposal units some more

* Adjusted exit throw distance

* Capitalized disposal and mailing unit window titles

* Bug fixes

* Taggers and routers show existing tags when the UI opens

* Fixed test fail

* test fail fix

* Minor performance improvement

* Minor improvement in pathing

* merge conflicts

* Fix heisentest

* Fixed mispredict

* Updated system referencing conventions

* Updated to account for changes made to the code

* Attempting to fix submodule issue

* Addressing test fails

* Additional fixes for tests

* Addressing reviewer comments

* Cleaned up exit randomization

* T-ray can see disposal pipes when in 'pipe' mode

* Fix for flush animation flickering

* mild thing

* fix both

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
2026-05-27 04:38:03 +00:00

244 lines
9.1 KiB
C#

using Content.Client.Items;
using Content.Client.Items.UI;
using Content.Client.Message;
using Content.Client.Power.Visualizers;
using Content.Client.Stylesheets;
using Content.Shared.Atmos.Components;
using Content.Shared.Disposal.Tube;
using Content.Shared.Input;
using Content.Shared.Inventory;
using Content.Shared.SubFloor;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Timing;
namespace Content.Client.SubFloor;
public sealed partial class TrayScannerSystem : SharedTrayScannerSystem
{
[Dependency] private IGameTiming _timing = default!;
[Dependency] private IPlayerManager _player = default!;
[Dependency] private AnimationPlayerSystem _animation = default!;
[Dependency] private EntityLookupSystem _lookup = default!;
[Dependency] private InventorySystem _inventory = default!;
[Dependency] private SharedAppearanceSystem _appearance = default!;
[Dependency] private SharedTransformSystem _transform = default!;
[Dependency] private SpriteSystem _sprite = default!;
[Dependency] private TrayScanRevealSystem _trayScanReveal = default!;
[Dependency] private IInputManager _inputManager = default!;
[Dependency] private EntityQuery<TrayScannerComponent> _trayScannerQuery = default!;
[Dependency] private EntityQuery<SubFloorHideComponent> _subFloorHideQuery = default!;
private const string TRayAnimationKey = "trays";
private const double AnimationLength = 0.3;
public const LookupFlags Flags = LookupFlags.Static | LookupFlags.Sundries | LookupFlags.Approximate;
public override void Initialize()
{
base.Initialize();
Subs.ItemStatus<TrayScannerComponent>(OnCollectItemStatus);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
if (!_timing.IsFirstTimePredicted)
return;
// TODO: Multiple viewports or w/e
var player = _player.LocalEntity;
if (!TryComp(player, out TransformComponent? playerXform))
return;
var playerPos = _transform.GetWorldPosition(playerXform);
var playerMap = playerXform.MapID;
var range = 0f;
var mode = TrayScannerMode.All;
HashSet<Entity<SubFloorHideComponent>> inRange;
// TODO: Should probably sub to player attached changes / inventory changes but inventory's
// API is extremely skrungly. If this ever shows up on dottrace ping me and laugh.
var canSee = false;
foreach (var item in _inventory.GetHandOrInventoryEntities(player.Value, SlotFlags.POCKET))
{
if (!_trayScannerQuery.TryGetComponent(item, out var scanner) || !scanner.Enabled)
continue;
range = MathF.Max(scanner.Range, range);
mode = scanner.Mode;
canSee = true;
break;
}
inRange = new HashSet<Entity<SubFloorHideComponent>>();
if (canSee)
{
var entitiesInRange = new HashSet<Entity<SubFloorHideComponent>>();
_lookup.GetEntitiesInRange(playerMap, playerPos, range, entitiesInRange, flags: Flags);
foreach (var (uid, comp) in entitiesInRange)
{
if (!MatchesMode(uid, mode))
continue;
inRange.Add((uid, comp));
if (comp.IsUnderCover || _trayScanReveal.IsUnderRevealingEntity(uid))
EnsureComp<TrayRevealedComponent>(uid);
}
}
var revealedQuery = AllEntityQuery<TrayRevealedComponent, SpriteComponent>();
while (revealedQuery.MoveNext(out var uid, out _, out var sprite))
{
// Revealing
// Add buffer range to avoid flickers.
if (_subFloorHideQuery.TryGetComponent(uid, out var subfloor) &&
inRange.Contains((uid, subfloor)))
{
// Due to the fact client is predicting this server states will reset it constantly
if ((!_appearance.TryGetData(uid, SubFloorVisuals.ScannerRevealed, out bool value) || !value) &&
sprite.Color.A > SubfloorRevealAlpha)
{
_sprite.SetColor((uid, sprite), sprite.Color.WithAlpha(0f));
}
SetRevealed(uid, true);
if (sprite.Color.A >= SubfloorRevealAlpha || _animation.HasRunningAnimation(uid, TRayAnimationKey))
continue;
_animation.Play(uid, new Animation()
{
Length = TimeSpan.FromSeconds(AnimationLength),
AnimationTracks =
{
new AnimationTrackComponentProperty()
{
ComponentType = typeof(SpriteComponent),
Property = nameof(SpriteComponent.Color),
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(sprite.Color.WithAlpha(0f), 0f),
new AnimationTrackProperty.KeyFrame(sprite.Color.WithAlpha(SubfloorRevealAlpha), (float) AnimationLength)
}
}
}
}, TRayAnimationKey);
}
// Hiding
else
{
// Hidden completely so unreveal and reset the alpha.
if (sprite.Color.A <= 0f)
{
SetRevealed(uid, false);
RemCompDeferred<TrayRevealedComponent>(uid);
_sprite.SetColor((uid, sprite), sprite.Color.WithAlpha(1f));
continue;
}
SetRevealed(uid, true);
if (_animation.HasRunningAnimation(uid, TRayAnimationKey))
continue;
_animation.Play(uid, new Animation()
{
Length = TimeSpan.FromSeconds(AnimationLength),
AnimationTracks =
{
new AnimationTrackComponentProperty()
{
ComponentType = typeof(SpriteComponent),
Property = nameof(SpriteComponent.Color),
KeyFrames =
{
new AnimationTrackProperty.KeyFrame(sprite.Color, 0f),
new AnimationTrackProperty.KeyFrame(sprite.Color.WithAlpha(0f), (float) AnimationLength)
}
}
}
}, TRayAnimationKey);
}
}
}
private void SetRevealed(EntityUid uid, bool value)
{
_appearance.SetData(uid, SubFloorVisuals.ScannerRevealed, value);
}
private bool MatchesMode(EntityUid uid, TrayScannerMode mode)
{
return mode switch
{
TrayScannerMode.All => true,
TrayScannerMode.Wiring => HasComp<CableVisualizerComponent>(uid),
TrayScannerMode.Piping => HasComp<AtmosPipeLayersComponent>(uid) || HasComp<DisposalTubeComponent>(uid),
_ => false,
};
}
#region UI
private Control OnCollectItemStatus(Entity<TrayScannerComponent> entity)
{
_inputManager.TryGetKeyBinding((ContentKeyFunctions.AltUseItemInHand), out var binding);
return new StatusControl(entity, binding?.GetKeyString() ?? "");
}
private sealed class StatusControl : PollingItemStatusControl<StatusControl.TRayData>
{
private readonly RichTextLabel _label;
private readonly TrayScannerComponent _scanner;
private readonly string _keyBindingName;
public StatusControl(TrayScannerComponent scanner, string keyBindingName)
{
_scanner = scanner;
_keyBindingName = keyBindingName;
_label = new RichTextLabel { StyleClasses = { StyleClass.ItemStatus } };
AddChild(_label);
}
protected override TRayData PollData()
{
return new TRayData(_scanner.Enabled, _scanner.Mode);
}
protected override void Update(in TRayData data)
{
if (!data.Enabled)
{
_label.SetMarkup(string.Empty);
return;
}
var modeLocString = data.Mode switch
{
TrayScannerMode.All => "tray-scanner-examine-mode-all",
TrayScannerMode.Wiring => "tray-scanner-examine-mode-wiring",
TrayScannerMode.Piping => "tray-scanner-examine-mode-piping",
_ => "",
};
_label.SetMarkup(Robust.Shared.Localization.Loc.GetString("tray-scanner-item-status-label",
("mode", Robust.Shared.Localization.Loc.GetString(modeLocString)),
("keybinding", _keyBindingName)));
}
public readonly record struct TRayData(bool Enabled, TrayScannerMode Mode);
}
#endregion
}