diff --git a/.editorconfig b/.editorconfig index 3e44d1a281..370c3f9dee 100644 --- a/.editorconfig +++ b/.editorconfig @@ -72,7 +72,7 @@ csharp_style_expression_bodied_constructors = false:suggestion #csharp_style_expression_bodied_indexers = true:silent #csharp_style_expression_bodied_lambdas = true:silent #csharp_style_expression_bodied_local_functions = false:silent -csharp_style_expression_bodied_methods = false:suggestion +csharp_style_expression_bodied_methods = true:suggestion #csharp_style_expression_bodied_operators = false:silent csharp_style_expression_bodied_properties = true:suggestion diff --git a/Content.Client/Alerts/ClientAlertsSystem.cs b/Content.Client/Alerts/ClientAlertsSystem.cs index 223bf7876a..359c8957f9 100644 --- a/Content.Client/Alerts/ClientAlertsSystem.cs +++ b/Content.Client/Alerts/ClientAlertsSystem.cs @@ -91,7 +91,7 @@ private void OnPlayerDetached(EntityUid uid, AlertsComponent component, LocalPla ClearAlerts?.Invoke(this, EventArgs.Empty); } - public void AlertClicked(AlertType alertType) + public void AlertClicked(ProtoId alertType) { RaiseNetworkEvent(new ClickAlertEvent(alertType)); } diff --git a/Content.Client/Audio/AmbientSoundSystem.cs b/Content.Client/Audio/AmbientSoundSystem.cs index 0206017bae..f4e755a013 100644 --- a/Content.Client/Audio/AmbientSoundSystem.cs +++ b/Content.Client/Audio/AmbientSoundSystem.cs @@ -303,7 +303,11 @@ private void ProcessNearbyAmbience(TransformComponent playerXform) .WithMaxDistance(comp.Range); var stream = _audio.PlayEntity(comp.Sound, Filter.Local(), uid, false, audioParams); - _playingSounds[comp] = (stream.Value.Entity, comp.Sound, key); + + if (stream == null) + continue; + + _playingSounds[comp] = (stream!.Value.Entity, comp.Sound, key); playingCount++; if (_playingSounds.Count >= _maxAmbientCount) diff --git a/Content.Client/Audio/ClientGlobalSoundSystem.cs b/Content.Client/Audio/ClientGlobalSoundSystem.cs index 7c77865f74..6619285a96 100644 --- a/Content.Client/Audio/ClientGlobalSoundSystem.cs +++ b/Content.Client/Audio/ClientGlobalSoundSystem.cs @@ -67,7 +67,7 @@ private void PlayAdminSound(AdminSoundEvent soundEvent) if(!_adminAudioEnabled) return; var stream = _audio.PlayGlobal(soundEvent.Filename, Filter.Local(), false, soundEvent.AudioParams); - _adminAudio.Add(stream.Value.Entity); + _adminAudio.Add(stream!.Value.Entity); } private void PlayStationEventMusic(StationEventMusicEvent soundEvent) @@ -76,7 +76,7 @@ private void PlayStationEventMusic(StationEventMusicEvent soundEvent) if(!_eventAudioEnabled || _eventAudio.ContainsKey(soundEvent.Type)) return; var stream = _audio.PlayGlobal(soundEvent.Filename, Filter.Local(), false, soundEvent.AudioParams); - _eventAudio.Add(soundEvent.Type, stream.Value.Entity); + _eventAudio.Add(soundEvent.Type, stream!.Value.Entity); } private void PlayGameSound(GameGlobalSoundEvent soundEvent) diff --git a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs index 84b787a4ec..58cd026da4 100644 --- a/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs +++ b/Content.Client/Audio/ContentAudioSystem.AmbientMusic.cs @@ -210,7 +210,7 @@ private void UpdateAmbientMusic() track.ToString(), Filter.Local(), false, - AudioParams.Default.WithVolume(_musicProto.Sound.Params.Volume + _volumeSlider)); + AudioParams.Default.WithVolume(_musicProto.Sound.Params.Volume + _volumeSlider))!; _ambientMusicStream = strim.Value.Entity; diff --git a/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs b/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs index 92c5b7a419..60b81b1c2d 100644 --- a/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs +++ b/Content.Client/Audio/ContentAudioSystem.LobbyMusic.cs @@ -185,7 +185,11 @@ private void PlaySoundtrack(string soundtrackFilename) false, _lobbySoundtrackParams.WithVolume(_lobbySoundtrackParams.Volume + SharedAudioSystem.GainToVolume(_configManager.GetCVar(CCVars.LobbyMusicVolume))) ); - if (playResult.Value.Entity == default) + + if (playResult == null) + return; + + if (playResult!.Value.Entity == default) { _sawmill.Warning( $"Tried to play lobby soundtrack '{{Filename}}' using {nameof(SharedAudioSystem)}.{nameof(SharedAudioSystem.PlayGlobal)} but it returned default value of EntityUid!", diff --git a/Content.Client/Body/Components/BrainComponent.cs b/Content.Client/Body/Components/BrainComponent.cs new file mode 100644 index 0000000000..5ef9cea990 --- /dev/null +++ b/Content.Client/Body/Components/BrainComponent.cs @@ -0,0 +1,3 @@ +namespace Content.Client.Body.Components; +[RegisterComponent] +public sealed partial class BrainComponent : Component { } diff --git a/Content.Client/Body/Components/LungComponent.cs b/Content.Client/Body/Components/LungComponent.cs new file mode 100644 index 0000000000..71a19323b8 --- /dev/null +++ b/Content.Client/Body/Components/LungComponent.cs @@ -0,0 +1,3 @@ +namespace Content.Client.Body.Components; +[RegisterComponent] +public sealed partial class LungComponent : Component { } diff --git a/Content.Client/Body/Components/StomachComponent.cs b/Content.Client/Body/Components/StomachComponent.cs new file mode 100644 index 0000000000..fbc06ac7d7 --- /dev/null +++ b/Content.Client/Body/Components/StomachComponent.cs @@ -0,0 +1,3 @@ +namespace Content.Client.Body.Components; +[RegisterComponent] +public sealed partial class StomachComponent : Component { } diff --git a/Content.Client/Body/Systems/BodySystem.cs b/Content.Client/Body/Systems/BodySystem.cs index bab785525b..10dc057a8f 100644 --- a/Content.Client/Body/Systems/BodySystem.cs +++ b/Content.Client/Body/Systems/BodySystem.cs @@ -1,7 +1,72 @@ using Content.Shared.Body.Systems; +using Content.Shared.Body.Part; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Robust.Client.GameObjects; +using Robust.Shared.Utility; +using Content.Shared.Body.Components; namespace Content.Client.Body.Systems; public sealed class BodySystem : SharedBodySystem { + [Dependency] private readonly MarkingManager _markingManager = default!; + + private void ApplyMarkingToPart(MarkingPrototype markingPrototype, + IReadOnlyList? colors, + bool visible, + SpriteComponent sprite) + { + for (var j = 0; j < markingPrototype.Sprites.Count; j++) + { + var markingSprite = markingPrototype.Sprites[j]; + + if (markingSprite is not SpriteSpecifier.Rsi rsi) + continue; + + var layerId = $"{markingPrototype.ID}-{rsi.RsiState}"; + + if (!sprite.LayerMapTryGet(layerId, out _)) + { + var layer = sprite.AddLayer(markingSprite, j + 1); + sprite.LayerMapSet(layerId, layer); + sprite.LayerSetSprite(layerId, rsi); + } + + sprite.LayerSetVisible(layerId, visible); + + if (!visible) + continue; + + // Okay so if the marking prototype is modified but we load old marking data this may no longer be valid + // and we need to check the index is correct. So if that happens just default to white? + if (colors != null && j < colors.Count) + sprite.LayerSetColor(layerId, colors[j]); + else + sprite.LayerSetColor(layerId, Color.White); + } + } + + protected override void ApplyPartMarkings(EntityUid target, BodyPartAppearanceComponent component) + { + if (!TryComp(target, out SpriteComponent? sprite)) + return; + + if (component.Color != null) + sprite.Color = component.Color.Value; + + foreach (var (visualLayer, markingList) in component.Markings) + foreach (var marking in markingList) + { + if (!_markingManager.TryGetMarking(marking, out var markingPrototype)) + continue; + + ApplyMarkingToPart(markingPrototype, marking.MarkingColors, marking.Visible, sprite); + } + } + + protected override void RemoveBodyMarkings(EntityUid target, BodyPartAppearanceComponent partAppearance, HumanoidAppearanceComponent bodyAppearance) + { + return; + } } diff --git a/Content.Client/Buckle/BuckleSystem.cs b/Content.Client/Buckle/BuckleSystem.cs index d4614210d9..4429996aca 100644 --- a/Content.Client/Buckle/BuckleSystem.cs +++ b/Content.Client/Buckle/BuckleSystem.cs @@ -3,6 +3,7 @@ using Content.Shared.Buckle.Components; using Content.Shared.Rotation; using Robust.Client.GameObjects; +using Robust.Shared.GameStates; namespace Content.Client.Buckle; @@ -14,40 +15,63 @@ public override void Initialize() { base.Initialize(); - SubscribeLocalEvent(OnBuckleAfterAutoHandleState); + SubscribeLocalEvent(OnHandleState); SubscribeLocalEvent(OnAppearanceChange); + SubscribeLocalEvent(OnStrapMoveEvent); } - private void OnBuckleAfterAutoHandleState(EntityUid uid, BuckleComponent component, ref AfterAutoHandleStateEvent args) + private void OnStrapMoveEvent(EntityUid uid, StrapComponent component, ref MoveEvent args) { - ActionBlocker.UpdateCanMove(uid); + // I'm moving this to the client-side system, but for the sake of posterity let's keep this comment: + // > This is mega cursed. Please somebody save me from Mr Buckle's wild ride - if (!TryComp(uid, out var ownerSprite)) + // The nice thing is its still true, this is quite cursed, though maybe not omega cursed anymore. + // This code is garbage, it doesn't work with rotated viewports. I need to finally get around to reworking + // sprite rendering for entity layers & direction dependent sorting. + + if (args.NewRotation == args.OldRotation) return; - // Adjust draw depth when the chair faces north so that the seat back is drawn over the player. - // Reset the draw depth when rotated in any other direction. - // TODO when ECSing, make this a visualizer - // This code was written before rotatable viewports were introduced, so hard-coding Direction.North - // and comparing it against LocalRotation now breaks this in other rotations. This is a FIXME, but - // better to get it working for most people before we look at a more permanent solution. - if (component is { Buckled: true, LastEntityBuckledTo: { } } && - Transform(component.LastEntityBuckledTo.Value).LocalRotation.GetCardinalDir() == Direction.North && - TryComp(component.LastEntityBuckledTo, out var buckledSprite)) - { - component.OriginalDrawDepth ??= ownerSprite.DrawDepth; - ownerSprite.DrawDepth = buckledSprite.DrawDepth - 1; + if (!TryComp(uid, out var strapSprite)) return; - } - // If here, we're not turning north and should restore the saved draw depth. - if (component.OriginalDrawDepth.HasValue) + var isNorth = Transform(uid).LocalRotation.GetCardinalDir() == Direction.North; + foreach (var buckledEntity in component.BuckledEntities) { - ownerSprite.DrawDepth = component.OriginalDrawDepth.Value; - component.OriginalDrawDepth = null; + if (!TryComp(buckledEntity, out var buckle)) + continue; + + if (!TryComp(buckledEntity, out var buckledSprite)) + continue; + + if (isNorth) + { + buckle.OriginalDrawDepth ??= buckledSprite.DrawDepth; + buckledSprite.DrawDepth = strapSprite.DrawDepth - 1; + } + else if (buckle.OriginalDrawDepth.HasValue) + { + buckledSprite.DrawDepth = buckle.OriginalDrawDepth.Value; + buckle.OriginalDrawDepth = null; + } } } + private void OnHandleState(Entity ent, ref ComponentHandleState args) + { + if (args.Current is not BuckleState state) + return; + + ent.Comp.DontCollide = state.DontCollide; + ent.Comp.BuckleTime = state.BuckleTime; + var strapUid = EnsureEntity(state.BuckledTo, ent); + + SetBuckledTo(ent, strapUid == null ? null : new (strapUid.Value, null)); + + var (uid, component) = ent; + + } + private void OnAppearanceChange(EntityUid uid, BuckleComponent component, ref AppearanceChangeEvent args) { if (!TryComp(uid, out var rotVisuals) diff --git a/Content.Client/Clickable/ClickableComponent.cs b/Content.Client/Clickable/ClickableComponent.cs index 6f75df4683..da81ed4c84 100644 --- a/Content.Client/Clickable/ClickableComponent.cs +++ b/Content.Client/Clickable/ClickableComponent.cs @@ -1,145 +1,17 @@ -using System.Numerics; -using Robust.Client.GameObjects; -using Robust.Client.Graphics; -using Robust.Client.Utility; -using Robust.Shared.Graphics; -using static Robust.Client.GameObjects.SpriteComponent; -using Direction = Robust.Shared.Maths.Direction; +namespace Content.Client.Clickable; -namespace Content.Client.Clickable +[RegisterComponent] +public sealed partial class ClickableComponent : Component { - [RegisterComponent] - public sealed partial class ClickableComponent : Component - { - [Dependency] private readonly IClickMapManager _clickMapManager = default!; - - [DataField("bounds")] public DirBoundData? Bounds; - - /// - /// Used to check whether a click worked. Will first check if the click falls inside of some explicit bounding - /// boxes (see ). If that fails, attempts to use automatically generated click maps. - /// - /// The world position that was clicked. - /// - /// The draw depth for the sprite that captured the click. - /// - /// True if the click worked, false otherwise. - public bool CheckClick(SpriteComponent sprite, TransformComponent transform, EntityQuery xformQuery, Vector2 worldPos, IEye eye, out int drawDepth, out uint renderOrder, out float bottom) - { - if (!sprite.Visible) - { - drawDepth = default; - renderOrder = default; - bottom = default; - return false; - } - - drawDepth = sprite.DrawDepth; - renderOrder = sprite.RenderOrder; - var (spritePos, spriteRot) = transform.GetWorldPositionRotation(xformQuery); - var spriteBB = sprite.CalculateRotatedBoundingBox(spritePos, spriteRot, eye.Rotation); - bottom = Matrix3Helpers.CreateRotation(eye.Rotation).TransformBox(spriteBB).Bottom; - - Matrix3x2.Invert(sprite.GetLocalMatrix(), out var invSpriteMatrix); - - // This should have been the rotation of the sprite relative to the screen, but this is not the case with no-rot or directional sprites. - var relativeRotation = (spriteRot + eye.Rotation).Reduced().FlipPositive(); - - Angle cardinalSnapping = sprite.SnapCardinals ? relativeRotation.GetCardinalDir().ToAngle() : Angle.Zero; - - // First we get `localPos`, the clicked location in the sprite-coordinate frame. - var entityXform = Matrix3Helpers.CreateInverseTransform(transform.WorldPosition, sprite.NoRotation ? -eye.Rotation : spriteRot - cardinalSnapping); - var localPos = Vector2.Transform(Vector2.Transform(worldPos, entityXform), invSpriteMatrix); - - // Check explicitly defined click-able bounds - if (CheckDirBound(sprite, relativeRotation, localPos)) - return true; - - // Next check each individual sprite layer using automatically computed click maps. - foreach (var spriteLayer in sprite.AllLayers) - { - if (!spriteLayer.Visible || spriteLayer is not Layer layer) - continue; - - // Check the layer's texture, if it has one - if (layer.Texture != null) - { - // Convert to image coordinates - var imagePos = (Vector2i) (localPos * EyeManager.PixelsPerMeter * new Vector2(1, -1) + layer.Texture.Size / 2f); - - if (_clickMapManager.IsOccluding(layer.Texture, imagePos)) - return true; - } - - // Either we weren't clicking on the texture, or there wasn't one. In which case: check the RSI next - if (layer.ActualRsi is not { } rsi || !rsi.TryGetState(layer.State, out var rsiState)) - continue; - - var dir = Layer.GetDirection(rsiState.RsiDirections, relativeRotation); + [DataField] public DirBoundData? Bounds; - // convert to layer-local coordinates - layer.GetLayerDrawMatrix(dir, out var matrix); - Matrix3x2.Invert(matrix, out var inverseMatrix); - var layerLocal = Vector2.Transform(localPos, inverseMatrix); - - // Convert to image coordinates - var layerImagePos = (Vector2i) (layerLocal * EyeManager.PixelsPerMeter * new Vector2(1, -1) + rsiState.Size / 2f); - - // Next, to get the right click map we need the "direction" of this layer that is actually being used to draw the sprite on the screen. - // This **can** differ from the dir defined before, but can also just be the same. - if (sprite.EnableDirectionOverride) - dir = sprite.DirectionOverride.Convert(rsiState.RsiDirections); - dir = dir.OffsetRsiDir(layer.DirOffset); - - if (_clickMapManager.IsOccluding(layer.ActualRsi!, layer.State, dir, layer.AnimationFrame, layerImagePos)) - return true; - } - - drawDepth = default; - renderOrder = default; - bottom = default; - return false; - } - - public bool CheckDirBound(SpriteComponent sprite, Angle relativeRotation, Vector2 localPos) - { - if (Bounds == null) - return false; - - // These explicit bounds only work for either 1 or 4 directional sprites. - - // This would be the orientation of a 4-directional sprite. - var direction = relativeRotation.GetCardinalDir(); - - var modLocalPos = sprite.NoRotation - ? localPos - : direction.ToAngle().RotateVec(localPos); - - // First, check the bounding box that is valid for all orientations - if (Bounds.All.Contains(modLocalPos)) - return true; - - // Next, get and check the appropriate bounding box for the current sprite orientation - var boundsForDir = (sprite.EnableDirectionOverride ? sprite.DirectionOverride : direction) switch - { - Direction.East => Bounds.East, - Direction.North => Bounds.North, - Direction.South => Bounds.South, - Direction.West => Bounds.West, - _ => throw new InvalidOperationException() - }; - - return boundsForDir.Contains(modLocalPos); - } - - [DataDefinition] - public sealed partial class DirBoundData - { - [DataField("all")] public Box2 All; - [DataField("north")] public Box2 North; - [DataField("south")] public Box2 South; - [DataField("east")] public Box2 East; - [DataField("west")] public Box2 West; - } + [DataDefinition] + public sealed partial class DirBoundData + { + [DataField] public Box2 All; + [DataField] public Box2 North; + [DataField] public Box2 South; + [DataField] public Box2 East; + [DataField] public Box2 West; } } diff --git a/Content.Client/Clickable/ClickableSystem.cs b/Content.Client/Clickable/ClickableSystem.cs new file mode 100644 index 0000000000..15d13df625 --- /dev/null +++ b/Content.Client/Clickable/ClickableSystem.cs @@ -0,0 +1,168 @@ +using System.Numerics; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Utility; +using Robust.Shared.Graphics; + +namespace Content.Client.Clickable; + +/// +/// Handles click detection for sprites. +/// +public sealed class ClickableSystem : EntitySystem +{ + [Dependency] private readonly IClickMapManager _clickMapManager = default!; + [Dependency] private readonly SharedTransformSystem _transforms = default!; + [Dependency] private readonly SpriteSystem _sprites = default!; + + private EntityQuery _clickableQuery; + private EntityQuery _xformQuery; + + public override void Initialize() + { + base.Initialize(); + _clickableQuery = GetEntityQuery(); + _xformQuery = GetEntityQuery(); + } + + /// + /// Used to check whether a click worked. Will first check if the click falls inside of some explicit bounding + /// boxes (see ). If that fails, attempts to use automatically generated click maps. + /// + /// The world position that was clicked. + /// + /// The draw depth for the sprite that captured the click. + /// + /// True if the click worked, false otherwise. + public bool CheckClick(Entity entity, Vector2 worldPos, IEye eye, out int drawDepth, out uint renderOrder, out float bottom) + { + if (!_clickableQuery.Resolve(entity.Owner, ref entity.Comp1, false)) + { + drawDepth = default; + renderOrder = default; + bottom = default; + return false; + } + + if (!_xformQuery.Resolve(entity.Owner, ref entity.Comp3)) + { + drawDepth = default; + renderOrder = default; + bottom = default; + return false; + } + + var sprite = entity.Comp2; + var transform = entity.Comp3; + + if (!sprite.Visible) + { + drawDepth = default; + renderOrder = default; + bottom = default; + return false; + } + + drawDepth = sprite.DrawDepth; + renderOrder = sprite.RenderOrder; + var (spritePos, spriteRot) = _transforms.GetWorldPositionRotation(transform); + var spriteBB = sprite.CalculateRotatedBoundingBox(spritePos, spriteRot, eye.Rotation); + bottom = Matrix3Helpers.CreateRotation(eye.Rotation).TransformBox(spriteBB).Bottom; + + Matrix3x2.Invert(sprite.GetLocalMatrix(), out var invSpriteMatrix); + + // This should have been the rotation of the sprite relative to the screen, but this is not the case with no-rot or directional sprites. + var relativeRotation = (spriteRot + eye.Rotation).Reduced().FlipPositive(); + + var cardinalSnapping = sprite.SnapCardinals ? relativeRotation.GetCardinalDir().ToAngle() : Angle.Zero; + + // First we get `localPos`, the clicked location in the sprite-coordinate frame. + var entityXform = Matrix3Helpers.CreateInverseTransform(spritePos, sprite.NoRotation ? -eye.Rotation : spriteRot - cardinalSnapping); + var localPos = Vector2.Transform(Vector2.Transform(worldPos, entityXform), invSpriteMatrix); + + // Check explicitly defined click-able bounds + if (CheckDirBound((entity.Owner, entity.Comp1, entity.Comp2), relativeRotation, localPos)) + return true; + + // Next check each individual sprite layer using automatically computed click maps. + foreach (var spriteLayer in sprite.AllLayers) + { + if (spriteLayer is not SpriteComponent.Layer layer || !_sprites.IsVisible(layer)) + { + continue; + } + + // Check the layer's texture, if it has one + if (layer.Texture != null) + { + // Convert to image coordinates + var imagePos = (Vector2i) (localPos * EyeManager.PixelsPerMeter * new Vector2(1, -1) + layer.Texture.Size / 2f); + + if (_clickMapManager.IsOccluding(layer.Texture, imagePos)) + return true; + } + + // Either we weren't clicking on the texture, or there wasn't one. In which case: check the RSI next + if (layer.ActualRsi is not { } rsi || !rsi.TryGetState(layer.State, out var rsiState)) + continue; + + var dir = SpriteComponent.Layer.GetDirection(rsiState.RsiDirections, relativeRotation); + + // convert to layer-local coordinates + layer.GetLayerDrawMatrix(dir, out var matrix); + Matrix3x2.Invert(matrix, out var inverseMatrix); + var layerLocal = Vector2.Transform(localPos, inverseMatrix); + + // Convert to image coordinates + var layerImagePos = (Vector2i) (layerLocal * EyeManager.PixelsPerMeter * new Vector2(1, -1) + rsiState.Size / 2f); + + // Next, to get the right click map we need the "direction" of this layer that is actually being used to draw the sprite on the screen. + // This **can** differ from the dir defined before, but can also just be the same. + if (sprite.EnableDirectionOverride) + dir = sprite.DirectionOverride.Convert(rsiState.RsiDirections); + dir = dir.OffsetRsiDir(layer.DirOffset); + + if (_clickMapManager.IsOccluding(layer.ActualRsi!, layer.State, dir, layer.AnimationFrame, layerImagePos)) + return true; + } + + drawDepth = default; + renderOrder = default; + bottom = default; + return false; + } + + public bool CheckDirBound(Entity entity, Angle relativeRotation, Vector2 localPos) + { + var clickable = entity.Comp1; + var sprite = entity.Comp2; + + if (clickable.Bounds == null) + return false; + + // These explicit bounds only work for either 1 or 4 directional sprites. + + // This would be the orientation of a 4-directional sprite. + var direction = relativeRotation.GetCardinalDir(); + + var modLocalPos = sprite.NoRotation + ? localPos + : direction.ToAngle().RotateVec(localPos); + + // First, check the bounding box that is valid for all orientations + if (clickable.Bounds.All.Contains(modLocalPos)) + return true; + + // Next, get and check the appropriate bounding box for the current sprite orientation + var boundsForDir = (sprite.EnableDirectionOverride ? sprite.DirectionOverride : direction) switch + { + Direction.East => clickable.Bounds.East, + Direction.North => clickable.Bounds.North, + Direction.South => clickable.Bounds.South, + Direction.West => clickable.Bounds.West, + _ => throw new InvalidOperationException() + }; + + return boundsForDir.Contains(modLocalPos); + } +} diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index 1ab213ae0a..bf5f021be3 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -8,7 +8,6 @@ using Content.Client.Fullscreen; using Content.Client.GhostKick; using Content.Client.Guidebook; -using Content.Client.Info; using Content.Client.Input; using Content.Client.IoC; using Content.Client.Launcher; @@ -54,7 +53,6 @@ public sealed class EntryPoint : GameClient [Dependency] private readonly IScreenshotHook _screenshotHook = default!; [Dependency] private readonly FullscreenHook _fullscreenHook = default!; [Dependency] private readonly ChangelogManager _changelogManager = default!; - [Dependency] private readonly RulesManager _rulesManager = default!; [Dependency] private readonly ViewportManager _viewportManager = default!; [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; [Dependency] private readonly IInputManager _inputManager = default!; @@ -131,7 +129,6 @@ public override void Init() _screenshotHook.Initialize(); _fullscreenHook.Initialize(); _changelogManager.Initialize(); - _rulesManager.Initialize(); _viewportManager.Initialize(); _ghostKick.Initialize(); _extendedDisconnectInformation.Initialize(); diff --git a/Content.Client/Explosion/ExplosionOverlay.cs b/Content.Client/Explosion/ExplosionOverlay.cs index 8cf7447a5d..f8f8ae5362 100644 --- a/Content.Client/Explosion/ExplosionOverlay.cs +++ b/Content.Client/Explosion/ExplosionOverlay.cs @@ -29,6 +29,7 @@ public ExplosionOverlay() protected override void Draw(in OverlayDrawArgs args) { + var appearanceSystem = _entMan.System(); var drawHandle = args.WorldHandle; drawHandle.UseShader(_shader); @@ -41,7 +42,7 @@ protected override void Draw(in OverlayDrawArgs args) if (visuals.Epicenter.MapId != args.MapId) continue; - if (!appearance.TryGetData(ExplosionAppearanceData.Progress, out int index)) + if (!appearanceSystem.TryGetData(appearance.Owner, ExplosionAppearanceData.Progress, out int index)) continue; index = Math.Min(index, visuals.Intensity.Count - 1); diff --git a/Content.Client/Gameplay/GameplayStateBase.cs b/Content.Client/Gameplay/GameplayStateBase.cs index 6236cd8e95..315c053d93 100644 --- a/Content.Client/Gameplay/GameplayStateBase.cs +++ b/Content.Client/Gameplay/GameplayStateBase.cs @@ -2,6 +2,7 @@ using System.Numerics; using Content.Client.Clickable; using Content.Client.UserInterface; +using Content.Client.Viewport; using Content.Shared.Input; using Robust.Client.ComponentTrees; using Robust.Client.GameObjects; @@ -13,11 +14,13 @@ using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.CustomControls; using Robust.Shared.Console; +using Robust.Shared.Graphics; using Robust.Shared.Input; using Robust.Shared.Input.Binding; using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Timing; +using YamlDotNet.Serialization.TypeInspectors; namespace Content.Client.Gameplay { @@ -98,7 +101,15 @@ private bool HandleInspect(ICommonSession? session, EntityCoordinates coords, En public EntityUid? GetClickedEntity(MapCoordinates coordinates) { - var first = GetClickableEntities(coordinates).FirstOrDefault(); + return GetClickedEntity(coordinates, _eyeManager.CurrentEye); + } + + public EntityUid? GetClickedEntity(MapCoordinates coordinates, IEye? eye) + { + if (eye == null) + return null; + + var first = GetClickableEntities(coordinates, eye).FirstOrDefault(); return first.IsValid() ? first : null; } @@ -109,6 +120,20 @@ public IEnumerable GetClickableEntities(EntityCoordinates coordinates public IEnumerable GetClickableEntities(MapCoordinates coordinates) { + return GetClickableEntities(coordinates, _eyeManager.CurrentEye); + } + + public IEnumerable GetClickableEntities(MapCoordinates coordinates, IEye? eye) + { + /* + * TODO: + * 1. Stuff like MeleeWeaponSystem need an easy way to hook into viewport specific entities / entities under mouse + * 2. Cleanup the mess around InteractionOutlineSystem + below the keybind click detection. + */ + + if (eye == null) + return Array.Empty(); + // Find all the entities intersecting our click var spriteTree = _entityManager.EntitySysManager.GetEntitySystem(); var entities = spriteTree.QueryAabb(coordinates.MapId, Box2.CenteredAround(coordinates.Position, new Vector2(1, 1))); @@ -116,15 +141,12 @@ public IEnumerable GetClickableEntities(MapCoordinates coordinates) // 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(); - var xformQuery = _entityManager.GetEntityQuery(); - - // TODO: Smelly - var eye = _eyeManager.CurrentEye; + var clickables = _entityManager.System(); foreach (var entity in entities) { if (clickQuery.TryGetComponent(entity.Uid, out var component) && - component.CheckClick(entity.Component, entity.Transform, xformQuery, coordinates.Position, eye, out var drawDepthClicked, out var renderOrder, out var bottom)) + clickables.CheckClick((entity.Uid, component, entity.Component, entity.Transform), coordinates.Position, eye, out var drawDepthClicked, out var renderOrder, out var bottom)) { foundEntities.Add((entity.Uid, drawDepthClicked, renderOrder, bottom)); } @@ -187,7 +209,15 @@ protected virtual void OnKeyBindStateChanged(ViewportBoundKeyEventArgs args) if (args.Viewport is IViewportControl vp) { var mousePosWorld = vp.PixelToMap(kArgs.PointerLocation.Position); - entityToClick = GetClickedEntity(mousePosWorld); + + if (vp is ScalingViewport svp) + { + entityToClick = GetClickedEntity(mousePosWorld, svp.Eye); + } + else + { + entityToClick = GetClickedEntity(mousePosWorld); + } coordinates = _mapManager.TryFindGridAt(mousePosWorld, out _, out var grid) ? grid.MapToGrid(mousePosWorld) : diff --git a/Content.Client/Guidebook/Controls/GuidebookWindow.xaml b/Content.Client/Guidebook/Controls/GuidebookWindow.xaml index 8dbfde3c47..cc6cc6e82b 100644 --- a/Content.Client/Guidebook/Controls/GuidebookWindow.xaml +++ b/Content.Client/Guidebook/Controls/GuidebookWindow.xaml @@ -18,7 +18,7 @@ Name="SearchBar" PlaceHolder="{Loc 'guidebook-filter-placeholder-text'}" HorizontalExpand="True" - Margin="0 5 10 5"> + Margin="0 5 10 5"> diff --git a/Content.Client/Guidebook/DocumentParsingManager.cs b/Content.Client/Guidebook/DocumentParsingManager.cs index b81866a626..9c9e569eb4 100644 --- a/Content.Client/Guidebook/DocumentParsingManager.cs +++ b/Content.Client/Guidebook/DocumentParsingManager.cs @@ -2,6 +2,8 @@ using Content.Client.Guidebook.Richtext; using Pidgin; using Robust.Client.UserInterface; +using Robust.Shared.ContentPack; +using Robust.Shared.Prototypes; using Robust.Shared.Reflection; using Robust.Shared.Sandboxing; using static Pidgin.Parser; @@ -13,7 +15,9 @@ namespace Content.Client.Guidebook; /// public sealed partial class DocumentParsingManager { + [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly IReflectionManager _reflectionManager = default!; + [Dependency] private readonly IResourceManager _resourceManager = default!; [Dependency] private readonly ISandboxHelper _sandboxHelper = default!; private readonly Dictionary> _tagControlParsers = new(); @@ -37,6 +41,21 @@ public void Initialize() ControlParser = SkipWhitespaces.Then(_controlParser.Many()); } + public bool TryAddMarkup(Control control, ProtoId entryId, bool log = true) + { + if (!_prototype.TryIndex(entryId, out var entry)) + return false; + + using var file = _resourceManager.ContentFileReadText(entry.Text); + return TryAddMarkup(control, file.ReadToEnd(), log); + } + + public bool TryAddMarkup(Control control, GuideEntry entry, bool log = true) + { + using var file = _resourceManager.ContentFileReadText(entry.Text); + return TryAddMarkup(control, file.ReadToEnd(), log); + } + public bool TryAddMarkup(Control control, string text, bool log = true) { try diff --git a/Content.Client/Hands/Systems/HandsSystem.cs b/Content.Client/Hands/Systems/HandsSystem.cs index 7319b97b42..7ea3b69de5 100644 --- a/Content.Client/Hands/Systems/HandsSystem.cs +++ b/Content.Client/Hands/Systems/HandsSystem.cs @@ -3,6 +3,7 @@ using Content.Client.Examine; using Content.Client.Strip; using Content.Client.Verbs.UI; +using Content.Shared.Body.Part; using Content.Shared.Hands; using Content.Shared.Hands.Components; using Content.Shared.Hands.EntitySystems; @@ -38,7 +39,6 @@ public sealed class HandsSystem : SharedHandsSystem public event Action? OnPlayerItemRemoved; public event Action? OnPlayerHandBlocked; public event Action? OnPlayerHandUnblocked; - public override void Initialize() { base.Initialize(); @@ -49,6 +49,8 @@ public override void Initialize() SubscribeLocalEvent(OnHandsShutdown); SubscribeLocalEvent(HandleComponentState); SubscribeLocalEvent(OnVisualsChanged); + SubscribeLocalEvent(HandleBodyPartRemoved); + SubscribeLocalEvent(HandleBodyPartDisabled); OnHandSetActive += OnHandActivated; } @@ -236,8 +238,38 @@ public void UIHandAltActivateItem(string handName) RaisePredictiveEvent(new RequestHandAltInteractEvent(handName)); } + #region pulling + + #endregion + #region visuals + private void HideLayers(EntityUid uid, HandsComponent component, Entity part, SpriteComponent? sprite = null) + { + if (part.Comp.PartType != BodyPartType.Hand || !Resolve(uid, ref sprite, logMissing: false)) + return; + + var location = part.Comp.Symmetry switch + { + BodyPartSymmetry.None => HandLocation.Middle, + BodyPartSymmetry.Left => HandLocation.Left, + BodyPartSymmetry.Right => HandLocation.Right, + _ => throw new ArgumentOutOfRangeException(nameof(part.Comp.Symmetry)) + }; + + if (component.RevealedLayers.TryGetValue(location, out var revealedLayers)) + { + foreach (var key in revealedLayers) + sprite.RemoveLayer(key); + + revealedLayers.Clear(); + } + } + + private void HandleBodyPartRemoved(EntityUid uid, HandsComponent component, ref BodyPartRemovedEvent args) => HideLayers(uid, component, args.Part); + + private void HandleBodyPartDisabled(EntityUid uid, HandsComponent component, ref BodyPartDisabledEvent args) => HideLayers(uid, component, args.Part); + protected override void HandleEntityInserted(EntityUid uid, HandsComponent hands, EntInsertedIntoContainerMessage args) { base.HandleEntityInserted(uid, hands, args); @@ -262,6 +294,7 @@ protected override void HandleEntityRemoved(EntityUid uid, HandsComponent hands, if (!hands.Hands.TryGetValue(args.Container.ID, out var hand)) return; + UpdateHandVisuals(uid, args.Entity, hand); _stripSys.UpdateUi(uid); diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerBoundUserInterface.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerBoundUserInterface.cs index dc0a3e9fcc..39bb52d72c 100644 --- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerBoundUserInterface.cs +++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerBoundUserInterface.cs @@ -1,4 +1,5 @@ using Content.Shared.MedicalScanner; +using Content.Shared.Targeting; using JetBrains.Annotations; using Robust.Client.GameObjects; @@ -22,6 +23,7 @@ protected override void Open() Title = EntMan.GetComponent(Owner).EntityName, }; _window.OnClose += Close; + _window.OnBodyPartSelected += SendBodyPartMessage; _window.OpenCentered(); } @@ -36,6 +38,8 @@ protected override void ReceiveMessage(BoundUserInterfaceMessage message) _window.Populate(cast); } + private void SendBodyPartMessage(TargetBodyPart? part, EntityUid target) => SendMessage(new HealthAnalyzerPartMessage(EntMan.GetNetEntity(target), part ?? null)); + protected override void Dispose(bool disposing) { base.Dispose(disposing); @@ -43,7 +47,10 @@ protected override void Dispose(bool disposing) return; if (_window != null) + { _window.OnClose -= Close; + _window.OnBodyPartSelected -= SendBodyPartMessage; + } _window?.Dispose(); } diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml index e070af95d8..0a0b5ac89e 100644 --- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml +++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml @@ -2,7 +2,7 @@ xmlns="https://spacestation14.io" xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls" MaxHeight="525" - MinWidth="300"> + MinWidth="350">