diff --git a/DMCompiler/DMStandard/Types/Atoms/_Atom.dm b/DMCompiler/DMStandard/Types/Atoms/_Atom.dm index 2168de66b1..7f647c8345 100644 --- a/DMCompiler/DMStandard/Types/Atoms/_Atom.dm +++ b/DMCompiler/DMStandard/Types/Atoms/_Atom.dm @@ -56,9 +56,9 @@ var/step_x as opendream_unimplemented var/step_y as opendream_unimplemented var/render_source - var/tmp/mouse_drag_pointer as opendream_unimplemented - var/tmp/mouse_drop_pointer as opendream_unimplemented - var/tmp/mouse_over_pointer as opendream_unimplemented + var/tmp/mouse_drag_pointer + var/tmp/mouse_drop_pointer + var/tmp/mouse_over_pointer var/render_target var/vis_flags as opendream_unimplemented diff --git a/DMCompiler/DMStandard/Types/Client.dm b/DMCompiler/DMStandard/Types/Client.dm index c875e82f23..742d47142b 100644 --- a/DMCompiler/DMStandard/Types/Client.dm +++ b/DMCompiler/DMStandard/Types/Client.dm @@ -40,7 +40,7 @@ var/script as opendream_unimplemented var/color = 0 as opendream_unimplemented var/control_freak as opendream_unimplemented - var/mouse_pointer_icon as opendream_unimplemented + var/mouse_pointer_icon var/preload_rsc = 1 as opendream_unimplemented var/fps = 0 as opendream_unimplemented var/dir = NORTH as opendream_unimplemented diff --git a/DMCompiler/DMStandard/Types/Image.dm b/DMCompiler/DMStandard/Types/Image.dm index a237bd2646..e8ccd1d648 100644 --- a/DMCompiler/DMStandard/Types/Image.dm +++ b/DMCompiler/DMStandard/Types/Image.dm @@ -22,10 +22,10 @@ var/maptext_height = 32 var/maptext_x = 0 var/maptext_y = 0 - var/mouse_over_pointer = 0 as opendream_unimplemented - var/mouse_drag_pointer = 0 as opendream_unimplemented - var/mouse_drop_pointer = 1 as opendream_unimplemented - var/mouse_drop_zone = 0 as opendream_unimplemented + var/mouse_over_pointer = 0 + var/mouse_drag_pointer = 0 + var/mouse_drop_pointer = 1 + var/mouse_drop_zone = 0 var/mouse_opacity = 1 var/name = "image" var/opacity = 0 as opendream_unimplemented diff --git a/OpenDreamClient/Input/MouseInputSystem.cs b/OpenDreamClient/Input/MouseInputSystem.cs index 1264c436eb..d10c9a92e8 100644 --- a/OpenDreamClient/Input/MouseInputSystem.cs +++ b/OpenDreamClient/Input/MouseInputSystem.cs @@ -28,6 +28,11 @@ 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 IClyde _clyde = default!; + [Dependency] private readonly ILogManager _logManager = default!; + private ISawmill _sawmill = default!; + + public bool IsDragging { get => _selectedEntity?.IsDrag ?? false; } private DreamViewOverlay? _dreamViewOverlay; private ContextMenuPopup _contextMenu = default!; @@ -42,6 +47,7 @@ private sealed class EntityClickInformation(ClientObjectReference atom, ScreenCo public override void Initialize() { UpdatesOutsidePrediction = true; + _sawmill = _logManager.GetSawmill("opendream.mouseinput"); _contextMenu = new ContextMenuPopup(); _userInterfaceManager.ModalRoot.AddChild(_contextMenu); @@ -193,25 +199,80 @@ private bool OnPress(ScalingViewport viewport, GUIBoundKeyEventArgs args, Contro var clickParams = CreateClickParams(viewport, args, underMouse.Value.IconPosition); // If client.show_popup_menu is disabled, this will handle sending right clicks _selectedEntity = new(atom, args.PointerLocation, clickParams); + //cursor stuff + if (_appearanceSystem.TryGetAppearance(atom, out var atomAppearance)) { + SetCursorFromDefine(atomAppearance.MouseDragPointer, _dreamInterfaceManager.Cursors.DragCursor, viewport); + } return true; } private bool OnRelease(ScalingViewport viewport, GUIBoundKeyEventArgs args) { - if (_selectedEntity == null) + if (_selectedEntity == null) { + SetCursorFromDefine(0, _dreamInterfaceManager.Cursors.BaseCursor, viewport); //default return false; + } + + var overAtom = GetAtomUnderMouse(viewport, args.RelativePixelPosition, args.PointerLocation); + if (overAtom is not null && _appearanceSystem.TryGetAppearance(overAtom.Value.Atom, out var atomAppearance)) { + SetCursorFromDefine(atomAppearance.MouseOverPointer, _dreamInterfaceManager.Cursors.OverCursor, viewport); + } else + SetCursorFromDefine(0, _dreamInterfaceManager.Cursors.BaseCursor, viewport); if (!_selectedEntity.IsDrag) { RaiseNetworkEvent(new AtomClickedEvent(_selectedEntity.Atom, _selectedEntity.ClickParams)); } else { - var overAtom = GetAtomUnderMouse(viewport, args.RelativePixelPosition, args.PointerLocation); RaiseNetworkEvent(new AtomDraggedEvent(_selectedEntity.Atom, overAtom?.Atom, _selectedEntity.ClickParams)); } + _selectedEntity = null; return true; } + public void SetCursorFromDefine(int define, ICursor? activeCursor, ScalingViewport viewport) { + _sawmill.Debug($"SetCursor {define}"); + if (_dreamInterfaceManager.Cursors.AllStateSet) { + viewport.CustomCursorShape = (_dreamInterfaceManager.Cursors.BaseCursor); + _clyde.SetCursor(viewport.CustomCursorShape); + return; + } + switch (define) { + case 0: //MOUSE_INACTIVE_POINTER + viewport.CustomCursorShape = _dreamInterfaceManager.Cursors.BaseCursor; + _clyde.SetCursor(viewport.CustomCursorShape); + break; + case 1: //MOUSE_ACTIVE_POINTER + viewport.CustomCursorShape = (activeCursor); + _clyde.SetCursor(viewport.CustomCursorShape); + break; + //skipping 2 is intentional, it's what byond does + case 3: //MOUSE_DRAG_POINTER + viewport.CustomCursorShape = (_clyde.GetStandardCursor(StandardCursorShape.Move)); + _clyde.SetCursor(viewport.CustomCursorShape); + break; + case 4: //MOUSE_DROP_POINTER + viewport.CustomCursorShape = (_clyde.GetStandardCursor(StandardCursorShape.NotAllowed)); + _clyde.SetCursor(viewport.CustomCursorShape); + break; + case 5: //MOUSE_ARROW_POINTER + viewport.CustomCursorShape = (_clyde.GetStandardCursor(StandardCursorShape.Arrow)); + _clyde.SetCursor(viewport.CustomCursorShape); + break; + case 6: //MOUSE_CROSSHAIRS_POINTER + viewport.CustomCursorShape = (_clyde.GetStandardCursor(StandardCursorShape.Crosshair)); + _clyde.SetCursor(viewport.CustomCursorShape); + break; + case 7: //MOUSE_HAND_POINTER + viewport.CustomCursorShape = (_clyde.GetStandardCursor(StandardCursorShape.Hand)); + _clyde.SetCursor(viewport.CustomCursorShape); + break; + default: //invalid + viewport.CustomCursorShape = (null); //default cursor + _clyde.SetCursor(viewport.CustomCursorShape); + break; + } + } private ClickParams CreateClickParams(ScalingViewport viewport, GUIBoundKeyEventArgs args, Vector2i iconPos) { bool right = args.Function == EngineKeyFunctions.UIRightClick; bool middle = args.Function == OpenDreamKeyFunctions.MouseMiddle; @@ -221,7 +282,7 @@ private ClickParams CreateClickParams(ScalingViewport viewport, GUIBoundKeyEvent UIBox2i viewportBox = viewport.GetDrawBox(); Vector2 screenLocPos = (args.RelativePixelPosition - viewportBox.TopLeft) / viewportBox.Size * viewport.ViewportSize; float screenLocY = viewport.ViewportSize.Y - screenLocPos.Y; // Flip the Y - ScreenLocation screenLoc = new ScreenLocation((int) screenLocPos.X, (int) screenLocY, 32); // TODO: icon_size other than 32 + ScreenLocation screenLoc = new ScreenLocation((int)screenLocPos.X, (int)screenLocY, 32); // TODO: icon_size other than 32 // TODO: Take icon transformations into account for iconPos return new(screenLoc, right, middle, shift, ctrl, alt, iconPos.X, iconPos.Y); diff --git a/OpenDreamClient/Interface/Controls/ControlMap.cs b/OpenDreamClient/Interface/Controls/ControlMap.cs index 3fe2efc114..f341908c01 100644 --- a/OpenDreamClient/Interface/Controls/ControlMap.cs +++ b/OpenDreamClient/Interface/Controls/ControlMap.cs @@ -16,6 +16,7 @@ public sealed class ControlMap(ControlDescriptor controlDescriptor, ControlWindo public ScalingViewport Viewport { get; private set; } [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; + [Dependency] private readonly IDreamInterfaceManager _dreamInterfaceManager = default!; private MouseInputSystem? _mouseInput; private ClientAppearanceSystem? _appearanceSystem; @@ -138,19 +139,31 @@ public override bool TryGetProperty(string property, [NotNullWhen(true)] out IDM } private void UpdateAtomUnderMouse(ClientObjectReference? atom, Vector2 relativePos, Vector2i iconPos) { + //if dragging and atom drop pointer: set drop pointer + //if atom over pointer: set over pointer + //else set pointer to default state if (!_atomUnderMouse.Equals(atom)) { _entitySystemManager.Resolve(ref _appearanceSystem); - var name = (atom != null) ? _appearanceSystem.GetName(atom.Value) : string.Empty; Window?.SetStatus(name); if (_atomUnderMouse != null) _mouseInput?.HandleAtomMouseExited(Viewport, _atomUnderMouse.Value); - if (atom != null) + if (atom != null) { _mouseInput?.HandleAtomMouseEntered(Viewport, relativePos, atom.Value, iconPos); + if ( _appearanceSystem.TryGetAppearance(atom.Value, out var atomAppearance)) { + if(_mouseInput?.IsDragging ?? false) + if(atomAppearance.MouseDropZone) + _mouseInput?.SetCursorFromDefine(atomAppearance.MouseDropPointer, _dreamInterfaceManager.Cursors.DropCursor, Viewport); + else + _mouseInput?.SetCursorFromDefine(atomAppearance.MouseOverPointer, _dreamInterfaceManager.Cursors.DragCursor, Viewport); + } //else + // _mouseInput?.SetCursorFromDefine(0, _dreamInterfaceManager.Cursors.BaseCursor); + + } } else if (atom.HasValue) { - _mouseInput?.HandleAtomMouseMove(Viewport, relativePos, atom.Value, iconPos); - } + _mouseInput?.HandleAtomMouseMove(Viewport, relativePos, atom.Value, iconPos); + } _atomUnderMouse = atom; } diff --git a/OpenDreamClient/Interface/DreamInterfaceManager.cs b/OpenDreamClient/Interface/DreamInterfaceManager.cs index f69ecb8070..f730e6c6dc 100644 --- a/OpenDreamClient/Interface/DreamInterfaceManager.cs +++ b/OpenDreamClient/Interface/DreamInterfaceManager.cs @@ -23,6 +23,9 @@ using SixLabors.ImageSharp; using System.Linq; using Robust.Shared.Map; +using SixLabors.ImageSharp.PixelFormats; +using OpenToolkit.GraphicsLibraryFramework; +using Robust.Client.Utility; namespace OpenDreamClient.Interface; @@ -58,6 +61,7 @@ internal sealed class DreamInterfaceManager : IDreamInterfaceManager { public Dictionary Menus { get; } = new(); public Dictionary MacroSets { get; } = new(); private Dictionary ClydeWindowIdToControl { get; } = new(); + public CursorHolder Cursors { get; private set; } = new(); public ViewRange View { get => _view; @@ -75,6 +79,7 @@ private set { public bool ShowPopupMenus { get; private set; } = true; public int IconSize { get; private set; } + private ViewRange _view = new(5); public void LoadInterfaceFromSource(string source) { @@ -326,6 +331,31 @@ private void RxUpdateClientInfo(MsgUpdateClientInfo msg) { IconSize = msg.IconSize; View = msg.View; ShowPopupMenus = msg.ShowPopupMenus; + if (msg.CursorResource != 0) + _dreamResource.LoadResourceAsync(msg.CursorResource, resource => { + var allState = resource.GetStateAsImage("all", AtomDirection.South); + if (allState is not null) { //all overrides all possible states + Cursors.BaseCursor = _clyde.CreateCursor(allState!, new(32, 32)); + Cursors.DragCursor = Cursors.BaseCursor; + Cursors.DropCursor = Cursors.BaseCursor; + Cursors.OverCursor = Cursors.BaseCursor; + Cursors.AllStateSet = true; + } else { + var baseState = resource.GetStateAsImage("", AtomDirection.South); + var overState = resource.GetStateAsImage("over", AtomDirection.South); + var dragState = resource.GetStateAsImage("drag", AtomDirection.South); + var dropState = resource.GetStateAsImage("drop", AtomDirection.South); + Cursors.BaseCursor = baseState is null ? null : _clyde.CreateCursor(baseState, new(32, 32)); + Cursors.DragCursor = overState is null ? null : _clyde.CreateCursor(overState, new(32, 32)); + Cursors.DropCursor = dragState is null ? null : _clyde.CreateCursor(dragState, new(32, 32)); + Cursors.OverCursor = dropState is null ? null : _clyde.CreateCursor(dropState, new(32, 32)); + } + //TODO should trigger a cursor update immediately + }); + else { + Cursors = new(); //reset to default + } + } private void ShowPrompt(PromptWindow prompt) { @@ -1024,6 +1054,16 @@ private void OnPromptFinished(int promptId, DreamValueType responseType, object? } } +public sealed class CursorHolder { + public ICursor? BaseCursor; + public ICursor? DragCursor; + public ICursor? OverCursor; + public ICursor? DropCursor; + public bool AllStateSet = false; + + public CursorHolder() {} +} + public interface IDreamInterfaceManager { Dictionary Windows { get; } Dictionary Menus { get; } @@ -1035,6 +1075,7 @@ public interface IDreamInterfaceManager { public ViewRange View { get; } public bool ShowPopupMenus { get; } public int IconSize { get; } + public CursorHolder Cursors { get; } void Initialize(); void FrameUpdate(FrameEventArgs frameEventArgs); diff --git a/OpenDreamClient/Interface/DummyDreamInterfaceManager.cs b/OpenDreamClient/Interface/DummyDreamInterfaceManager.cs index 352374bf5e..7b898ce356 100644 --- a/OpenDreamClient/Interface/DummyDreamInterfaceManager.cs +++ b/OpenDreamClient/Interface/DummyDreamInterfaceManager.cs @@ -1,6 +1,7 @@ using OpenDreamClient.Interface.Controls; using OpenDreamShared.Dream; using OpenDreamShared.Network.Messages; +using Robust.Client.Graphics; using Robust.Shared.Network; using Robust.Shared.Timing; @@ -20,6 +21,7 @@ public sealed class DummyDreamInterfaceManager : IDreamInterfaceManager { public ViewRange View => new(5); public bool ShowPopupMenus => true; public int IconSize => 32; + public CursorHolder Cursors => new(); [Dependency] private readonly IClientNetManager _netManager = default!; diff --git a/OpenDreamClient/Resources/ResourceTypes/DMIResource.cs b/OpenDreamClient/Resources/ResourceTypes/DMIResource.cs index 74c0ad83e5..036e2abe8d 100644 --- a/OpenDreamClient/Resources/ResourceTypes/DMIResource.cs +++ b/OpenDreamClient/Resources/ResourceTypes/DMIResource.cs @@ -4,6 +4,7 @@ using Robust.Client.Graphics; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; namespace OpenDreamClient.Resources.ResourceTypes; @@ -50,6 +51,23 @@ private void ProcessDMIData() { return _states[stateName]; } + public Image? GetStateAsImage(string? stateName, AtomDirection dir) { + using Stream dmiStream = new MemoryStream(Data); + DMIParser.ParsedDMIDescription description = DMIParser.ParseDMI(dmiStream); + + dmiStream.Seek(0, SeekOrigin.Begin); + + Image image = Image.Load(dmiStream); + if (!(description.GetStateOrDefault(stateName)?.Directions.TryGetValue(dir, out var state) ?? false)) + return null; + + var result = image.Clone(clone => { + clone.Resize(new Size(description.Width, description.Height)); + clone.Crop(new Rectangle(state[0].X, state[0].Y, state[0].X + description.Width, state[0].Y + description.Height)); + }); + return result; + } + public struct State { public Dictionary Frames; diff --git a/OpenDreamRuntime/AtomManager.cs b/OpenDreamRuntime/AtomManager.cs index c3a8637ab6..94aae173da 100644 --- a/OpenDreamRuntime/AtomManager.cs +++ b/OpenDreamRuntime/AtomManager.cs @@ -265,6 +265,10 @@ public bool IsValidAppearanceVar(string name) { case "maptext_height": case "maptext_x": case "maptext_y": + case "mouse_drag_pointer": + case "mouse_drop_pointer": + case "mouse_drop_zone": + case "mouse_over_pointer": return true; // Get/SetAppearanceVar doesn't handle filters right now @@ -422,6 +426,18 @@ public void SetAppearanceVar(MutableAppearance appearance, string varName, Dream case "maptext_y": value.TryGetValueAsInteger(out appearance.MaptextOffset.Y); break; + case "mouse_drag_pointer": + value.TryGetValueAsInteger(out appearance.MouseDragPointer); + break; + case "mouse_drop_pointer": + value.TryGetValueAsInteger(out appearance.MouseDropPointer); + break; + case "mouse_drop_zone": + appearance.MouseDropZone = value.IsTruthy(); + break; + case "mouse_over_pointer": + value.TryGetValueAsInteger(out appearance.MouseOverPointer); + break; case "appearance": throw new Exception("Cannot assign the appearance var on an appearance"); @@ -530,6 +546,14 @@ public DreamValue GetAppearanceVar(ImmutableAppearance appearance, string varNam return new(appearance.MaptextOffset.X); case "maptext_y": return new(appearance.MaptextOffset.Y); + case "mouse_drag_pointer": + return new(appearance.MouseDragPointer); + case "mouse_drop_pointer": + return new(appearance.MouseDropPointer); + case "mouse_drop_zone": + return appearance.MouseDropZone ? DreamValue.True : DreamValue.False; + case "mouse_over_pointer": + return new(appearance.MouseOverPointer); case "appearance": MutableAppearance appearanceCopy = appearance.ToMutable(); // Return a copy return new(appearanceCopy); @@ -730,6 +754,10 @@ public MutableAppearance GetAppearanceFromDefinition(DreamObjectDefinition def) def.TryGetVariable("maptext_height", out var maptextHeightVar); def.TryGetVariable("maptext_x", out var maptextXVar); def.TryGetVariable("maptext_y", out var maptextYVar); + def.TryGetVariable("mouse_over_pointer", out var mouseOverPointer); + def.TryGetVariable("mouse_drag_pointer", out var mouseDragPointer); + def.TryGetVariable("mouse_drop_pointer", out var mouseDropPointer); + def.TryGetVariable("mouse_drop_zone", out var mouseDropZone); appearance = MutableAppearance.Get(); SetAppearanceVar(appearance, "name", nameVar); @@ -756,6 +784,10 @@ public MutableAppearance GetAppearanceFromDefinition(DreamObjectDefinition def) SetAppearanceVar(appearance, "maptext_height", maptextHeightVar); SetAppearanceVar(appearance, "maptext_x", maptextXVar); SetAppearanceVar(appearance, "maptext_y", maptextYVar); + SetAppearanceVar(appearance, "mouse_over_pointer", mouseOverPointer); + SetAppearanceVar(appearance, "mouse_drag_pointer", mouseDragPointer); + SetAppearanceVar(appearance, "mouse_drop_pointer", mouseDropPointer); + SetAppearanceVar(appearance, "mouse_drop_zone", mouseDropZone); if (def.TryGetVariable("transform", out var transformVar) && transformVar.TryGetValueAsDreamObject(out var transformMatrix)) { appearance.Transform = DreamObjectMatrix.MatrixToTransformFloatArray(transformMatrix); diff --git a/OpenDreamRuntime/DreamConnection.cs b/OpenDreamRuntime/DreamConnection.cs index 4985f91af0..8f08d03560 100644 --- a/OpenDreamRuntime/DreamConnection.cs +++ b/OpenDreamRuntime/DreamConnection.cs @@ -164,7 +164,8 @@ public void SendClientInfoUpdate() { MsgUpdateClientInfo msg = new() { IconSize = _dreamManager.WorldInstance.IconSize, View = Client!.View, - ShowPopupMenus = Client!.ShowPopupMenus + ShowPopupMenus = Client!.ShowPopupMenus, + CursorResource = Client!.CursorIcon?.Id ?? 0 }; Session?.Channel.SendMessage(msg); diff --git a/OpenDreamRuntime/Objects/Types/DreamObjectClient.cs b/OpenDreamRuntime/Objects/Types/DreamObjectClient.cs index 82782fe697..94d9741e60 100644 --- a/OpenDreamRuntime/Objects/Types/DreamObjectClient.cs +++ b/OpenDreamRuntime/Objects/Types/DreamObjectClient.cs @@ -2,6 +2,7 @@ using System.Text; using OpenDreamRuntime.Procs.Native; using OpenDreamRuntime.Rendering; +using OpenDreamRuntime.Resources; using OpenDreamShared.Dream; namespace OpenDreamRuntime.Objects.Types; @@ -13,6 +14,7 @@ public sealed class DreamObjectClient : DreamObject { public readonly ClientVerbsList ClientVerbs; public ViewRange View { get; private set; } public bool ShowPopupMenus { get; private set; } = true; + public IconResource? CursorIcon = null; public DreamObjectClient(DreamObjectDefinition objectDefinition, DreamConnection connection, ServerScreenOverlaySystem? screenOverlaySystem, ServerClientImagesSystem? clientImagesSystem) : base(objectDefinition) { Connection = connection; @@ -101,6 +103,9 @@ protected override bool TryGetVar(string varName, out DreamValue value) { case "images": value = new(Images); return true; + case "mouse_pointer_icon": + value = CursorIcon is null ? DreamValue.Null : new(CursorIcon); + return true; default: return base.TryGetVar(varName, out value); } @@ -194,6 +199,14 @@ protected override void SetVar(string varName, DreamValue value) { Connection.SelectedStatPanel = statPanel; break; + case "mouse_pointer_icon": + //resolve the value to an icon file + if (value.TryGetValueAsDreamResource(out var iconResource) && iconResource is IconResource) + CursorIcon = (IconResource)iconResource; + else + CursorIcon = null; + Connection.SendClientInfoUpdate(); + break; default: base.SetVar(varName, value); break; diff --git a/OpenDreamShared/Dream/ImmutableAppearance.cs b/OpenDreamShared/Dream/ImmutableAppearance.cs index 5999acae83..aad98a8281 100644 --- a/OpenDreamShared/Dream/ImmutableAppearance.cs +++ b/OpenDreamShared/Dream/ImmutableAppearance.cs @@ -58,6 +58,10 @@ public sealed class ImmutableAppearance : IEquatable { [ViewVariables] public Vector2i MaptextOffset = MutableAppearance.Default.MaptextOffset; [ViewVariables] public string? Maptext = MutableAppearance.Default.Maptext; [ViewVariables] public AtomMouseEvents EnabledMouseEvents; + [ViewVariables] public int MouseDragPointer = MutableAppearance.Default.MouseDragPointer; + [ViewVariables] public bool MouseDropZone = MutableAppearance.Default.MouseDropZone; + [ViewVariables] public int MouseOverPointer = MutableAppearance.Default.MouseOverPointer; + [ViewVariables] public int MouseDropPointer = MutableAppearance.Default.MouseDropPointer; /// The Transform property of this appearance, in [a,d,b,e,c,f] order [ViewVariables] public readonly float[] Transform = [ @@ -103,6 +107,10 @@ public ImmutableAppearance(MutableAppearance appearance, SharedAppearanceSystem? MaptextSize = appearance.MaptextSize; MaptextOffset = appearance.MaptextOffset; EnabledMouseEvents = appearance.EnabledMouseEvents; + MouseDragPointer = appearance.MouseDragPointer; + MouseDropZone = appearance.MouseDropZone; + MouseOverPointer = appearance.MouseOverPointer; + MouseDropPointer = appearance.MouseDropPointer; Overlays = appearance.Overlays.ToArray(); Underlays = appearance.Underlays.ToArray(); @@ -176,6 +184,10 @@ public bool Equals(ImmutableAppearance? immutableAppearance) { if (immutableAppearance.Maptext != Maptext) return false; if (immutableAppearance.MaptextSize != MaptextSize) return false; if (immutableAppearance.MaptextOffset != MaptextOffset) return false; + if (immutableAppearance.MouseDragPointer != MouseDragPointer) return false; + if (immutableAppearance.MouseDropZone != MouseDropZone) return false; + if (immutableAppearance.MouseOverPointer != MouseOverPointer) return false; + if (immutableAppearance.MouseDropPointer != MouseDropPointer) return false; for (int i = 0; i < Filters.Length; i++) { if (immutableAppearance.Filters[i] != Filters[i]) return false; @@ -247,6 +259,10 @@ public override int GetHashCode() { hashCode.Add(Maptext); hashCode.Add(MaptextOffset); hashCode.Add(MaptextSize); + hashCode.Add(MouseDragPointer); + hashCode.Add(MouseDropZone); + hashCode.Add(MouseOverPointer); + hashCode.Add(MouseDropPointer); foreach (ImmutableAppearance overlay in Overlays) { hashCode.Add(overlay.GetHashCode()); @@ -444,6 +460,22 @@ public ImmutableAppearance(NetIncomingMessage buffer, IRobustSerializer serializ EnabledMouseEvents = (AtomMouseEvents)buffer.ReadByte(); break; } + case IconAppearanceProperty.MouseDragPointer: { + MouseDragPointer = buffer.ReadVariableInt32(); + break; + } + case IconAppearanceProperty.MouseDropZone: { + MouseDropZone = true; + break; + } + case IconAppearanceProperty.MouseOverPointer: { + MouseOverPointer = buffer.ReadVariableInt32(); + break; + } + case IconAppearanceProperty.MouseDropPointer: { + MouseDropPointer = buffer.ReadVariableInt32(); + break; + } default: throw new Exception($"Invalid property {property}"); } @@ -486,6 +518,10 @@ public MutableAppearance ToMutable() { result.MaptextOffset = MaptextOffset; result.MaptextSize = MaptextSize; result.EnabledMouseEvents = EnabledMouseEvents; + result.MouseDragPointer = MouseDragPointer; + result.MouseDropZone = MouseDropZone; + result.MouseOverPointer = MouseOverPointer; + result.MouseDropPointer = MouseDropPointer; result.Overlays.EnsureCapacity(Overlays.Length); result.Underlays.EnsureCapacity(Underlays.Length); @@ -699,6 +735,22 @@ public void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serialize buffer.Write((byte)EnabledMouseEvents); } + if (MouseDragPointer != MutableAppearance.Default.MouseDragPointer) { + buffer.Write((byte)IconAppearanceProperty.MouseDragPointer); + buffer.WriteVariableInt32(MouseDragPointer); + } + if (MouseDropZone) { + buffer.Write((byte)IconAppearanceProperty.MouseDropZone); + } + if (MouseOverPointer != MutableAppearance.Default.MouseOverPointer) { + buffer.Write((byte)IconAppearanceProperty.MouseOverPointer); + buffer.WriteVariableInt32(MouseOverPointer); + } + if (MouseDropPointer != MutableAppearance.Default.MouseDropPointer) { + buffer.Write((byte)IconAppearanceProperty.MouseOverPointer); + buffer.WriteVariableInt32(MouseOverPointer); + } + buffer.Write((byte)IconAppearanceProperty.End); } diff --git a/OpenDreamShared/Dream/MutableAppearance.cs b/OpenDreamShared/Dream/MutableAppearance.cs index 8a45e759f1..0a897d64bf 100644 --- a/OpenDreamShared/Dream/MutableAppearance.cs +++ b/OpenDreamShared/Dream/MutableAppearance.cs @@ -54,6 +54,10 @@ public sealed class MutableAppearance : IEquatable, IDisposab [ViewVariables] public Vector2i MaptextSize = new(32,32); [ViewVariables] public Vector2i MaptextOffset = new(0,0); [ViewVariables] public string? Maptext; + [ViewVariables] public int MouseDragPointer; + [ViewVariables] public bool MouseDropZone; + [ViewVariables] public int MouseOverPointer; + [ViewVariables] public int MouseDropPointer; /// /// Used by atoms to mark what mouse events are enabled. Doesn't mean anything outside the context of atoms. @@ -138,6 +142,10 @@ public void CopyFrom(MutableAppearance appearance) { MaptextSize = appearance.MaptextSize; MaptextOffset = appearance.MaptextOffset; EnabledMouseEvents = appearance.EnabledMouseEvents; + MouseDragPointer = appearance.MouseDragPointer; + MouseDropZone = appearance.MouseDropZone; + MouseOverPointer = appearance.MouseOverPointer; + MouseDropPointer = appearance.MouseDropPointer; Overlays.Clear(); Underlays.Clear(); @@ -187,6 +195,10 @@ public bool Equals(MutableAppearance? appearance) { if (appearance.Maptext != Maptext) return false; if (appearance.MaptextSize != MaptextSize) return false; if (appearance.MaptextOffset != MaptextOffset) return false; + if (MouseDragPointer != appearance.MouseDragPointer) return false; + if (MouseDropZone != appearance.MouseDropZone) return false; + if (MouseOverPointer != appearance.MouseOverPointer) return false; + if (MouseDropPointer != appearance.MouseDropPointer) return false; for (int i = 0; i < Filters.Count; i++) { if (appearance.Filters[i] != Filters[i]) return false; @@ -276,6 +288,10 @@ public override int GetHashCode() { hashCode.Add(Maptext); hashCode.Add(MaptextOffset); hashCode.Add(MaptextSize); + hashCode.Add(MouseDragPointer); + hashCode.Add(MouseDropZone); + hashCode.Add(MouseOverPointer); + hashCode.Add(MouseDropPointer); foreach (var overlay in Overlays) { hashCode.Add(overlay.GetHashCode()); @@ -431,6 +447,10 @@ public enum IconAppearanceProperty : byte { MaptextSize, MaptextOffset, EnabledMouseEvents, + MouseDragPointer, + MouseDropZone, + MouseOverPointer, + MouseDropPointer, Id, End } diff --git a/OpenDreamShared/Network/Messages/MsgUpdateClientInfo.cs b/OpenDreamShared/Network/Messages/MsgUpdateClientInfo.cs index 77318ae240..5bef086648 100644 --- a/OpenDreamShared/Network/Messages/MsgUpdateClientInfo.cs +++ b/OpenDreamShared/Network/Messages/MsgUpdateClientInfo.cs @@ -13,6 +13,7 @@ public sealed class MsgUpdateClientInfo : NetMessage { public int IconSize; public ViewRange View; + public int CursorResource; public bool ShowPopupMenus; @@ -20,6 +21,7 @@ public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer IconSize = buffer.ReadInt32(); View = new(buffer.ReadInt32(), buffer.ReadInt32()); ShowPopupMenus = buffer.ReadBoolean(); + CursorResource = buffer.ReadInt32(); } public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer) { @@ -27,5 +29,6 @@ public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer buffer.Write(View.Width); buffer.Write(View.Height); buffer.Write(ShowPopupMenus); + buffer.Write(CursorResource); } } diff --git a/TestGame/icons/objects.dmi b/TestGame/icons/objects.dmi index 6ad8fefa1e..b3a6adeb12 100644 Binary files a/TestGame/icons/objects.dmi and b/TestGame/icons/objects.dmi differ diff --git a/TestGame/map_z1.dmm b/TestGame/map_z1.dmm index 54a7a34400..d9620a6012 100644 --- a/TestGame/map_z1.dmm +++ b/TestGame/map_z1.dmm @@ -39,15 +39,17 @@ "M" = (/mob,/turf,/area) "N" = (/obj/plaque/animation_turf_test,/turf,/area) "O" = (/obj/plaque/animation_test,/turf,/area) +"Q" = (/obj/bin,/turf,/area) "R" = (/turf/blue,/area/withicon) "S" = (/obj/button/animation_turf_test,/turf,/area) +"W" = (/obj/rubbish,/turf,/area) "X" = (/turf,/area/withicon) "Z" = (/obj/button/animation_test,/turf,/area) (1,1,1) = {" bbbbbbbbbbbbbbbbbbbbbb -bedciklwopsuaaaaaaaaab -bfghjmnxqrtvaaaaaaaaab +bedciklwopsuQaaaaaaaab +bfghjmnxqrtvWaaaaaaaab byADFGZSaaaaaaaaaaaaab bzBEHIONaaaaaaaaaaaaab baaaaaaaaaaaaaaaaaaaab diff --git a/TestGame/renderer_tests.dm b/TestGame/renderer_tests.dm index 4e806e75c0..9ff6803b79 100644 --- a/TestGame/renderer_tests.dm +++ b/TestGame/renderer_tests.dm @@ -578,3 +578,18 @@ six.overlays += seven src.overlays += six + +// mouse pointer tests +/obj/bin + name = "bin" + icon = 'icons/objects.dmi' + icon_state = "bin" + mouse_drop_pointer = MOUSE_DROP_POINTER + mouse_drop_zone = 1 + +/obj/rubbish + name = "rubbish" + icon = 'icons/objects.dmi' + icon_state = "rubbish" + mouse_over_pointer = MOUSE_HAND_POINTER + mouse_drag_pointer = MOUSE_DRAG_POINTER \ No newline at end of file