diff --git a/OpenDreamClient/ClientVerbSystem.cs b/OpenDreamClient/ClientVerbSystem.cs index f92212d5d5..2ae6dee122 100644 --- a/OpenDreamClient/ClientVerbSystem.cs +++ b/OpenDreamClient/ClientVerbSystem.cs @@ -18,6 +18,7 @@ public sealed class ClientVerbSystem : VerbSystem { [Dependency] private readonly ITaskManager _taskManager = default!; [Dependency] private readonly ITimerManager _timerManager = default!; [Dependency] private readonly IOverlayManager _overlayManager = default!; + [Dependency] private readonly DreamClientSystem _dreamClientSystem = default!; [Dependency] private readonly TransformSystem _transformSystem = default!; private EntityQuery _spriteQuery; @@ -83,8 +84,10 @@ public IEnumerable GetAllVerbs() { /// The ID, src, and information of every executable verb public IEnumerable<(int Id, ClientObjectReference Src, VerbInfo VerbInfo)> GetExecutableVerbs(bool ignoreHiddenAttr = false) { sbyte? seeInvisibility = null; - if (_playerManager.LocalEntity != null) { - _sightQuery.TryGetComponent(_playerManager.LocalEntity.Value, out var mobSight); + + EntityUid mob = _dreamClientSystem.MobUid; + if (mob.IsValid()) { + _sightQuery.TryGetComponent(mob, out var mobSight); seeInvisibility = mobSight?.SeeInvisibility; } @@ -120,12 +123,12 @@ public IEnumerable GetAllVerbs() { // Check the verb's "set src" allows us to execute this switch (verb.Accessibility) { case VerbAccessibility.Usr: - if (entity != _playerManager.LocalEntity) + if (entity != mob) continue; break; case VerbAccessibility.InUsr: - if (_transformSystem.GetParentUid(entity) != _playerManager.LocalEntity) + if (_transformSystem.GetParentUid(entity) != mob) continue; break; diff --git a/OpenDreamClient/DreamClientSystem.cs b/OpenDreamClient/DreamClientSystem.cs index a2d1c288ae..ec2cadbcd0 100644 --- a/OpenDreamClient/DreamClientSystem.cs +++ b/OpenDreamClient/DreamClientSystem.cs @@ -1,10 +1,57 @@ using OpenDreamClient.Interface; +using OpenDreamClient.Rendering; +using OpenDreamShared.Dream; +using OpenDreamShared.Network.Messages; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; using Robust.Shared.Player; namespace OpenDreamClient; internal sealed class DreamClientSystem : EntitySystem { + [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IDreamInterfaceManager _interfaceManager = default!; + [Dependency] private readonly IEyeManager _eyeManager = default!; + [Dependency] private readonly TransformSystem _transformSystem = default!; + + // Current NetEntityof player's mob, or Invalid if could not be determined. + private NetEntity _mobNet = NetEntity.Invalid; + + // Current Entity of player's mob, or Invalid if could not be determined. + private EntityUid _mobUid = EntityUid.Invalid; + + // Sometimes we get mob info before we know all the net entities, so store the net entity and refer to it + public EntityUid MobUid { + get { + // if entity of mob is invalid but net entity isn't, try referring to known net entities + if (! _mobUid.IsValid() && _mobNet.IsValid() && _entityManager.TryGetEntity(_mobNet, out var ent)) { + _mobUid = ent.GetValueOrDefault(EntityUid.Invalid); + } + + return _mobUid; + } + } + + // Current Entity of player's eye, or Invalid if could not be determined. + private ClientObjectReference _eyeRef = new(NetEntity.Invalid); + + public ClientObjectReference EyeRef { + get { + if (_eyeRef.Type == ClientObjectReference.RefType.Entity && !_eyeRef.Entity.IsValid()) { + return new(_entityManager.GetNetEntity(MobUid)); + } else { + return _eyeRef; + } + } + private set { + _eyeManager.CurrentEye = new DreamClientEye(_eyeManager.CurrentEye, value, _entityManager, _transformSystem); + _eyeRef = value; + } + } + + public bool IsEyeMissing() { + return EyeRef.Type == ClientObjectReference.RefType.Client; + } public override void Initialize() { SubscribeLocalEvent(OnPlayerAttached); @@ -15,4 +62,22 @@ private void OnPlayerAttached(LocalPlayerAttachedEvent e) { // So we have to set it again _interfaceManager.DefaultWindow?.Macro.SetActive(); } + + public void RxNotifyMobEyeUpdate(MsgNotifyMobEyeUpdate msg) { + var prevMobNet = _mobNet; + _mobNet = msg.MobNetEntity; + + if (prevMobNet != _mobNet) { + // mark mob cache as dirty/invalid + _mobUid = EntityUid.Invalid; + } + + var incomingEyeRef = msg.EyeRef; + + if (incomingEyeRef.Type == ClientObjectReference.RefType.Entity && !incomingEyeRef.Entity.IsValid()) { + EyeRef = new(msg.MobNetEntity); + } else { + EyeRef = incomingEyeRef; + } + } } diff --git a/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs b/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs index 3e90159083..19a90d5fc9 100644 --- a/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs +++ b/OpenDreamClient/Input/ContextMenu/ContextMenuPopup.xaml.cs @@ -19,6 +19,7 @@ internal sealed partial class ContextMenuPopup : Popup { [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; [Dependency] private readonly IUserInterfaceManager _uiManager = default!; private readonly ClientAppearanceSystem _appearanceSystem; + private readonly DreamClientSystem _dreamClientSystem; private readonly ClientVerbSystem _verbSystem; private readonly DMISpriteSystem _spriteSystem; private readonly EntityLookupSystem _lookupSystem; @@ -37,6 +38,7 @@ public ContextMenuPopup() { _verbSystem = _entitySystemManager.GetEntitySystem(); _appearanceSystem = _entitySystemManager.GetEntitySystem(); + _dreamClientSystem = _entitySystemManager.GetEntitySystem(); _spriteSystem = _entitySystemManager.GetEntitySystem(); _lookupSystem = _entitySystemManager.GetEntitySystem(); _mouseInputSystem = _entitySystemManager.GetEntitySystem(); @@ -115,7 +117,7 @@ public void SetActiveItem(ContextMenuItem item) { private sbyte GetSeeInvisible() { if (_playerManager.LocalSession == null) return 127; - if (!_mobSightQuery.TryGetComponent(_playerManager.LocalSession.AttachedEntity, out DreamMobSightComponent? sight)) + if (!_mobSightQuery.TryGetComponent(_dreamClientSystem.MobUid, out DreamMobSightComponent? sight)) return 127; return sight.SeeInvisibility; diff --git a/OpenDreamClient/Input/MouseInputSystem.cs b/OpenDreamClient/Input/MouseInputSystem.cs index 0d6375f346..85daeaffef 100644 --- a/OpenDreamClient/Input/MouseInputSystem.cs +++ b/OpenDreamClient/Input/MouseInputSystem.cs @@ -27,6 +27,7 @@ internal sealed class MouseInputSystem : SharedMouseInputSystem { [Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly IDreamInterfaceManager _dreamInterfaceManager = default!; [Dependency] private readonly ClientAppearanceSystem _appearanceSystem = default!; + [Dependency] private readonly DreamClientSystem _dreamClientSystem = default!; [Dependency] private readonly IClyde _clyde = default!; [Dependency] private readonly ILogManager _logManager = default!; private ISawmill _sawmill = default!; @@ -144,6 +145,11 @@ public void HandleAtomMouseMove(ScalingViewport viewport, Vector2 relativePos, C } public (ClientObjectReference Atom, Vector2i IconPosition)? GetTurfUnderMouse(MapCoordinates mapCoords, out uint? turfId) { + if (_dreamClientSystem.IsEyeMissing()) { + turfId = null; + return null; + } + // Grid coordinates are half a meter off from entity coordinates mapCoords = new MapCoordinates(mapCoords.Position + new Vector2(0.5f), mapCoords.MapId); diff --git a/OpenDreamClient/Interface/DreamInterfaceManager.cs b/OpenDreamClient/Interface/DreamInterfaceManager.cs index a1465bfd13..9889946ac5 100644 --- a/OpenDreamClient/Interface/DreamInterfaceManager.cs +++ b/OpenDreamClient/Interface/DreamInterfaceManager.cs @@ -1,28 +1,28 @@ -using System.IO; -using System.Text; -using System.Globalization; -using OpenDreamShared.Network.Messages; -using OpenDreamClient.Interface.Controls; -using OpenDreamShared.Interface.Descriptors; -using OpenDreamShared.Interface.DMF; +using OpenDreamClient.Interface.Controls; using OpenDreamClient.Interface.Prompts; using OpenDreamClient.Resources; using OpenDreamClient.Resources.ResourceTypes; using OpenDreamShared.Dream; +using OpenDreamShared.Interface.Descriptors; +using OpenDreamShared.Interface.DMF; +using OpenDreamShared.Network.Messages; using Robust.Client; using Robust.Client.Graphics; using Robust.Client.Input; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.ContentPack; +using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Random; using Robust.Shared.Serialization.Manager; using Robust.Shared.Timing; using Robust.Shared.Utility; using SixLabors.ImageSharp; +using System.Globalization; +using System.IO; using System.Linq; -using Robust.Shared.Map; +using System.Text; namespace OpenDreamClient.Interface; @@ -131,9 +131,14 @@ public void Initialize() { _netManager.RegisterNetMessage(RxLoadInterface); _netManager.RegisterNetMessage(); _netManager.RegisterNetMessage(RxUpdateClientInfo); + _netManager.RegisterNetMessage(RxNotifyMobEyeUpdate); _clyde.OnWindowFocused += OnWindowFocused; } + private void RxNotifyMobEyeUpdate(MsgNotifyMobEyeUpdate message) { + _entitySystemManager.GetEntitySystem().RxNotifyMobEyeUpdate(message); + } + private void RxUpdateStatPanels(MsgUpdateStatPanels message) { DefaultInfo?.UpdateStatPanels(message); } diff --git a/OpenDreamClient/Rendering/DreamClientEye.cs b/OpenDreamClient/Rendering/DreamClientEye.cs new file mode 100644 index 0000000000..17e7b5e267 --- /dev/null +++ b/OpenDreamClient/Rendering/DreamClientEye.cs @@ -0,0 +1,101 @@ +using OpenDreamShared.Dream; +using Robust.Client.GameObjects; +using Robust.Shared.Graphics; +using Robust.Shared.Map; + +namespace OpenDreamClient.Rendering; + +public sealed class DreamClientEye: IEye { + private readonly ClientObjectReference _eyeRef; + private readonly IEntityManager _entityManager; + private readonly TransformSystem _transformSystem; + + public DreamClientEye(IEye baseEye, ClientObjectReference eyeRef, IEntityManager entityManager, TransformSystem transformSystem) { + Rotation = baseEye.Rotation; + Scale = baseEye.Scale; + DrawFov = baseEye.DrawFov; + DrawLight = baseEye.DrawLight; + + _eyeRef = eyeRef; + _entityManager = entityManager; + _transformSystem = transformSystem; + } + + [ViewVariables(VVAccess.ReadOnly)] + public MapCoordinates Position { + get { + switch (_eyeRef.Type) { + case ClientObjectReference.RefType.Entity: + var ent = _entityManager.GetEntity(_eyeRef.Entity); + if (_entityManager.TryGetComponent(ent, out var pos)) { + return _transformSystem.GetMapCoordinates(pos); + } + + break; + + case ClientObjectReference.RefType.Turf: + return new(new(_eyeRef.TurfX, _eyeRef.TurfY), new(_eyeRef.TurfZ)); + } + + // Nullspace position stops all rendering but we still want to render certain screen space objects... + //return MapCoordinates.Nullspace; + return new(0, 0, new(1)); + } + } + + public void GetViewMatrix(out Matrix3x2 viewMatrix, Vector2 renderScale) { + var pos = Position; + viewMatrix = Matrix3Helpers.CreateInverseTransform( + pos.X + Offset.X, + pos.Y + Offset.Y, + (float)-Rotation.Theta, + 1 / (Scale.X * renderScale.X), + 1 / (Scale.Y * renderScale.Y)); + } + + public void GetViewMatrixNoOffset(out Matrix3x2 viewMatrix, Vector2 renderScale) { + var pos = Position; + viewMatrix = Matrix3Helpers.CreateInverseTransform( + pos.X, + pos.Y, + (float)-Rotation.Theta, + 1 / (Scale.X * renderScale.X), + 1 / (Scale.Y * renderScale.Y)); + } + + public void GetViewMatrixInv(out Matrix3x2 viewMatrixInv, Vector2 renderScale) { + var pos = Position; + viewMatrixInv = Matrix3Helpers.CreateTransform( + pos.X + Offset.X, + pos.Y + Offset.Y, + (float)-Rotation.Theta, + 1 / (Scale.X * renderScale.X), + 1 / (Scale.Y * renderScale.Y)); + } + + private Vector2 _scale = Vector2.One / 2f; + + [ViewVariables(VVAccess.ReadWrite)] + public bool DrawFov { get; set; } + + [ViewVariables] + public bool DrawLight { get; set; } + + [ViewVariables(VVAccess.ReadWrite)] + public Vector2 Offset { get; set; } + + [ViewVariables(VVAccess.ReadWrite)] + public Angle Rotation { get; set; } + + [ViewVariables(VVAccess.ReadWrite)] + public Vector2 Zoom { + get => new(1 / _scale.X, 1 / _scale.Y); + set => _scale = new Vector2(1 / value.X, 1 / value.Y); + } + + [ViewVariables(VVAccess.ReadWrite)] + public Vector2 Scale { + get => _scale; + set => _scale = value; + } +} diff --git a/OpenDreamClient/Rendering/DreamPlane.cs b/OpenDreamClient/Rendering/DreamPlane.cs index e0a89d5d2c..2de04cf29a 100644 --- a/OpenDreamClient/Rendering/DreamPlane.cs +++ b/OpenDreamClient/Rendering/DreamPlane.cs @@ -83,12 +83,15 @@ public void Draw(DreamViewOverlay overlay, DrawingHandleWorld handle, Box2 world /// /// Draws this plane's mouse map onto the current render target /// - public void DrawMouseMap(DrawingHandleWorld handle, DreamViewOverlay overlay, Vector2i renderTargetSize, Box2 worldAABB) { + public void DrawMouseMap(DrawingHandleWorld handle, DreamViewOverlay overlay, Vector2i renderTargetSize, Box2 worldAABB, bool onlyDrawScreenSprites = false) { if (Master?.MouseOpacity == MouseOpacity.Transparent || !Enabled) return; handle.UseShader(overlay.BlockColorInstance); foreach (var sprite in Sprites) { + if (onlyDrawScreenSprites && !sprite.IsScreen) + continue; + if (sprite.MouseOpacity == MouseOpacity.Transparent || sprite.ShouldPassMouse) continue; diff --git a/OpenDreamClient/Rendering/DreamViewOverlay.cs b/OpenDreamClient/Rendering/DreamViewOverlay.cs index cb4bd8e3e7..b1aa6f8c8c 100644 --- a/OpenDreamClient/Rendering/DreamViewOverlay.cs +++ b/OpenDreamClient/Rendering/DreamViewOverlay.cs @@ -1,20 +1,20 @@ -using System.Linq; -using OpenDreamClient.Interface; -using Robust.Client.Graphics; -using Robust.Client.Player; -using Robust.Shared.Map; +using OpenDreamClient.Interface; +using OpenDreamClient.Rendering.Particles; using OpenDreamShared.Dream; -using Robust.Shared.Console; -using Robust.Shared.Prototypes; using OpenDreamShared.Rendering; -using OpenDreamClient.Rendering.Particles; using Robust.Client.GameObjects; -using Robust.Shared.Map.Components; -using Robust.Shared.Profiling; -using Matrix3x2 = System.Numerics.Matrix3x2; +using Robust.Client.Graphics; +using Robust.Client.Player; using Robust.Client.ResourceManagement; using Robust.Client.UserInterface.RichText; +using Robust.Shared.Console; using Robust.Shared.Enums; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Profiling; +using Robust.Shared.Prototypes; +using System.Linq; +using Matrix3x2 = System.Numerics.Matrix3x2; namespace OpenDreamClient.Rendering; @@ -60,6 +60,7 @@ internal sealed partial class DreamViewOverlay : Overlay { private readonly ClientScreenOverlaySystem _screenOverlaySystem; private readonly ClientImagesSystem _imagesSystem; private readonly DMISpriteSystem _spriteSystem; + private readonly DreamClientSystem _dreamClientSystem; private readonly EntityQuery _spriteQuery; private readonly EntityQuery _xformQuery; @@ -90,6 +91,7 @@ public DreamViewOverlay(RenderTargetPool renderTargetPool) { _screenOverlaySystem = _entitySystemManager.GetEntitySystem(); _imagesSystem = _entitySystemManager.GetEntitySystem(); _spriteSystem = _entitySystemManager.GetEntitySystem(); + _dreamClientSystem = _entitySystemManager.GetEntitySystem(); _spriteQuery = _entityManager.GetEntityQuery(); _xformQuery = _entityManager.GetEntityQuery(); @@ -120,15 +122,21 @@ public DreamViewOverlay(RenderTargetPool renderTargetPool) { protected override void Draw(in OverlayDrawArgs args) { using var _ = _prof.Group("Dream View Overlay"); - EntityUid? eye = _playerManager.LocalSession?.AttachedEntity; - if (eye == null) + var eyeRef = _dreamClientSystem.EyeRef; + if (eyeRef.Type == ClientObjectReference.RefType.Entity && !eyeRef.Entity.IsValid()) { return; + } + + EntityUid mob = _dreamClientSystem.MobUid; + if (!mob.IsValid()) { + return; + } //Main drawing of sprites happens here try { var viewportSize = (Vector2i)(args.Viewport.Size / args.Viewport.RenderScale); - DrawAll(args, eye.Value, viewportSize); + DrawAll(args, mob, eyeRef, viewportSize); } catch (Exception e) { _sawmill.Error($"Error occurred while rendering frame. Error details:\n{e.Message}\n{e.StackTrace}"); } @@ -145,25 +153,48 @@ protected override void Draw(in OverlayDrawArgs args) { _rendererMetaDataRental.Push(_rendererMetaDataToReturn.Pop()); } - private void DrawAll(OverlayDrawArgs args, EntityUid eye, Vector2i viewportSize) { - if (!_xformQuery.TryGetComponent(eye, out var eyeTransform)) - return; + private void DrawAll(OverlayDrawArgs args, EntityUid mob, ClientObjectReference eyeRef, Vector2i viewportSize) { + MapCoordinates eyeCoords; + DreamMobSightComponent? eyeSight, mobSight; + Box2 worldAABB = args.WorldAABB; + sbyte seeVis; + + EntitiesInView.Clear(); + + switch (eyeRef.Type) { + default: + _mobSightQuery.TryGetComponent(mob, out mobSight); + seeVis = mobSight?.SeeInvisibility ?? 127; + DrawNullEyeSprites(args, viewportSize, seeVis); + return; + case ClientObjectReference.RefType.Turf: + eyeCoords = new(new(eyeRef.TurfX, eyeRef.TurfY), new(eyeRef.TurfZ)); + _mobSightQuery.TryGetComponent(mob, out eyeSight); + break; + case ClientObjectReference.RefType.Entity: + var eyeUid = _entityManager.GetEntity(eyeRef.Entity); + if (!_xformQuery.TryGetComponent(eyeUid, out var eyeTransform)) + return; + eyeCoords = _transformSystem.GetMapCoordinates(eyeUid, eyeTransform); + _mobSightQuery.TryGetComponent(eyeUid, out eyeSight); + break; + } + + MapId mapId = eyeCoords.MapId; - var eyeCoords = _transformSystem.GetMapCoordinates(eye, eyeTransform); if (!_mapManager.TryFindGridAt(eyeCoords, out var gridUid, out var grid)) return; - _mobSightQuery.TryGetComponent(eye, out var mobSight); - var seeVis = mobSight?.SeeInvisibility ?? 127; - var sight = mobSight?.Sight ?? 0; + _mobSightQuery.TryGetComponent(mob, out mobSight); + seeVis = mobSight?.SeeInvisibility ?? 127; + var sight = eyeSight?.Sight ?? 0; var worldHandle = args.WorldHandle; - var worldAABB = args.WorldAABB; using (_prof.Group("lookup")) { //TODO use a sprite tree. //the scaling is to attempt to prevent pop-in, by getting sprites that are *just* offscreen - _lookupSystem.GetEntitiesIntersecting(args.MapId, worldAABB.Scale(1.2f), EntitiesInView, MapLookupFlags); + _lookupSystem.GetEntitiesIntersecting(mapId, worldAABB.Scale(1.2f), EntitiesInView, MapLookupFlags); } var eyeTile = _mapSystem.GetTileRef(gridUid, grid, eyeCoords); @@ -173,7 +204,7 @@ private void DrawAll(OverlayDrawArgs args, EntityUid eye, Vector2i viewportSize) CollectVisibleSprites(tiles, gridUid, grid, eyeTile, seeVis, sight, worldAABB); ClearPlanes(); - ProcessSprites(worldHandle, viewportSize, worldAABB); + ProcessSprites(worldHandle, viewportSize); //Final draw DrawPlanes(worldHandle, worldAABB); @@ -184,6 +215,26 @@ private void DrawAll(OverlayDrawArgs args, EntityUid eye, Vector2i viewportSize) args.WorldAABB.BottomLeft); } + // Only draw sprites in screen space and also on non-negative planes + private void DrawNullEyeSprites(OverlayDrawArgs args, Vector2i viewportSize, sbyte seeVis) { + RefreshRenderTargets(args.WorldHandle, viewportSize); + + if (ScreenOverlayEnabled) { + CollectScreenSpaceSprites(seeVis, args.WorldAABB); + } + + ClearPlanes(); + ProcessSprites(args.WorldHandle, viewportSize, true); + + //Final draw + DrawPlanes(args.WorldHandle, args.WorldAABB); + + //At this point all the sprites have been rendered to the base target, now we just draw it to the viewport! + args.WorldHandle.DrawTexture( + MouseMapRenderEnabled ? _mouseMapRenderTarget!.Texture : _baseRenderTarget!.Texture, + args.WorldAABB.BottomLeft); + } + //handles underlays, overlays, appearance flags, images. Adds them to the result list, so they can be sorted and drawn with DrawIcon() private void ProcessIconComponents(DreamIcon icon, Vector2 position, EntityUid uid, bool isScreen, ref int tieBreaker, List result, sbyte seeVis, RendererMetaData? parentIcon = null, bool keepTogether = false, Vector3? turfCoords = null, ClientAppearanceSystem.Flick? flick = null) { if (icon.Appearance is null) //in the event that appearance hasn't loaded yet @@ -530,11 +581,14 @@ private DreamPlane GetPlane(int planeIndex, Vector2i viewportSize) { return plane; } - private void ProcessSprites(DrawingHandleWorld handle, Vector2i viewportSize, Box2 worldAABB) { + private void ProcessSprites(DrawingHandleWorld handle, Vector2i viewportSize, bool skipNegativePlanes = false) { using var _ = _prof.Group("process sprites / draw render targets"); //all sprites with render targets get handled first - these are ordered by sprites.Sort(), so we can just iterate normally foreach (var sprite in _spriteContainer) { + if (skipNegativePlanes && sprite.Plane < 0) + continue; + var plane = GetPlane(sprite.Plane, viewportSize); if (!string.IsNullOrEmpty(sprite.RenderTarget)) { @@ -672,26 +726,7 @@ private void CollectVisibleSprites(ViewAlgorithm.Tile?[,] tiles, EntityUid gridU // Screen objects if (ScreenOverlayEnabled) { - using var _ = _prof.Group("screen objects"); - - foreach (EntityUid uid in _screenOverlaySystem.ScreenObjects) { - if (!_entityManager.TryGetComponent(uid, out DMISpriteComponent? sprite) || sprite.ScreenLocation == null) - continue; - if (!_spriteSystem.IsVisible(sprite, null, seeVis, null)) - continue; - if (sprite.ScreenLocation.MapControl != null) // Don't render screen objects meant for other map controls - continue; - - Vector2i dmiIconSize = sprite.Icon.DMI?.IconSize ?? new(IconSize, IconSize); - Vector2 position = sprite.ScreenLocation.GetViewPosition(worldAABB.BottomLeft, _interfaceManager.View, IconSize, dmiIconSize); - Vector2 iconSize = sprite.Icon.DMI == null ? Vector2.Zero : sprite.Icon.DMI.IconSize / (float)IconSize; - for (int x = 0; x < sprite.ScreenLocation.RepeatX; x++) { - for (int y = 0; y < sprite.ScreenLocation.RepeatY; y++) { - tValue = 0; - ProcessIconComponents(sprite.Icon, position + iconSize * new Vector2(x, y), uid, true, ref tValue, _spriteContainer, seeVis); - } - } - } + CollectScreenSpaceSprites(seeVis, worldAABB); } using (_prof.Group("sort sprites")) { @@ -699,6 +734,28 @@ private void CollectVisibleSprites(ViewAlgorithm.Tile?[,] tiles, EntityUid gridU } } + private void CollectScreenSpaceSprites(sbyte seeVis, Box2 worldAABB) { + using var _ = _prof.Group("screen objects"); + foreach (EntityUid uid in _screenOverlaySystem.ScreenObjects) { + if (!_entityManager.TryGetComponent(uid, out DMISpriteComponent? sprite) || sprite.ScreenLocation == null) + continue; + if (!_spriteSystem.IsVisible(sprite, null, seeVis, null)) + continue; + if (sprite.ScreenLocation.MapControl != null) // Don't render screen objects meant for other map controls + continue; + + Vector2i dmiIconSize = sprite.Icon.DMI?.IconSize ?? new(IconSize, IconSize); + Vector2 position = sprite.ScreenLocation.GetViewPosition(worldAABB.BottomLeft, _interfaceManager.View, IconSize, dmiIconSize); + Vector2 iconSize = sprite.Icon.DMI == null ? Vector2.Zero : sprite.Icon.DMI.IconSize / (float)IconSize; + for (int x = 0; x < sprite.ScreenLocation.RepeatX; x++) { + for (int y = 0; y < sprite.ScreenLocation.RepeatY; y++) { + int tValue = 0; + ProcessIconComponents(sprite.Icon, position + iconSize * new Vector2(x, y), uid, true, ref tValue, _spriteContainer, seeVis); + } + } + } + } + private RendererMetaData RentRendererMetaData() { RendererMetaData result; if (_rendererMetaDataRental.Count == 0) diff --git a/OpenDreamRuntime/DreamConnection.cs b/OpenDreamRuntime/DreamConnection.cs index f21fda33c5..c818351a34 100644 --- a/OpenDreamRuntime/DreamConnection.cs +++ b/OpenDreamRuntime/DreamConnection.cs @@ -1,3 +1,4 @@ + using System.Threading.Tasks; using System.Web; using DMCompiler.Bytecode; @@ -19,6 +20,7 @@ public sealed class DreamConnection { [Dependency] private readonly DreamObjectTree _objectTree = default!; [Dependency] private readonly DreamResourceManager _resourceManager = default!; [Dependency] private readonly WalkManager _walkManager = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; [Dependency] private readonly ISharedPlayerManager _playerManager = default!; @@ -48,11 +50,25 @@ [ViewVariables] public DreamObjectMob? Mob { } StatObj = new(value); - if (Eye != null && Eye == oldMob) { - Eye = value; + + // If eye is equivalent to old mob, and the new mob is different from old mob, set eye to new mob. + if (oldMob != null) { + var oldMobNetEntity = _entityManager.GetNetEntity(oldMob.Entity); + if (Eye != null && Eye.Value.Entity == oldMobNetEntity) { + if (_mob == null) { + _eye = null; + } else { + var newMobNetEntity = _entityManager.GetNetEntity(_mob.Entity); + _eye = new(newMobNetEntity); + } + } } + UpdateMobEye(); + if (_mob != null) { + _playerManager.SetAttachedEntity(Session!, _mob.Entity); + // If the mob is already owned by another player, kick them out if (_mob.Connection != null) _mob.Connection.Mob = null; @@ -65,11 +81,13 @@ [ViewVariables] public DreamObjectMob? Mob { } } - [ViewVariables] public DreamObjectMovable? Eye { + private ClientObjectReference? _eye; + + [ViewVariables] public ClientObjectReference? Eye { get => _eye; set { - _eye = value; - _playerManager.SetAttachedEntity(Session!, _eye?.Entity); + _eye = value; + UpdateMobEye(); } } @@ -82,7 +100,6 @@ [ViewVariables] public DreamObjectMovable? Eye { [ViewVariables] private int _nextPromptEvent = 1; private readonly Dictionary _permittedBrowseRscFiles = new(); private DreamObjectMob? _mob; - private DreamObjectMovable? _eye; private readonly ISawmill _sawmill = Logger.GetSawmill("opendream.connection"); @@ -552,4 +569,29 @@ public bool TryConvertPromptResponse(DreamValueType type, object? value, out Dre converted = default; return false; } + + private void UpdateMobEye() { + var mobUid = Mob?.Entity ?? EntityUid.Invalid; + ClientObjectReference eyeRef; + switch (Eye?.Type) { + default: + eyeRef = new(_entityManager.GetNetEntity(mobUid)); + break; + case null: + eyeRef = new(); + break; + case ClientObjectReference.RefType.Entity when Eye.HasValue: + eyeRef = new(_entityManager.GetNetEntity(new(Eye.Value.Entity.Id))); + break; + case ClientObjectReference.RefType.Turf: + eyeRef = new(new(Eye.Value.TurfX, Eye.Value.TurfY), Eye.Value.TurfZ); + break; + } + + var msg = new MsgNotifyMobEyeUpdate() { + MobNetEntity = _entityManager.GetNetEntity(mobUid), + EyeRef = eyeRef + }; + Session?.Channel.SendMessage(msg); + } } diff --git a/OpenDreamRuntime/DreamManager.Connections.cs b/OpenDreamRuntime/DreamManager.Connections.cs index 9778fda616..c4dbd39090 100644 --- a/OpenDreamRuntime/DreamManager.Connections.cs +++ b/OpenDreamRuntime/DreamManager.Connections.cs @@ -76,6 +76,7 @@ private void InitializeConnectionManager() { _netManager.RegisterNetMessage(); _netManager.RegisterNetMessage(); _netManager.RegisterNetMessage(); + _netManager.RegisterNetMessage(); var topicPort = _config.GetCVar(OpenDreamCVars.TopicPort); var worldTopicAddress = new IPEndPoint(IPAddress.Loopback, topicPort); diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectClient.cs b/OpenDreamRuntime/Objects/Types/DreamObjectClient.cs index 52fd7cdb11..ec6755f7cb 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectClient.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectClient.cs @@ -1,9 +1,9 @@ -using System.Security.Cryptography; -using System.Text; -using OpenDreamRuntime.Procs.Native; +using OpenDreamRuntime.Procs.Native; using OpenDreamRuntime.Rendering; using OpenDreamRuntime.Resources; using OpenDreamShared.Dream; +using System.Security.Cryptography; +using System.Text; namespace OpenDreamRuntime.Objects.Types; @@ -55,7 +55,12 @@ protected override bool TryGetVar(string varName, out DreamValue value) { value = Connection.StatObj; return true; case "eye": - value = new(Connection.Eye); + if (Connection.Eye == null) { + value = DreamValue.Null; + return true; + } + + value = new(DreamManager.GetFromClientReference(Connection, Connection.Eye.Value)); return true; case "view": // Number if square & centerable, string representation otherwise @@ -124,11 +129,19 @@ protected override void SetVar(string varName, DreamValue value) { break; case "eye": { value.TryGetValueAsDreamObject(out var newEye); - if (newEye is not (DreamObjectMovable or null)) { - throw new Exception($"Cannot set eye to non-movable {value}"); // TODO: You can set it to a turf + switch (newEye) { + case DreamObjectMovable movable: + Connection.Eye = new(EntityManager.GetNetEntity(movable.Entity)); + break; + case DreamObjectTurf turf: + Connection.Eye = new(new(turf.X, turf.Y), turf.Z); + break; + case null: + Connection.Eye = null; + break; + default: + throw new Exception($"Cannot set eye to non-movable, non-turf {value}"); } - - Connection.Eye = newEye as DreamObjectMovable; break; } case "view": { diff --git a/OpenDreamShared/Network/Messages/MsgNotifyMobEyeUpdate.cs b/OpenDreamShared/Network/Messages/MsgNotifyMobEyeUpdate.cs new file mode 100644 index 0000000000..b4283c3131 --- /dev/null +++ b/OpenDreamShared/Network/Messages/MsgNotifyMobEyeUpdate.cs @@ -0,0 +1,55 @@ +using Lidgren.Network; +using OpenDreamShared.Dream; +using Robust.Shared.GameObjects; +using Robust.Shared.Network; +using Robust.Shared.Serialization; +using static OpenDreamShared.Dream.ClientObjectReference; + +namespace OpenDreamShared.Network.Messages; + +//Server -> Client: Tell the client about the current entity UIDs of its mob and eye +public sealed class MsgNotifyMobEyeUpdate : NetMessage { + public override MsgGroups MsgGroup => MsgGroups.EntityEvent; + + public NetEntity MobNetEntity; + + // Type = Client -> null + public ClientObjectReference EyeRef; + + public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer) { + MobNetEntity = new(buffer.ReadInt32()); + + RefType eyeType = (RefType)buffer.ReadInt32(); + switch (eyeType) { + case RefType.Entity: + EyeRef = new(new NetEntity(buffer.ReadInt32())); + break; + case RefType.Turf: + int x = buffer.ReadInt32(); + int y = buffer.ReadInt32(); + int z = buffer.ReadInt32(); + EyeRef = new(new(x, y), z); + break; + default: + // null eye + EyeRef = new(); + break; + } + } + + public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { + buffer.Write(MobNetEntity.Id); + + buffer.Write((int)EyeRef.Type); + switch (EyeRef.Type) { + case RefType.Entity: + buffer.Write(EyeRef.Entity.Id); + break; + case RefType.Turf: + buffer.Write(EyeRef.TurfX); + buffer.Write(EyeRef.TurfY); + buffer.Write(EyeRef.TurfZ); + break; + } + } +}