From fe80bd8d02d9d0145dc00b92b54a867abcf031b6 Mon Sep 17 00:00:00 2001 From: amy Date: Sun, 21 Sep 2025 23:23:08 +0100 Subject: [PATCH 1/7] mostly done --- DMCompiler/DMStandard/Types/Client.dm | 2 +- .../Interface/Controls/ControlMap.cs | 5 ++- .../Interface/DreamInterfaceManager.cs | 32 +++++++++++++++++++ .../Interface/DummyDreamInterfaceManager.cs | 2 ++ .../Resources/ResourceTypes/DMIResource.cs | 21 +++++++++++- OpenDreamRuntime/DreamConnection.cs | 3 +- .../Objects/Types/DreamObjectClient.cs | 13 ++++++++ .../Network/Messages/MsgUpdateClientInfo.cs | 3 ++ 8 files changed, 77 insertions(+), 4 deletions(-) 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/OpenDreamClient/Interface/Controls/ControlMap.cs b/OpenDreamClient/Interface/Controls/ControlMap.cs index 3fe2efc114..9701447747 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 IClyde _clyde = default!; private MouseInputSystem? _mouseInput; private ClientAppearanceSystem? _appearanceSystem; @@ -140,7 +141,9 @@ public override bool TryGetProperty(string property, [NotNullWhen(true)] out IDM private void UpdateAtomUnderMouse(ClientObjectReference? atom, Vector2 relativePos, Vector2i iconPos) { if (!_atomUnderMouse.Equals(atom)) { _entitySystemManager.Resolve(ref _appearanceSystem); - + //appearnace lookup for mouse_over_pointer and mouse_drag_pointer and mouse_drop_zone along with client's mouse_pointer_icon + //work out the logic and set the cursor + _clyde.SetCursor(_interfaceManager.Cursors[0]); var name = (atom != null) ? _appearanceSystem.GetName(atom.Value) : string.Empty; Window?.SetStatus(name); diff --git a/OpenDreamClient/Interface/DreamInterfaceManager.cs b/OpenDreamClient/Interface/DreamInterfaceManager.cs index f69ecb8070..4ace80d9a6 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 ICursor?[] Cursors { get; private set; } = new ICursor?[4]; 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,32 @@ 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[0] = _clyde.CreateCursor(allState!, new(32, 32)); + Cursors[1] = Cursors[0]; + Cursors[2] = Cursors[0]; + Cursors[3] = Cursors[0]; + } else { + var baseState = resource.GetStateAsImage("all", AtomDirection.South); + var overState = resource.GetStateAsImage("all", AtomDirection.South); + var dragState = resource.GetStateAsImage("all", AtomDirection.South); + var dropState = resource.GetStateAsImage("all", AtomDirection.South); + Cursors[0] = baseState is null ? null : _clyde.CreateCursor(baseState, new(32, 32)); + Cursors[1] = overState is null ? null : _clyde.CreateCursor(overState, new(32, 32)); + Cursors[2] = dragState is null ? null : _clyde.CreateCursor(dragState, new(32, 32)); + Cursors[3] = dropState is null ? null : _clyde.CreateCursor(dropState, new(32, 32)); + } + }); + else { + Cursors[0] = null; + Cursors[1] = null; + Cursors[2] = null; + Cursors[3] = null; + } + } private void ShowPrompt(PromptWindow prompt) { @@ -1035,6 +1066,7 @@ public interface IDreamInterfaceManager { public ViewRange View { get; } public bool ShowPopupMenus { get; } public int IconSize { get; } + public ICursor?[] Cursors { get; } void Initialize(); void FrameUpdate(FrameEventArgs frameEventArgs); diff --git a/OpenDreamClient/Interface/DummyDreamInterfaceManager.cs b/OpenDreamClient/Interface/DummyDreamInterfaceManager.cs index 352374bf5e..2fb964ff57 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 ICursor?[] Cursors => new ICursor?[4]; [Dependency] private readonly IClientNetManager _netManager = default!; diff --git a/OpenDreamClient/Resources/ResourceTypes/DMIResource.cs b/OpenDreamClient/Resources/ResourceTypes/DMIResource.cs index 9619545a09..cc3b45af6b 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; @@ -54,10 +55,28 @@ 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(); + result.Mutate(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; + } + private bool IsValidPNG() { if (Data.Length < PngHeader.Length) return false; - for (int i=0; i Date: Mon, 22 Sep 2025 15:07:33 +0100 Subject: [PATCH 2/7] test bits --- TestGame/icons/objects.dmi | Bin 2858 -> 3537 bytes TestGame/renderer_tests.dm | 15 +++++++++++++++ 2 files changed, 15 insertions(+) diff --git a/TestGame/icons/objects.dmi b/TestGame/icons/objects.dmi index 6ad8fefa1ef6ccd900dd6bd596167ef65011da8e..b3a6adeb1229ed97684b7ab8761aa1d6fa9e19d1 100644 GIT binary patch literal 3537 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 LNkvXXu0mjfBoDZf literal 2858 zcmV+_3)S?AP)V=-0C=2@%)Ji6AQXn-8Lr}_-Hm@Mi)mvzwD)k@9t$Y~!a?KRJ2Y`KxP4#p z|Mx7|4GXX-RvzH zDqp2Q@3TMdJ?*)-=YICwd(Nfjm)!KcALn`A`#kUWd(JI5Sc4Pc1x`WSE+w9){g3Iax_#r4r*@F4T2lxhfb|aIA#X)w=*vkQA zKLo6aqk>#WdpwTvqsBg6kQAlD$xnb8an^JRhRi@6Jr`#^5n-bi@YzD(Hp3*f( z@XKiKv5^C~H-WmAA~Vwf#=-2(@W@P9HWZ3y!B=;{qp=7b8iPIU;Q;T~_I=p`!mQRB{GhtOKPzuLAur3!40z3h2 zQ(?sjxPLxOeh;SXfL(R4(gUssU~x0lLxLQTpPwJ5=Jt$BL^*W0z%?1*G@N@|H~444 z_&n&WfyN40a2BTi9IhOO&e<@#63PT#-VMu-Kw26w7mn(MzX%f!!M0R*`2m=d3R$1S z`xP*+9A;O+neW0yfYeZ>3~#u|$8B%B1#Re=3a_f6Qc0k%H{ z*H*&ArBJsLu4KchKf%vhpl%Gv5;(RMjwtvufPnf9EJ+C{{9IVO2zK3sx>~52=Z(Jc z3bece8%M)STHc^7z)a(Z$8#fbx&W94t}K8K`1moX`4zl-9rm@txQVb8U2^AM$ z55Qh{^-H*KIQ&asXDZxnB>W^aUVujwJo_MIYw^2bR|kCipWxQw`qfAy!?(1wg#AYO z`T3FLHEsctod+BM(!(k;4YdaK>Gs6wg9(r~3x3=Ty{mD*77)VtD$FO4Nb_mgJ4vYV z=>|#Y!A%a_dl)vC`&ySq$T2`?{5bRJ=a7Ach!GB0?YZaw`R5ThZ@XeCX+{lSx4 zucKN8$qt^ym=3%rY8p3y1{4XLWCX{rX85Lw5%k${VJ9Gf@fmLH1{)s`P6gQ(qb$?0 z&vrC`Wd99V0St|!mR6XSi*kqV$`XW4wjwlAs(cq_bq_rO;dYQ;+r{``HwUQJkAOi__mIvIwn16dD@##@^MpA+f;#%g!a_nl4ydx57= zZ3$g;MT5N^pdr)K!Bq^NZ-MJ>U>Zm!FaqRDP?Lajz%fv*AWa~9Q73|*9XmKUI5;>s z3~rKR78HPZU(i?yL;#$S4I$ z5}`l6yL)8jrAre5(z?1v4McxZIRN0~^y!B2?Y9{}SRF6@*V4Vu5cH&^aB9kwnDr-} z1CHe8^4df9_{G&cd>@DB%t@I3?Q7Q1HO%{>jZ1`o7Z=B@Kj{VzQHo=;X0c^`7Q-)J z=G^%4v}Gsmts_nU`K&(uSBJ%}Kj|Ex(J^)`9b?Ca&uDY~N!JD4YB&zC7{>t?<2b-# z90yp8;{c0s9AGhy11!dGA$Dqe#%hI+BT0fa>yl7Kpg+>Ad*y z+A*0|=f+dvnU0Z7Pp!QxH$`#a$=ar-c(xgcp z>(;F^d^6t){kC#IcX#*e7`#gx1&@Ep_D38N-2GOu>67X4ngqQsa`~iL}X8( z(SjKz5%9s^Wq*`^!- zzIbhzededPIQ_by>heM<+mu6fc|qGCafH?-QQp)X%(r+>BRZ1-lk++}2^anSAx_oH~?ehb} z(anMc7or}oE-#eo@_TPqR}?%K)&1eb_niYY=%RlIy~bK%U`T!5UW4llUmutTEHbau z17iTwP)X@?M!`Xo%F}??IE`9_uXZ>3t_^2sMh7^${7VAwRZ6*aVN1sVzbiFSCV|j)gD^)Gpa6KUy~c~_5UKG?}mD>Bi!J#bPPz{ zP}}SR@OC3=p1x$Uwut_y(mrQs7&@iEXXr*?p`Qd}V>W1wBA~@I`()MY2W>lISVOAI z3(kNF{NF1q__UgP)xkA#Y4=AMyb$$NA zfV|pZt>twaEfWpX!=>NlFWZ1yzXB77)&pM#8wz;gbw#kK(=LPdt zsLD&BQM_3??20L0FxXZ_G-H5;*l!Go7NhyTx4Xjh?&?ND9{>OV 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 From e68387a3ebd256b6773462f710748dfe716df186 Mon Sep 17 00:00:00 2001 From: amy Date: Mon, 22 Sep 2025 23:22:39 +0100 Subject: [PATCH 3/7] it's always so much more work than you expect --- OpenDreamClient/Input/MouseInputSystem.cs | 50 ++++++++++++++++-- .../Interface/Controls/ControlMap.cs | 22 +++++--- .../Interface/DreamInterfaceManager.cs | 9 ++-- OpenDreamRuntime/AtomManager.cs | 24 +++++++++ OpenDreamShared/Dream/ImmutableAppearance.cs | 52 +++++++++++++++++++ OpenDreamShared/Dream/MutableAppearance.cs | 20 +++++++ TestGame/map_z1.dmm | 6 ++- 7 files changed, 167 insertions(+), 16 deletions(-) diff --git a/OpenDreamClient/Input/MouseInputSystem.cs b/OpenDreamClient/Input/MouseInputSystem.cs index 1264c436eb..839ec66a09 100644 --- a/OpenDreamClient/Input/MouseInputSystem.cs +++ b/OpenDreamClient/Input/MouseInputSystem.cs @@ -28,6 +28,9 @@ 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!; + + public bool IsDragging { get => _selectedEntity?.IsDrag ?? false; } private DreamViewOverlay? _dreamViewOverlay; private ContextMenuPopup _contextMenu = default!; @@ -193,25 +196,66 @@ 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, 2); //2 is drag + } return true; } private bool OnRelease(ScalingViewport viewport, GUIBoundKeyEventArgs args) { - if (_selectedEntity == null) + if (_selectedEntity == null) { + SetCursorFromDefine(0, 0); //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, 1); //1 is over + } else + SetCursorFromDefine(0, 0); 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, int activePos) { + switch (define) { + case 0: //MOUSE_INACTIVE_POINTER + _clyde.SetCursor(_dreamInterfaceManager.Cursors[0]); + break; + case 1: //MOUSE_ACTIVE_POINTER + _clyde.SetCursor(_dreamInterfaceManager.Cursors[activePos]); + break; + //skipping 2 is intentional, it's what byond does + case 3: //MOUSE_DRAG_POINTER + _clyde.SetCursor(_clyde.GetStandardCursor(StandardCursorShape.Move)); + break; + case 4: //MOUSE_DROP_POINTER + _clyde.SetCursor(_clyde.GetStandardCursor(StandardCursorShape.NotAllowed)); + break; + case 5: //MOUSE_ARROW_POINTER + _clyde.SetCursor(_clyde.GetStandardCursor(StandardCursorShape.Arrow)); + break; + case 6: //MOUSE_CROSSHAIRS_POINTER + _clyde.SetCursor(_clyde.GetStandardCursor(StandardCursorShape.Crosshair)); + break; + case 7: //MOUSE_HAND_POINTER + _clyde.SetCursor(_clyde.GetStandardCursor(StandardCursorShape.Hand)); + break; + default: //invalid + _clyde.SetCursor(null); //default cursor + break; + } + } private ClickParams CreateClickParams(ScalingViewport viewport, GUIBoundKeyEventArgs args, Vector2i iconPos) { bool right = args.Function == EngineKeyFunctions.UIRightClick; bool middle = args.Function == OpenDreamKeyFunctions.MouseMiddle; @@ -221,7 +265,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 9701447747..fdc896b101 100644 --- a/OpenDreamClient/Interface/Controls/ControlMap.cs +++ b/OpenDreamClient/Interface/Controls/ControlMap.cs @@ -16,7 +16,6 @@ public sealed class ControlMap(ControlDescriptor controlDescriptor, ControlWindo public ScalingViewport Viewport { get; private set; } [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; - [Dependency] private readonly IClyde _clyde = default!; private MouseInputSystem? _mouseInput; private ClientAppearanceSystem? _appearanceSystem; @@ -139,21 +138,30 @@ 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); - //appearnace lookup for mouse_over_pointer and mouse_drag_pointer and mouse_drop_zone along with client's mouse_pointer_icon - //work out the logic and set the cursor - _clyde.SetCursor(_interfaceManager.Cursors[0]); 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) && atomAppearance.MouseDropZone) + _mouseInput?.SetCursorFromDefine(atomAppearance.MouseDropPointer, 3); //3 is drop + else + _mouseInput?.SetCursorFromDefine(atomAppearance.MouseOverPointer, 1); //1 is over + } else + _mouseInput?.SetCursorFromDefine(0, 0); + + } } 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 4ace80d9a6..f122ab173a 100644 --- a/OpenDreamClient/Interface/DreamInterfaceManager.cs +++ b/OpenDreamClient/Interface/DreamInterfaceManager.cs @@ -340,15 +340,16 @@ private void RxUpdateClientInfo(MsgUpdateClientInfo msg) { Cursors[2] = Cursors[0]; Cursors[3] = Cursors[0]; } else { - var baseState = resource.GetStateAsImage("all", AtomDirection.South); - var overState = resource.GetStateAsImage("all", AtomDirection.South); - var dragState = resource.GetStateAsImage("all", AtomDirection.South); - var dropState = resource.GetStateAsImage("all", AtomDirection.South); + 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[0] = baseState is null ? null : _clyde.CreateCursor(baseState, new(32, 32)); Cursors[1] = overState is null ? null : _clyde.CreateCursor(overState, new(32, 32)); Cursors[2] = dragState is null ? null : _clyde.CreateCursor(dragState, new(32, 32)); Cursors[3] = dropState is null ? null : _clyde.CreateCursor(dropState, new(32, 32)); } + //TODO should trigger a cursor update immediately }); else { Cursors[0] = null; diff --git a/OpenDreamRuntime/AtomManager.cs b/OpenDreamRuntime/AtomManager.cs index c3a8637ab6..cb7c9ad534 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); 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/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 From d6632faa487351964d3773f401463d85480e68d8 Mon Sep 17 00:00:00 2001 From: amy Date: Mon, 22 Sep 2025 23:33:38 +0100 Subject: [PATCH 4/7] standard --- DMCompiler/DMStandard/Defines.dm | 10 +++++----- DMCompiler/DMStandard/Types/Atoms/_Atom.dm | 6 +++--- DMCompiler/DMStandard/Types/Image.dm | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/DMCompiler/DMStandard/Defines.dm b/DMCompiler/DMStandard/Defines.dm index 148660d34c..988b350797 100644 --- a/DMCompiler/DMStandard/Defines.dm +++ b/DMCompiler/DMStandard/Defines.dm @@ -179,11 +179,11 @@ //see mouse handling #define MOUSE_INACTIVE_POINTER 0 #define MOUSE_ACTIVE_POINTER 1 -#define MOUSE_DRAG_POINTER 2 -#define MOUSE_DROP_POINTER 3 -#define MOUSE_ARROW_POINTER 4 -#define MOUSE_CROSSHAIRS_POINTER 5 -#define MOUSE_HAND_POINTER 6 +#define MOUSE_DRAG_POINTER 3 +#define MOUSE_DROP_POINTER 4 +#define MOUSE_ARROW_POINTER 5 +#define MOUSE_CROSSHAIRS_POINTER 6 +#define MOUSE_HAND_POINTER 7 //client.control_freak #define CONTROL_FREAK_ALL (1<<0) 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/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 From 64e592080b814a9a6f827be6d68f698279edc674 Mon Sep 17 00:00:00 2001 From: amy Date: Tue, 23 Sep 2025 22:14:46 +0100 Subject: [PATCH 5/7] gotcha, but logic issues to work out --- OpenDreamClient/Input/MouseInputSystem.cs | 18 ++++++---- .../Interface/Controls/ControlMap.cs | 7 ++-- .../Interface/DreamInterfaceManager.cs | 36 +++++++++++-------- .../Interface/DummyDreamInterfaceManager.cs | 2 +- OpenDreamRuntime/AtomManager.cs | 8 +++++ 5 files changed, 46 insertions(+), 25 deletions(-) diff --git a/OpenDreamClient/Input/MouseInputSystem.cs b/OpenDreamClient/Input/MouseInputSystem.cs index 839ec66a09..d36f8cf7c8 100644 --- a/OpenDreamClient/Input/MouseInputSystem.cs +++ b/OpenDreamClient/Input/MouseInputSystem.cs @@ -198,22 +198,22 @@ private bool OnPress(ScalingViewport viewport, GUIBoundKeyEventArgs args, Contro _selectedEntity = new(atom, args.PointerLocation, clickParams); //cursor stuff if (_appearanceSystem.TryGetAppearance(atom, out var atomAppearance)) { - SetCursorFromDefine(atomAppearance.MouseDragPointer, 2); //2 is drag + SetCursorFromDefine(atomAppearance.MouseDragPointer, _dreamInterfaceManager.Cursors.DragCursor); } return true; } private bool OnRelease(ScalingViewport viewport, GUIBoundKeyEventArgs args) { if (_selectedEntity == null) { - SetCursorFromDefine(0, 0); //default + SetCursorFromDefine(0, _dreamInterfaceManager.Cursors.BaseCursor); //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, 1); //1 is over + SetCursorFromDefine(atomAppearance.MouseOverPointer, _dreamInterfaceManager.Cursors.OverCursor); } else - SetCursorFromDefine(0, 0); + SetCursorFromDefine(0, _dreamInterfaceManager.Cursors.BaseCursor); if (!_selectedEntity.IsDrag) { RaiseNetworkEvent(new AtomClickedEvent(_selectedEntity.Atom, _selectedEntity.ClickParams)); @@ -227,13 +227,17 @@ private bool OnRelease(ScalingViewport viewport, GUIBoundKeyEventArgs args) { return true; } - public void SetCursorFromDefine(int define, int activePos) { + public void SetCursorFromDefine(int define, ICursor? activeCursor) { + if (_dreamInterfaceManager.Cursors.AllStateSet) { + _clyde.SetCursor(_dreamInterfaceManager.Cursors.BaseCursor); + return; + } switch (define) { case 0: //MOUSE_INACTIVE_POINTER - _clyde.SetCursor(_dreamInterfaceManager.Cursors[0]); + _clyde.SetCursor(_dreamInterfaceManager.Cursors.BaseCursor); break; case 1: //MOUSE_ACTIVE_POINTER - _clyde.SetCursor(_dreamInterfaceManager.Cursors[activePos]); + _clyde.SetCursor(activeCursor); break; //skipping 2 is intentional, it's what byond does case 3: //MOUSE_DRAG_POINTER diff --git a/OpenDreamClient/Interface/Controls/ControlMap.cs b/OpenDreamClient/Interface/Controls/ControlMap.cs index fdc896b101..e7670182f8 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; @@ -152,11 +153,11 @@ private void UpdateAtomUnderMouse(ClientObjectReference? atom, Vector2 relativeP _mouseInput?.HandleAtomMouseEntered(Viewport, relativePos, atom.Value, iconPos); if ( _appearanceSystem.TryGetAppearance(atom.Value, out var atomAppearance)) { if((_mouseInput?.IsDragging ?? false) && atomAppearance.MouseDropZone) - _mouseInput?.SetCursorFromDefine(atomAppearance.MouseDropPointer, 3); //3 is drop + _mouseInput?.SetCursorFromDefine(atomAppearance.MouseDropPointer, _dreamInterfaceManager.Cursors.DropCursor); else - _mouseInput?.SetCursorFromDefine(atomAppearance.MouseOverPointer, 1); //1 is over + _mouseInput?.SetCursorFromDefine(atomAppearance.MouseOverPointer, _dreamInterfaceManager.Cursors.OverCursor); } else - _mouseInput?.SetCursorFromDefine(0, 0); + _mouseInput?.SetCursorFromDefine(0, _dreamInterfaceManager.Cursors.BaseCursor); } } else if (atom.HasValue) { diff --git a/OpenDreamClient/Interface/DreamInterfaceManager.cs b/OpenDreamClient/Interface/DreamInterfaceManager.cs index f122ab173a..f730e6c6dc 100644 --- a/OpenDreamClient/Interface/DreamInterfaceManager.cs +++ b/OpenDreamClient/Interface/DreamInterfaceManager.cs @@ -61,7 +61,7 @@ internal sealed class DreamInterfaceManager : IDreamInterfaceManager { public Dictionary Menus { get; } = new(); public Dictionary MacroSets { get; } = new(); private Dictionary ClydeWindowIdToControl { get; } = new(); - public ICursor?[] Cursors { get; private set; } = new ICursor?[4]; + public CursorHolder Cursors { get; private set; } = new(); public ViewRange View { get => _view; @@ -335,27 +335,25 @@ private void RxUpdateClientInfo(MsgUpdateClientInfo msg) { _dreamResource.LoadResourceAsync(msg.CursorResource, resource => { var allState = resource.GetStateAsImage("all", AtomDirection.South); if (allState is not null) { //all overrides all possible states - Cursors[0] = _clyde.CreateCursor(allState!, new(32, 32)); - Cursors[1] = Cursors[0]; - Cursors[2] = Cursors[0]; - Cursors[3] = Cursors[0]; + 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[0] = baseState is null ? null : _clyde.CreateCursor(baseState, new(32, 32)); - Cursors[1] = overState is null ? null : _clyde.CreateCursor(overState, new(32, 32)); - Cursors[2] = dragState is null ? null : _clyde.CreateCursor(dragState, new(32, 32)); - Cursors[3] = dropState is null ? null : _clyde.CreateCursor(dropState, new(32, 32)); + 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[0] = null; - Cursors[1] = null; - Cursors[2] = null; - Cursors[3] = null; + Cursors = new(); //reset to default } } @@ -1056,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; } @@ -1067,7 +1075,7 @@ public interface IDreamInterfaceManager { public ViewRange View { get; } public bool ShowPopupMenus { get; } public int IconSize { get; } - public ICursor?[] Cursors { get; } + public CursorHolder Cursors { get; } void Initialize(); void FrameUpdate(FrameEventArgs frameEventArgs); diff --git a/OpenDreamClient/Interface/DummyDreamInterfaceManager.cs b/OpenDreamClient/Interface/DummyDreamInterfaceManager.cs index 2fb964ff57..7b898ce356 100644 --- a/OpenDreamClient/Interface/DummyDreamInterfaceManager.cs +++ b/OpenDreamClient/Interface/DummyDreamInterfaceManager.cs @@ -21,7 +21,7 @@ public sealed class DummyDreamInterfaceManager : IDreamInterfaceManager { public ViewRange View => new(5); public bool ShowPopupMenus => true; public int IconSize => 32; - public ICursor?[] Cursors => new ICursor?[4]; + public CursorHolder Cursors => new(); [Dependency] private readonly IClientNetManager _netManager = default!; diff --git a/OpenDreamRuntime/AtomManager.cs b/OpenDreamRuntime/AtomManager.cs index cb7c9ad534..94aae173da 100644 --- a/OpenDreamRuntime/AtomManager.cs +++ b/OpenDreamRuntime/AtomManager.cs @@ -754,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); @@ -780,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); From 682395acde1462f70479c2b640fce2a0fdfd9d94 Mon Sep 17 00:00:00 2001 From: amy Date: Wed, 24 Sep 2025 00:06:01 +0100 Subject: [PATCH 6/7] slightly cleaner --- OpenDreamClient/Resources/ResourceTypes/DMIResource.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/OpenDreamClient/Resources/ResourceTypes/DMIResource.cs b/OpenDreamClient/Resources/ResourceTypes/DMIResource.cs index cc3b45af6b..b6664acbf3 100644 --- a/OpenDreamClient/Resources/ResourceTypes/DMIResource.cs +++ b/OpenDreamClient/Resources/ResourceTypes/DMIResource.cs @@ -65,8 +65,7 @@ private void ProcessDMIData() { if (!(description.GetStateOrDefault(stateName)?.Directions.TryGetValue(dir, out var state) ?? false)) return null; - var result = image.Clone(); - result.Mutate(clone => { + 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)); }); From e0b3ce5e3efb37905214b9cd1fe8120ec32c6463 Mon Sep 17 00:00:00 2001 From: amy Date: Thu, 9 Oct 2025 20:01:13 +0100 Subject: [PATCH 7/7] turns out I was doing it wrong --- OpenDreamClient/Input/MouseInputSystem.cs | 41 ++++++++++++------- .../Interface/Controls/ControlMap.cs | 13 +++--- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/OpenDreamClient/Input/MouseInputSystem.cs b/OpenDreamClient/Input/MouseInputSystem.cs index d36f8cf7c8..d10c9a92e8 100644 --- a/OpenDreamClient/Input/MouseInputSystem.cs +++ b/OpenDreamClient/Input/MouseInputSystem.cs @@ -29,6 +29,8 @@ internal sealed class MouseInputSystem : SharedMouseInputSystem { [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; } @@ -45,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); @@ -198,22 +201,22 @@ private bool OnPress(ScalingViewport viewport, GUIBoundKeyEventArgs args, Contro _selectedEntity = new(atom, args.PointerLocation, clickParams); //cursor stuff if (_appearanceSystem.TryGetAppearance(atom, out var atomAppearance)) { - SetCursorFromDefine(atomAppearance.MouseDragPointer, _dreamInterfaceManager.Cursors.DragCursor); + SetCursorFromDefine(atomAppearance.MouseDragPointer, _dreamInterfaceManager.Cursors.DragCursor, viewport); } return true; } private bool OnRelease(ScalingViewport viewport, GUIBoundKeyEventArgs args) { if (_selectedEntity == null) { - SetCursorFromDefine(0, _dreamInterfaceManager.Cursors.BaseCursor); //default + 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); + SetCursorFromDefine(atomAppearance.MouseOverPointer, _dreamInterfaceManager.Cursors.OverCursor, viewport); } else - SetCursorFromDefine(0, _dreamInterfaceManager.Cursors.BaseCursor); + SetCursorFromDefine(0, _dreamInterfaceManager.Cursors.BaseCursor, viewport); if (!_selectedEntity.IsDrag) { RaiseNetworkEvent(new AtomClickedEvent(_selectedEntity.Atom, _selectedEntity.ClickParams)); @@ -227,36 +230,46 @@ private bool OnRelease(ScalingViewport viewport, GUIBoundKeyEventArgs args) { return true; } - public void SetCursorFromDefine(int define, ICursor? activeCursor) { + public void SetCursorFromDefine(int define, ICursor? activeCursor, ScalingViewport viewport) { + _sawmill.Debug($"SetCursor {define}"); if (_dreamInterfaceManager.Cursors.AllStateSet) { - _clyde.SetCursor(_dreamInterfaceManager.Cursors.BaseCursor); + viewport.CustomCursorShape = (_dreamInterfaceManager.Cursors.BaseCursor); + _clyde.SetCursor(viewport.CustomCursorShape); return; } switch (define) { case 0: //MOUSE_INACTIVE_POINTER - _clyde.SetCursor(_dreamInterfaceManager.Cursors.BaseCursor); + viewport.CustomCursorShape = _dreamInterfaceManager.Cursors.BaseCursor; + _clyde.SetCursor(viewport.CustomCursorShape); break; case 1: //MOUSE_ACTIVE_POINTER - _clyde.SetCursor(activeCursor); + viewport.CustomCursorShape = (activeCursor); + _clyde.SetCursor(viewport.CustomCursorShape); break; //skipping 2 is intentional, it's what byond does case 3: //MOUSE_DRAG_POINTER - _clyde.SetCursor(_clyde.GetStandardCursor(StandardCursorShape.Move)); + viewport.CustomCursorShape = (_clyde.GetStandardCursor(StandardCursorShape.Move)); + _clyde.SetCursor(viewport.CustomCursorShape); break; case 4: //MOUSE_DROP_POINTER - _clyde.SetCursor(_clyde.GetStandardCursor(StandardCursorShape.NotAllowed)); + viewport.CustomCursorShape = (_clyde.GetStandardCursor(StandardCursorShape.NotAllowed)); + _clyde.SetCursor(viewport.CustomCursorShape); break; case 5: //MOUSE_ARROW_POINTER - _clyde.SetCursor(_clyde.GetStandardCursor(StandardCursorShape.Arrow)); + viewport.CustomCursorShape = (_clyde.GetStandardCursor(StandardCursorShape.Arrow)); + _clyde.SetCursor(viewport.CustomCursorShape); break; case 6: //MOUSE_CROSSHAIRS_POINTER - _clyde.SetCursor(_clyde.GetStandardCursor(StandardCursorShape.Crosshair)); + viewport.CustomCursorShape = (_clyde.GetStandardCursor(StandardCursorShape.Crosshair)); + _clyde.SetCursor(viewport.CustomCursorShape); break; case 7: //MOUSE_HAND_POINTER - _clyde.SetCursor(_clyde.GetStandardCursor(StandardCursorShape.Hand)); + viewport.CustomCursorShape = (_clyde.GetStandardCursor(StandardCursorShape.Hand)); + _clyde.SetCursor(viewport.CustomCursorShape); break; default: //invalid - _clyde.SetCursor(null); //default cursor + viewport.CustomCursorShape = (null); //default cursor + _clyde.SetCursor(viewport.CustomCursorShape); break; } } diff --git a/OpenDreamClient/Interface/Controls/ControlMap.cs b/OpenDreamClient/Interface/Controls/ControlMap.cs index e7670182f8..f341908c01 100644 --- a/OpenDreamClient/Interface/Controls/ControlMap.cs +++ b/OpenDreamClient/Interface/Controls/ControlMap.cs @@ -152,12 +152,13 @@ private void UpdateAtomUnderMouse(ClientObjectReference? atom, Vector2 relativeP if (atom != null) { _mouseInput?.HandleAtomMouseEntered(Viewport, relativePos, atom.Value, iconPos); if ( _appearanceSystem.TryGetAppearance(atom.Value, out var atomAppearance)) { - if((_mouseInput?.IsDragging ?? false) && atomAppearance.MouseDropZone) - _mouseInput?.SetCursorFromDefine(atomAppearance.MouseDropPointer, _dreamInterfaceManager.Cursors.DropCursor); - else - _mouseInput?.SetCursorFromDefine(atomAppearance.MouseOverPointer, _dreamInterfaceManager.Cursors.OverCursor); - } else - _mouseInput?.SetCursorFromDefine(0, _dreamInterfaceManager.Cursors.BaseCursor); + 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) {