From 3fbef3b2846e76232323bc9a53a7f33cab85ff5f Mon Sep 17 00:00:00 2001 From: wixoaGit Date: Sun, 22 Mar 2026 16:52:49 -0400 Subject: [PATCH 1/2] Add support for hotspots on custom cursors --- .../Interface/DreamInterfaceManager.cs | 23 ++++++------------- .../Resources/ResourceTypes/DMIResource.cs | 20 +++++++++------- OpenDreamShared/Resources/DMIParser.cs | 12 +++++++++- 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/OpenDreamClient/Interface/DreamInterfaceManager.cs b/OpenDreamClient/Interface/DreamInterfaceManager.cs index 104de7f702..7ead1208f7 100644 --- a/OpenDreamClient/Interface/DreamInterfaceManager.cs +++ b/OpenDreamClient/Interface/DreamInterfaceManager.cs @@ -1048,28 +1048,19 @@ public sealed class CursorHolder(IClyde clyde) { public readonly bool AllStateSet; public CursorHolder(IClyde clyde, DMIResource resource) : this(clyde) { - var allState = resource.GetStateAsImage("all", AtomDirection.South); + var allCursor = resource.GetStateAsImage(clyde, "all"); - if (allState is not null) { //all overrides all possible states - BaseCursor = clyde.CreateCursor(allState, new(32, 32)); + if (allCursor is not null) { //all overrides all possible states + BaseCursor = allCursor; DragCursor = BaseCursor; DropCursor = BaseCursor; OverCursor = BaseCursor; 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); - - if (baseState is not null) - BaseCursor = clyde.CreateCursor(baseState, new(32, 32)); - if (overState is not null) - OverCursor = clyde.CreateCursor(overState, new(32, 32)); - if (dragState is not null) - DragCursor = clyde.CreateCursor(dragState, new(32, 32)); - if (dropState is not null) - DropCursor = clyde.CreateCursor(dropState, new(32, 32)); + BaseCursor = resource.GetStateAsImage(clyde, ""); + OverCursor = resource.GetStateAsImage(clyde, "over"); + DragCursor = resource.GetStateAsImage(clyde, "drag"); + DropCursor = resource.GetStateAsImage(clyde, "drop"); } } } diff --git a/OpenDreamClient/Resources/ResourceTypes/DMIResource.cs b/OpenDreamClient/Resources/ResourceTypes/DMIResource.cs index 036e2abe8d..8e1883c52f 100644 --- a/OpenDreamClient/Resources/ResourceTypes/DMIResource.cs +++ b/OpenDreamClient/Resources/ResourceTypes/DMIResource.cs @@ -51,21 +51,25 @@ 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); + public ICursor? GetStateAsImage(IClyde clyde, string? stateName) { + using var dmiStream = new MemoryStream(Data); + var 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)) + var state = description.GetStateOrDefault(stateName); + if (!(state?.Directions.TryGetValue(AtomDirection.South, out var frames) ?? 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)); + var stateImage = image.Clone(clone => { + var frame = frames[0]; + + clone.Crop(new Rectangle(frame.X, frame.Y, frame.X + description.Width, frame.Y + description.Height)); }); - return result; + + var cursor = clyde.CreateCursor(stateImage, state.Hotspot); + return cursor; } public struct State { diff --git a/OpenDreamShared/Resources/DMIParser.cs b/OpenDreamShared/Resources/DMIParser.cs index e2f97cb5c3..1381de22f4 100644 --- a/OpenDreamShared/Resources/DMIParser.cs +++ b/OpenDreamShared/Resources/DMIParser.cs @@ -126,6 +126,7 @@ public sealed class ParsedDMIState(string name) { public string Name = name; public bool Loop = true; public bool Rewind; + public Vector2i Hotspot = (0, 31); // TODO: This can only contain either 1, 4, or 8 directions. Enforcing this could simplify some things. public readonly Dictionary Directions = new(); @@ -450,7 +451,16 @@ private static ParsedDMIDescription ParseDMIDescription(string dmiDescription, u //TODO break; case "hotspot": - //TODO + if (currentState is null) break; + var hotspotValues = value.Split(','); + if (hotspotValues.Length != 3) + throw new Exception($"Invalid hotspot value \"{value}\""); + + var hotspotX = int.Parse(hotspotValues[0]); + var hotspotY = int.Parse(hotspotValues[1]); + // TODO: 3rd value? Something to do with what frames the hotspot applies to apparently + + currentState.Hotspot = (hotspotX, hotspotY); break; default: throw new Exception($"Invalid key \"{key}\" in DMI description"); From 1c6cc7eb03d3e39549a81a20ba832228032a28c0 Mon Sep 17 00:00:00 2001 From: wixoaGit Date: Sun, 22 Mar 2026 16:59:24 -0400 Subject: [PATCH 2/2] Don't hardcode the default for a 32x32 image --- OpenDreamClient/Resources/ResourceTypes/DMIResource.cs | 3 ++- OpenDreamShared/Resources/DMIParser.cs | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/OpenDreamClient/Resources/ResourceTypes/DMIResource.cs b/OpenDreamClient/Resources/ResourceTypes/DMIResource.cs index 8e1883c52f..d18d163630 100644 --- a/OpenDreamClient/Resources/ResourceTypes/DMIResource.cs +++ b/OpenDreamClient/Resources/ResourceTypes/DMIResource.cs @@ -68,7 +68,8 @@ private void ProcessDMIData() { clone.Crop(new Rectangle(frame.X, frame.Y, frame.X + description.Width, frame.Y + description.Height)); }); - var cursor = clyde.CreateCursor(stateImage, state.Hotspot); + var hotspot = state.Hotspot ?? (0, stateImage.Height - 1); // Default to the top-left + var cursor = clyde.CreateCursor(stateImage, hotspot); return cursor; } diff --git a/OpenDreamShared/Resources/DMIParser.cs b/OpenDreamShared/Resources/DMIParser.cs index 1381de22f4..527fd0fafb 100644 --- a/OpenDreamShared/Resources/DMIParser.cs +++ b/OpenDreamShared/Resources/DMIParser.cs @@ -126,7 +126,11 @@ public sealed class ParsedDMIState(string name) { public string Name = name; public bool Loop = true; public bool Rewind; - public Vector2i Hotspot = (0, 31); + + /// + /// The part of the image considered the tip when this is used as a custom cursor + /// + public Vector2i? Hotspot; // TODO: This can only contain either 1, 4, or 8 directions. Enforcing this could simplify some things. public readonly Dictionary Directions = new();