diff --git a/OpenRA.Game/Game.cs b/OpenRA.Game/Game.cs index 352cfa5999..69f72664f8 100644 --- a/OpenRA.Game/Game.cs +++ b/OpenRA.Game/Game.cs @@ -39,7 +39,7 @@ namespace OpenRA public static ModData ModData; public static Settings Settings; - public static ICursor Cursor; + public static CursorManager Cursor; public static bool HideCursor; static WorldRenderer worldRenderer; static string modLaunchWrapper; @@ -435,25 +435,7 @@ namespace OpenRA if (Cursor != null) Cursor.Dispose(); - if (Settings.Graphics.HardwareCursors) - { - try - { - Cursor = new HardwareCursor(ModData.CursorProvider); - } - catch (Exception e) - { - Log.Write("debug", "Failed to initialize hardware cursors. Falling back to software cursors."); - Log.Write("debug", "Error was: " + e.Message); - - Console.WriteLine("Failed to initialize hardware cursors. Falling back to software cursors."); - Console.WriteLine("Error was: " + e.Message); - - Cursor = new SoftwareCursor(ModData.CursorProvider); - } - } - else - Cursor = new SoftwareCursor(ModData.CursorProvider); + Cursor = new CursorManager(ModData.CursorProvider); PerfHistory.Items["render"].HasNormalTick = false; PerfHistory.Items["batches"].HasNormalTick = false; diff --git a/OpenRA.Game/Graphics/CursorManager.cs b/OpenRA.Game/Graphics/CursorManager.cs new file mode 100644 index 0000000000..23ed06d21e --- /dev/null +++ b/OpenRA.Game/Graphics/CursorManager.cs @@ -0,0 +1,286 @@ +#region Copyright & License Information +/* + * Copyright 2007-2020 The OpenRA Developers (see AUTHORS) + * This file is part of OpenRA, which is free software. It is made + * available to you under the terms of the GNU General Public License + * as published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. For more + * information, see COPYING. + */ +#endregion + +using System; +using System.Collections.Generic; +using OpenRA.Primitives; + +namespace OpenRA.Graphics +{ + public sealed class CursorManager + { + class Cursor + { + public string Name; + public int2 PaddedSize; + public Rectangle Bounds; + + public int Length; + public Sprite[] Sprites; + public IHardwareCursor[] Cursors; + } + + readonly Dictionary cursors = new Dictionary(); + readonly SheetBuilder sheetBuilder; + + Cursor cursor; + bool isLocked = false; + int2 lockedPosition; + bool hardwareCursorsDisabled = false; + + public readonly bool DoubleCursorSize; + + public CursorManager(CursorProvider cursorProvider) + { + // Cursor settings are applied on game start + DoubleCursorSize = Game.Settings.Graphics.CursorDouble; + hardwareCursorsDisabled = !Game.Settings.Graphics.HardwareCursors; + + sheetBuilder = new SheetBuilder(SheetType.BGRA); + foreach (var kv in cursorProvider.Cursors) + { + var frames = kv.Value.Frames; + var palette = !string.IsNullOrEmpty(kv.Value.Palette) ? cursorProvider.Palettes[kv.Value.Palette] : null; + + var c = new Cursor + { + Name = kv.Key, + Bounds = Rectangle.FromLTRB(0, 0, 1, 1), + + Length = 0, + Sprites = new Sprite[frames.Length], + Cursors = new IHardwareCursor[frames.Length] + }; + + // Hardware cursors have a number of odd platform-specific bugs/limitations. + // Reduce the number of edge cases by padding the individual frames such that: + // - the hotspot is inside the frame bounds (enforced by SDL) + // - all frames within a sequence have the same size (needed for macOS 10.15) + // - the frame size is a multiple of 8 (needed for Windows) + foreach (var f in frames) + { + // Hotspot is specified relative to the center of the frame + var hotspot = f.Offset.ToInt2() - kv.Value.Hotspot - new int2(f.Size) / 2; + + // SheetBuilder expects data in BGRA + var data = FrameToBGRA(kv.Key, f, palette); + c.Sprites[c.Length++] = sheetBuilder.Add(data, f.Size, 0, hotspot); + + // Bounds relative to the hotspot + c.Bounds = Rectangle.Union(c.Bounds, new Rectangle(hotspot, f.Size)); + } + + // Pad bottom-right edge to make the frame size a multiple of 8 + c.PaddedSize = 8 * new int2((c.Bounds.Width + 7) / 8, (c.Bounds.Height + 7) / 8); + + cursors.Add(kv.Key, c); + } + + CreateOrUpdateHardwareCursors(); + + foreach (var s in sheetBuilder.AllSheets) + s.ReleaseBuffer(); + + Update(); + } + + void CreateOrUpdateHardwareCursors() + { + if (hardwareCursorsDisabled) + return; + + // Dispose any existing cursors to avoid leaking native resources + ClearHardwareCursors(); + + try + { + foreach (var kv in cursors) + { + var template = kv.Value; + for (var i = 0; i < template.Sprites.Length; i++) + { + if (template.Cursors[i] != null) + template.Cursors[i].Dispose(); + + // Calculate the padding to position the frame within sequenceBounds + var paddingTL = -(template.Bounds.Location - template.Sprites[i].Offset.XY.ToInt2()); + var paddingBR = template.PaddedSize - new int2(template.Sprites[i].Bounds.Size) - paddingTL; + + template.Cursors[i] = CreateHardwareCursor(kv.Key, template.Sprites[i], paddingTL, paddingBR, -template.Bounds.Location); + } + } + } + catch (Exception e) + { + Log.Write("debug", "Failed to initialize hardware cursors. Falling back to software cursors."); + Log.Write("debug", "Error was: " + e.Message); + + Console.WriteLine("Failed to initialize hardware cursors. Falling back to software cursors."); + Console.WriteLine("Error was: " + e.Message); + + ClearHardwareCursors(); + } + } + + public void SetCursor(string cursorName) + { + if ((cursorName == null && cursor == null) || (cursor != null && cursorName == cursor.Name)) + return; + + if (cursorName == null || !cursors.TryGetValue(cursorName, out cursor)) + cursor = null; + + Update(); + } + + int frame; + int ticks; + + public void Tick() + { + if (cursor == null || cursor.Cursors.Length == 1) + return; + + if (++ticks > 2) + { + ticks -= 2; + frame++; + + Update(); + } + } + + void Update() + { + if (cursor != null && frame >= cursor.Cursors.Length) + frame %= cursor.Cursors.Length; + + if (cursor == null || isLocked) + Game.Renderer.Window.SetHardwareCursor(null); + else + Game.Renderer.Window.SetHardwareCursor(cursor.Cursors[frame]); + } + + public void Render(Renderer renderer) + { + // Cursor is hidden + if (cursor == null) + return; + + // Hardware cursor is enabled + if (!isLocked && cursor.Cursors[frame % cursor.Length] != null) + return; + + // Render cursor in software + var doubleCursor = DoubleCursorSize && cursor.Name != "default"; + var cursorSprite = cursor.Sprites[frame % cursor.Length]; + var cursorSize = doubleCursor ? 2.0f * cursorSprite.Size : cursorSprite.Size; + var mousePos = isLocked ? lockedPosition : Viewport.LastMousePos; + + renderer.RgbaSpriteRenderer.DrawSprite(cursorSprite, + mousePos, + cursorSize); + } + + public void Lock() + { + lockedPosition = Viewport.LastMousePos; + Game.Renderer.Window.SetRelativeMouseMode(true); + isLocked = true; + Update(); + } + + public void Unlock() + { + Game.Renderer.Window.SetRelativeMouseMode(false); + isLocked = false; + Update(); + } + + public static byte[] FrameToBGRA(string name, ISpriteFrame frame, ImmutablePalette palette) + { + // Data is already in BGRA format + if (frame.Type == SpriteFrameType.BGRA) + return frame.Data; + + // Cursors may be either native BGRA or Indexed. + // Indexed sprites are converted to BGRA using the referenced palette. + // All palettes must be explicitly referenced, even if they are embedded in the sprite. + if (frame.Type == SpriteFrameType.Indexed && palette == null) + throw new InvalidOperationException("Cursor sequence `{0}` attempted to load an indexed sprite but does not define Palette".F(name)); + + var width = frame.Size.Width; + var height = frame.Size.Height; + var data = new byte[4 * width * height]; + for (var j = 0; j < height; j++) + { + for (var i = 0; i < width; i++) + { + var bytes = BitConverter.GetBytes(palette[frame.Data[j * width + i]]); + var c = palette[frame.Data[j * width + i]]; + var k = 4 * (j * width + i); + + // Convert RGBA to BGRA + data[k] = bytes[2]; + data[k + 1] = bytes[1]; + data[k + 2] = bytes[0]; + data[k + 3] = bytes[3]; + } + } + + return data; + } + + IHardwareCursor CreateHardwareCursor(string name, Sprite data, int2 paddingTL, int2 paddingBR, int2 hotspot) + { + var size = data.Bounds.Size; + var srcStride = data.Sheet.Size.Width; + var srcData = data.Sheet.GetData(); + var newWidth = paddingTL.X + size.Width + paddingBR.X; + var newHeight = paddingTL.Y + size.Height + paddingBR.Y; + var rgbaData = new byte[4 * newWidth * newHeight]; + for (var j = 0; j < size.Height; j++) + { + for (var i = 0; i < size.Width; i++) + { + var src = 4 * ((j + data.Bounds.Top) * srcStride + data.Bounds.Left + i); + var dest = 4 * ((j + paddingTL.Y) * newWidth + i + paddingTL.X); + Array.Copy(srcData, src, rgbaData, dest, 4); + } + } + + return Game.Renderer.Window.CreateHardwareCursor(name, new Size(newWidth, newHeight), rgbaData, hotspot); + } + + void ClearHardwareCursors() + { + foreach (var c in cursors.Values) + { + for (var i = 0; i < c.Cursors.Length; i++) + { + if (c.Cursors[i] != null) + { + c.Cursors[i].Dispose(); + c.Cursors[i] = null; + } + } + } + } + + public void Dispose() + { + ClearHardwareCursors(); + + cursors.Clear(); + sheetBuilder.Dispose(); + } + } +} diff --git a/OpenRA.Game/Graphics/CursorProvider.cs b/OpenRA.Game/Graphics/CursorProvider.cs index 54bf495879..c03bd605b4 100644 --- a/OpenRA.Game/Graphics/CursorProvider.cs +++ b/OpenRA.Game/Graphics/CursorProvider.cs @@ -20,7 +20,6 @@ namespace OpenRA.Graphics { public readonly IReadOnlyDictionary Cursors; public readonly IReadOnlyDictionary Palettes; - public readonly bool DoubleCursorSize; public CursorProvider(ModData modData) { @@ -49,9 +48,6 @@ namespace OpenRA.Graphics cursors.Add(sequence.Key, new CursorSequence(frameCache, sequence.Key, s.Key, s.Value.Value, sequence.Value)); Cursors = cursors.AsReadOnly(); - - // Cursor size changes are applied on game start - DoubleCursorSize = Game.Settings.Graphics.CursorDouble; } public bool HasCursorSequence(string cursor) diff --git a/OpenRA.Game/Graphics/HardwareCursor.cs b/OpenRA.Game/Graphics/HardwareCursor.cs deleted file mode 100644 index 1e6d9ea842..0000000000 --- a/OpenRA.Game/Graphics/HardwareCursor.cs +++ /dev/null @@ -1,184 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2020 The OpenRA Developers (see AUTHORS) - * This file is part of OpenRA, which is free software. It is made - * available to you under the terms of the GNU General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. For more - * information, see COPYING. - */ -#endregion - -using System; -using System.Collections.Generic; -using OpenRA.Primitives; - -namespace OpenRA.Graphics -{ - public sealed class HardwareCursor : ICursor - { - readonly Dictionary hardwareCursors = new Dictionary(); - readonly CursorProvider cursorProvider; - readonly Dictionary sprites = new Dictionary(); - readonly SheetBuilder sheetBuilder; - - CursorSequence cursor; - bool isLocked = false; - int2 lockedPosition; - - public HardwareCursor(CursorProvider cursorProvider) - { - this.cursorProvider = cursorProvider; - - sheetBuilder = new SheetBuilder(SheetType.Indexed); - foreach (var kv in cursorProvider.Cursors) - { - var frames = kv.Value.Frames; - var palette = !string.IsNullOrEmpty(kv.Value.Palette) ? cursorProvider.Palettes[kv.Value.Palette] : null; - - // Hardware cursors have a number of odd platform-specific bugs/limitations. - // Reduce the number of edge cases by padding the individual frames such that: - // - the hotspot is inside the frame bounds (enforced by SDL) - // - all frames within a sequence have the same size (needed for macOS 10.15) - // - the frame size is a multiple of 8 (needed for Windows) - var sequenceBounds = Rectangle.FromLTRB(0, 0, 1, 1); - var frameHotspots = new int2[frames.Length]; - for (var i = 0; i < frames.Length; i++) - { - // Hotspot relative to the center of the frame - frameHotspots[i] = kv.Value.Hotspot - frames[i].Offset.ToInt2() + new int2(frames[i].Size) / 2; - - // Bounds relative to the hotspot - sequenceBounds = Rectangle.Union(sequenceBounds, new Rectangle(-frameHotspots[i], frames[i].Size)); - } - - // Pad bottom-right edge to make the frame size a multiple of 8 - var paddedSize = 8 * new int2((sequenceBounds.Width + 7) / 8, (sequenceBounds.Height + 7) / 8); - - var cursors = new IHardwareCursor[frames.Length]; - var frameSprites = new Sprite[frames.Length]; - for (var i = 0; i < frames.Length; i++) - { - // Software rendering is used when the cursor is locked - // SheetBuilder expects data in BGRA - var data = SoftwareCursor.FrameToBGRA(kv.Key, frames[i], palette); - frameSprites[i] = sheetBuilder.Add(data, frames[i].Size, 0, frames[i].Offset); - - // Calculate the padding to position the frame within sequenceBounds - var paddingTL = -(sequenceBounds.Location + frameHotspots[i]); - var paddingBR = paddedSize - new int2(frames[i].Size) - paddingTL; - cursors[i] = CreateCursor(kv.Key, data, frames[i].Size, paddingTL, paddingBR, -sequenceBounds.Location); - } - - hardwareCursors.Add(kv.Key, cursors); - sprites.Add(kv.Key, frameSprites); - } - - sheetBuilder.Current.ReleaseBuffer(); - - Update(); - } - - IHardwareCursor CreateCursor(string name, byte[] data, Size size, int2 paddingTL, int2 paddingBR, int2 hotspot) - { - // Pad the cursor and convert to RBGA - var newWidth = paddingTL.X + size.Width + paddingBR.X; - var newHeight = paddingTL.Y + size.Height + paddingBR.Y; - var rgbaData = new byte[4 * newWidth * newHeight]; - for (var j = 0; j < size.Height; j++) - { - for (var i = 0; i < size.Width; i++) - { - var src = 4 * (j * size.Width + i); - var dest = 4 * ((j + paddingTL.Y) * newWidth + i + paddingTL.X); - - // CreateHardwareCursor expects data in RGBA - rgbaData[dest] = data[src + 2]; - rgbaData[dest + 1] = data[src + 1]; - rgbaData[dest + 2] = data[src]; - rgbaData[dest + 3] = data[src + 3]; - } - } - - return Game.Renderer.Window.CreateHardwareCursor(name, new Size(newWidth, newHeight), rgbaData, hotspot); - } - - public void SetCursor(string cursorName) - { - if ((cursorName == null && cursor == null) || (cursor != null && cursorName == cursor.Name)) - return; - - if (cursorName == null || !cursorProvider.Cursors.TryGetValue(cursorName, out cursor)) - cursor = null; - - Update(); - } - - int frame; - int ticks; - - public void Tick() - { - if (cursor == null || cursor.Length == 1) - return; - - if (++ticks > 2) - { - ticks -= 2; - frame++; - - Update(); - } - } - - void Update() - { - if (cursor != null && frame >= cursor.Length) - frame %= cursor.Length; - - if (cursor == null || isLocked) - Game.Renderer.Window.SetHardwareCursor(null); - else - Game.Renderer.Window.SetHardwareCursor(hardwareCursors[cursor.Name][frame]); - } - - public void Render(Renderer renderer) - { - if (cursor == null || !isLocked) - return; - - var cursorSequence = cursorProvider.GetCursorSequence(cursor.Name); - var cursorSprite = sprites[cursor.Name][frame]; - - var cursorOffset = cursorSequence.Hotspot + (0.5f * cursorSprite.Size.XY).ToInt2(); - renderer.RgbaSpriteRenderer.DrawSprite(cursorSprite, - lockedPosition - cursorOffset, - cursorSprite.Size); - } - - public void Lock() - { - lockedPosition = Viewport.LastMousePos; - Game.Renderer.Window.SetRelativeMouseMode(true); - isLocked = true; - Update(); - } - - public void Unlock() - { - Game.Renderer.Window.SetRelativeMouseMode(false); - isLocked = false; - Update(); - } - - public void Dispose() - { - foreach (var cursors in hardwareCursors) - foreach (var cursor in cursors.Value) - cursor.Dispose(); - - sheetBuilder.Dispose(); - hardwareCursors.Clear(); - } - } -} diff --git a/OpenRA.Game/Graphics/SoftwareCursor.cs b/OpenRA.Game/Graphics/SoftwareCursor.cs deleted file mode 100644 index ae85b1cf44..0000000000 --- a/OpenRA.Game/Graphics/SoftwareCursor.cs +++ /dev/null @@ -1,140 +0,0 @@ -#region Copyright & License Information -/* - * Copyright 2007-2020 The OpenRA Developers (see AUTHORS) - * This file is part of OpenRA, which is free software. It is made - * available to you under the terms of the GNU General Public License - * as published by the Free Software Foundation, either version 3 of - * the License, or (at your option) any later version. For more - * information, see COPYING. - */ -#endregion - -using System; -using System.Collections.Generic; -using System.Linq; - -namespace OpenRA.Graphics -{ - public interface ICursor : IDisposable - { - void Render(Renderer renderer); - void SetCursor(string cursor); - void Tick(); - void Lock(); - void Unlock(); - } - - public sealed class SoftwareCursor : ICursor - { - readonly Dictionary sprites = new Dictionary(); - readonly CursorProvider cursorProvider; - SheetBuilder sheetBuilder; - - bool isLocked = false; - int2 lockedPosition; - - public SoftwareCursor(CursorProvider cursorProvider) - { - this.cursorProvider = cursorProvider; - - sheetBuilder = new SheetBuilder(SheetType.BGRA, 1024); - - foreach (var kv in cursorProvider.Cursors) - { - var palette = !string.IsNullOrEmpty(kv.Value.Palette) ? cursorProvider.Palettes[kv.Value.Palette] : null; - var s = kv.Value.Frames.Select(f => sheetBuilder.Add(FrameToBGRA(kv.Key, f, palette), f.Size, 0, f.Offset)).ToArray(); - - sprites.Add(kv.Key, s); - } - - sheetBuilder.Current.ReleaseBuffer(); - - Game.Renderer.Window.SetHardwareCursor(null); - } - - public static byte[] FrameToBGRA(string name, ISpriteFrame frame, ImmutablePalette palette) - { - // Data is already in BGRA format - if (frame.Type == SpriteFrameType.BGRA) - return frame.Data; - - // Cursors may be either native BGRA or Indexed. - // Indexed sprites are converted to BGRA using the referenced palette. - // All palettes must be explicitly referenced, even if they are embedded in the sprite. - if (frame.Type == SpriteFrameType.Indexed && palette == null) - throw new InvalidOperationException("Cursor sequence `{0}` attempted to load an indexed sprite but does not define Palette".F(name)); - - var width = frame.Size.Width; - var height = frame.Size.Height; - var data = new byte[4 * width * height]; - for (var j = 0; j < height; j++) - { - for (var i = 0; i < width; i++) - { - var bytes = BitConverter.GetBytes(palette[frame.Data[j * width + i]]); - var c = palette[frame.Data[j * width + i]]; - var k = 4 * (j * width + i); - - // Convert RGBA to BGRA - data[k] = bytes[2]; - data[k + 1] = bytes[1]; - data[k + 2] = bytes[0]; - data[k + 3] = bytes[3]; - } - } - - return data; - } - - string cursorName; - public void SetCursor(string cursor) - { - cursorName = cursor; - } - - float cursorFrame; - public void Tick() - { - cursorFrame += 0.5f; - } - - public void Render(Renderer renderer) - { - if (cursorName == null) - return; - - var doubleCursor = cursorProvider.DoubleCursorSize && cursorName != "default"; - var cursorSequence = cursorProvider.GetCursorSequence(cursorName); - var cursorSprite = sprites[cursorName][(int)cursorFrame % cursorSequence.Length]; - var cursorSize = doubleCursor ? 2.0f * cursorSprite.Size : cursorSprite.Size; - - var cursorOffset = doubleCursor ? - (2 * cursorSequence.Hotspot) + cursorSprite.Size.XY.ToInt2() : - cursorSequence.Hotspot + (0.5f * cursorSprite.Size.XY).ToInt2(); - - var mousePos = isLocked ? lockedPosition : Viewport.LastMousePos; - - renderer.RgbaSpriteRenderer.DrawSprite(cursorSprite, - mousePos - cursorOffset, - cursorSize); - } - - public void Lock() - { - Game.Renderer.Window.SetRelativeMouseMode(true); - lockedPosition = Viewport.LastMousePos; - isLocked = true; - } - - public void Unlock() - { - Game.Renderer.Window.SetRelativeMouseMode(false); - isLocked = false; - } - - public void Dispose() - { - sheetBuilder.Dispose(); - } - } -} diff --git a/OpenRA.Mods.Common/Widgets/MouseAttachmentWidget.cs b/OpenRA.Mods.Common/Widgets/MouseAttachmentWidget.cs index fa4a8e64f4..29e827dc57 100644 --- a/OpenRA.Mods.Common/Widgets/MouseAttachmentWidget.cs +++ b/OpenRA.Mods.Common/Widgets/MouseAttachmentWidget.cs @@ -20,14 +20,12 @@ namespace OpenRA.Mods.Common.Widgets Sprite sprite; readonly WorldRenderer worldRenderer; - readonly CursorProvider cursorProvider; string palette; int2 location; [ObjectCreator.UseCtor] public MouseAttachmentWidget(ModData modData, WorldRenderer worldRenderer) { - cursorProvider = modData.CursorProvider; this.worldRenderer = worldRenderer; } @@ -35,9 +33,8 @@ namespace OpenRA.Mods.Common.Widgets { if (sprite != null && palette != null) { - var scale = Game.Cursor is SoftwareCursor && cursorProvider.DoubleCursorSize ? 2 : 1; var directionPalette = worldRenderer.Palette(palette); - WidgetUtils.DrawSHPCentered(sprite, ChildOrigin, directionPalette, scale); + WidgetUtils.DrawSHPCentered(sprite, ChildOrigin, directionPalette, Game.Cursor.DoubleCursorSize ? 2 : 1); } } diff --git a/OpenRA.Mods.Common/Widgets/TooltipContainerWidget.cs b/OpenRA.Mods.Common/Widgets/TooltipContainerWidget.cs index 300e3de4fb..b9a08cb1d6 100644 --- a/OpenRA.Mods.Common/Widgets/TooltipContainerWidget.cs +++ b/OpenRA.Mods.Common/Widgets/TooltipContainerWidget.cs @@ -19,7 +19,6 @@ namespace OpenRA.Mods.Common.Widgets public class TooltipContainerWidget : Widget { static readonly Action Nothing = () => { }; - readonly CursorProvider cursorProvider; public int2 CursorOffset = new int2(0, 20); public int BottomEdgeYOffset = -5; @@ -30,7 +29,6 @@ namespace OpenRA.Mods.Common.Widgets public TooltipContainerWidget() { - cursorProvider = Game.ModData.CursorProvider; IsVisible = () => Game.RunTime > Viewport.LastMoveRunTime + TooltipDelayMilliseconds; } @@ -54,7 +52,7 @@ namespace OpenRA.Mods.Common.Widgets { get { - var pos = Viewport.LastMousePos + (cursorProvider.DoubleCursorSize ? CursorOffset * 2 : CursorOffset); + var pos = Viewport.LastMousePos + (Game.Cursor.DoubleCursorSize ? CursorOffset * 2 : CursorOffset); if (tooltip != null) { // If the tooltip overlaps the right edge of the screen, move it left until it fits @@ -63,7 +61,7 @@ namespace OpenRA.Mods.Common.Widgets // If the tooltip overlaps the bottom edge of the screen, switch tooltip above cursor if (pos.Y + tooltip.Bounds.Bottom > Game.Renderer.Resolution.Height) - pos = pos.WithY(Viewport.LastMousePos.Y + (cursorProvider.DoubleCursorSize ? 2 : 1) * BottomEdgeYOffset - tooltip.Bounds.Height); + pos = pos.WithY(Viewport.LastMousePos.Y + (Game.Cursor.DoubleCursorSize ? 2 : 1) * BottomEdgeYOffset - tooltip.Bounds.Height); } return pos;