using System.Linq; using System.Numerics; using Content.Client.Administration.Managers; using Content.Client.ContextMenu.UI; using Content.Client.Decals; using Content.Client.Gameplay; using Content.Client.UserInterface.Controls; using Content.Client.UserInterface.Systems.Gameplay; using Content.Client.Verbs; using Content.Shared.Administration; using Content.Shared.Decals; using Content.Shared.Input; using Content.Shared.Maps; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Client.Input; using Robust.Client.Placement; using Robust.Client.ResourceManagement; using Robust.Client.UserInterface; using Robust.Client.UserInterface.CustomControls; using Robust.Shared.Enums; using Robust.Shared.Input.Binding; using Robust.Shared.Map; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Markdown.Sequence; using Robust.Shared.Serialization.Markdown.Value; using Robust.Shared.Timing; using Robust.Shared.Utility; using static System.StringComparison; using static Robust.Client.UserInterface.Controls.BaseButton; using static Robust.Client.UserInterface.Controls.LineEdit; using static Robust.Client.UserInterface.Controls.OptionButton; using static Robust.Shared.Input.Binding.PointerInputCmdHandler; namespace Content.Client.Mapping; public sealed class MappingState : GameplayStateBase { [Dependency] private readonly IClientAdminManager _admin = default!; [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IEntityNetworkManager _entityNetwork = default!; [Dependency] private readonly IInputManager _input = default!; [Dependency] private readonly ILogManager _log = default!; [Dependency] private readonly IMapManager _mapMan = default!; [Dependency] private readonly MappingManager _mapping = default!; [Dependency] private readonly IOverlayManager _overlays = default!; [Dependency] private readonly IPlacementManager _placement = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IResourceCache _resources = default!; [Dependency] private readonly IGameTiming _timing = default!; private EntityMenuUIController _entityMenuController = default!; private DecalPlacementSystem _decal = default!; private SpriteSystem _sprite = default!; private TransformSystem _transform = default!; private VerbSystem _verbs = default!; private readonly ISawmill _sawmill; private readonly GameplayStateLoadController _loadController; private bool _setup; private readonly List _allPrototypes = new(); private readonly Dictionary _allPrototypesDict = new(); private readonly Dictionary> _idDict = new(); private readonly List _prototypes = new(); private (TimeSpan At, MappingSpawnButton Button)? _lastClicked; private Control? _scrollTo; private bool _updatePlacement; private bool _updateEraseDecal; private MappingScreen Screen => (MappingScreen) UserInterfaceManager.ActiveScreen!; private MainViewport Viewport => UserInterfaceManager.ActiveScreen!.GetWidget()!; public CursorState State { get; set; } public MappingState() { IoCManager.InjectDependencies(this); _sawmill = _log.GetSawmill("mapping"); _loadController = UserInterfaceManager.GetUIController(); } protected override void Startup() { EnsureSetup(); base.Startup(); UserInterfaceManager.LoadScreen(); _loadController.LoadScreen(); var context = _input.Contexts.GetContext("common"); context.AddFunction(ContentKeyFunctions.MappingUnselect); context.AddFunction(ContentKeyFunctions.SaveMap); context.AddFunction(ContentKeyFunctions.MappingEnablePick); context.AddFunction(ContentKeyFunctions.MappingEnableDelete); context.AddFunction(ContentKeyFunctions.MappingPick); context.AddFunction(ContentKeyFunctions.MappingRemoveDecal); context.AddFunction(ContentKeyFunctions.MappingCancelEraseDecal); context.AddFunction(ContentKeyFunctions.MappingOpenContextMenu); Screen.DecalSystem = _decal; Screen.Prototypes.SearchBar.OnTextChanged += OnSearch; Screen.Prototypes.CollapseAllButton.OnPressed += OnCollapseAll; Screen.Prototypes.ClearSearchButton.OnPressed += OnClearSearch; Screen.Prototypes.GetPrototypeData += OnGetData; Screen.Prototypes.SelectionChanged += OnSelected; Screen.Prototypes.CollapseToggled += OnCollapseToggled; Screen.Pick.OnPressed += OnPickPressed; Screen.Delete.OnPressed += OnDeletePressed; Screen.EntityReplaceButton.OnToggled += OnEntityReplacePressed; Screen.EntityPlacementMode.OnItemSelected += OnEntityPlacementSelected; Screen.EraseEntityButton.OnToggled += OnEraseEntityPressed; Screen.EraseDecalButton.OnToggled += OnEraseDecalPressed; _placement.PlacementChanged += OnPlacementChanged; CommandBinds.Builder .Bind(ContentKeyFunctions.MappingUnselect, new PointerInputCmdHandler(HandleMappingUnselect, outsidePrediction: true)) .Bind(ContentKeyFunctions.SaveMap, new PointerInputCmdHandler(HandleSaveMap, outsidePrediction: true)) .Bind(ContentKeyFunctions.MappingEnablePick, new PointerStateInputCmdHandler(HandleEnablePick, HandleDisablePick, outsidePrediction: true)) .Bind(ContentKeyFunctions.MappingEnableDelete, new PointerStateInputCmdHandler(HandleEnableDelete, HandleDisableDelete, outsidePrediction: true)) .Bind(ContentKeyFunctions.MappingPick, new PointerInputCmdHandler(HandlePick, outsidePrediction: true)) .Bind(ContentKeyFunctions.MappingRemoveDecal, new PointerInputCmdHandler(HandleEditorCancelPlace, outsidePrediction: true)) .Bind(ContentKeyFunctions.MappingCancelEraseDecal, new PointerInputCmdHandler(HandleCancelEraseDecal, outsidePrediction: true)) .Bind(ContentKeyFunctions.MappingOpenContextMenu, new PointerInputCmdHandler(HandleOpenContextMenu, outsidePrediction: true)) .Register(); _overlays.AddOverlay(new MappingOverlay(this)); _prototypeManager.PrototypesReloaded += OnPrototypesReloaded; Screen.Prototypes.UpdateVisible(_prototypes); } private void OnPrototypesReloaded(PrototypesReloadedEventArgs obj) { if (!obj.WasModified() && !obj.WasModified() && !obj.WasModified()) { return; } ReloadPrototypes(); } private bool HandleOpenContextMenu(in PointerInputCmdArgs args) { Deselect(); var coords = _transform.ToMapCoordinates(args.Coordinates); if (_verbs.TryGetEntityMenuEntities(coords, out var entities)) _entityMenuController.OpenRootMenu(entities); return true; } protected override void Shutdown() { CommandBinds.Unregister(); Screen.Prototypes.SearchBar.OnTextChanged -= OnSearch; Screen.Prototypes.CollapseAllButton.OnPressed -= OnCollapseAll; Screen.Prototypes.ClearSearchButton.OnPressed -= OnClearSearch; Screen.Prototypes.GetPrototypeData -= OnGetData; Screen.Prototypes.SelectionChanged -= OnSelected; Screen.Prototypes.CollapseToggled -= OnCollapseToggled; Screen.Pick.OnPressed -= OnPickPressed; Screen.Delete.OnPressed -= OnDeletePressed; Screen.EntityReplaceButton.OnToggled -= OnEntityReplacePressed; Screen.EntityPlacementMode.OnItemSelected -= OnEntityPlacementSelected; Screen.EraseEntityButton.OnToggled -= OnEraseEntityPressed; Screen.EraseDecalButton.OnToggled -= OnEraseDecalPressed; _placement.PlacementChanged -= OnPlacementChanged; _prototypeManager.PrototypesReloaded -= OnPrototypesReloaded; UserInterfaceManager.ClearWindows(); _loadController.UnloadScreen(); UserInterfaceManager.UnloadScreen(); var context = _input.Contexts.GetContext("common"); context.RemoveFunction(ContentKeyFunctions.MappingUnselect); context.RemoveFunction(ContentKeyFunctions.SaveMap); context.RemoveFunction(ContentKeyFunctions.MappingEnablePick); context.RemoveFunction(ContentKeyFunctions.MappingEnableDelete); context.RemoveFunction(ContentKeyFunctions.MappingPick); context.RemoveFunction(ContentKeyFunctions.MappingRemoveDecal); context.RemoveFunction(ContentKeyFunctions.MappingCancelEraseDecal); context.RemoveFunction(ContentKeyFunctions.MappingOpenContextMenu); _overlays.RemoveOverlay(); base.Shutdown(); } private void EnsureSetup() { if (_setup) return; _setup = true; _entityMenuController = UserInterfaceManager.GetUIController(); _decal = _entityManager.System(); _sprite = _entityManager.System(); _transform = _entityManager.System(); _verbs = _entityManager.System(); ReloadPrototypes(); } private void ReloadPrototypes() { var entities = new MappingPrototype(null, Loc.GetString("mapping-entities")) { Children = new List() }; _prototypes.Add(entities); var mappings = new Dictionary(); foreach (var entity in _prototypeManager.EnumeratePrototypes()) { Register(entity, entity.ID, entities); } Sort(mappings, entities); mappings.Clear(); var tiles = new MappingPrototype(null, Loc.GetString("mapping-tiles")) { Children = new List() }; _prototypes.Add(tiles); foreach (var tile in _prototypeManager.EnumeratePrototypes()) { Register(tile, tile.ID, tiles); } Sort(mappings, tiles); mappings.Clear(); var decals = new MappingPrototype(null, Loc.GetString("mapping-decals")) { Children = new List() }; _prototypes.Add(decals); foreach (var decal in _prototypeManager.EnumeratePrototypes()) { Register(decal, decal.ID, decals); } Sort(mappings, decals); mappings.Clear(); } private void Sort(Dictionary prototypes, MappingPrototype topLevel) { static int Compare(MappingPrototype a, MappingPrototype b) { return string.Compare(a.Name, b.Name, OrdinalIgnoreCase); } topLevel.Children ??= new List(); foreach (var prototype in prototypes.Values) { if (prototype.Parents == null && prototype != topLevel) { prototype.Parents = new List { topLevel }; topLevel.Children.Add(prototype); } prototype.Parents?.Sort(Compare); prototype.Children?.Sort(Compare); } topLevel.Children.Sort(Compare); } private MappingPrototype? Register(T? prototype, string id, MappingPrototype topLevel) where T : class, IPrototype, IInheritingPrototype { { if (prototype == null && _prototypeManager.TryIndex(id, out prototype) && prototype is EntityPrototype entity) { if (entity.HideSpawnMenu || entity.Abstract) prototype = null; } } if (prototype == null) { if (!_prototypeManager.TryGetMapping(typeof(T), id, out var node)) { _sawmill.Error($"No {nameof(T)} found with id {id}"); return null; } var ids = _idDict.GetOrNew(typeof(T)); if (ids.TryGetValue(id, out var mapping)) { return mapping; } else { var name = node.TryGet("name", out ValueDataNode? nameNode) ? nameNode.Value : id; if (node.TryGet("suffix", out ValueDataNode? suffix)) name = $"{name} [{suffix.Value}]"; mapping = new MappingPrototype(prototype, name); _allPrototypes.Add(mapping); ids.Add(id, mapping); if (node.TryGet("parent", out ValueDataNode? parentValue)) { var parent = Register(null, parentValue.Value, topLevel); if (parent != null) { mapping.Parents ??= new List(); mapping.Parents.Add(parent); parent.Children ??= new List(); parent.Children.Add(mapping); } } else if (node.TryGet("parent", out SequenceDataNode? parentSequence)) { foreach (var parentNode in parentSequence.Cast()) { var parent = Register(null, parentNode.Value, topLevel); if (parent != null) { mapping.Parents ??= new List(); mapping.Parents.Add(parent); parent.Children ??= new List(); parent.Children.Add(mapping); } } } else { topLevel.Children ??= new List(); topLevel.Children.Add(mapping); mapping.Parents ??= new List(); mapping.Parents.Add(topLevel); } return mapping; } } else { var ids = _idDict.GetOrNew(typeof(T)); if (ids.TryGetValue(id, out var mapping)) { return mapping; } else { var entity = prototype as EntityPrototype; var name = entity?.Name ?? prototype.ID; if (!string.IsNullOrWhiteSpace(entity?.EditorSuffix)) name = $"{name} [{entity.EditorSuffix}]"; mapping = new MappingPrototype(prototype, name); _allPrototypes.Add(mapping); _allPrototypesDict.Add(prototype, mapping); ids.Add(prototype.ID, mapping); } if (prototype.Parents == null) { topLevel.Children ??= new List(); topLevel.Children.Add(mapping); mapping.Parents ??= new List(); mapping.Parents.Add(topLevel); return mapping; } foreach (var parentId in prototype.Parents) { var parent = Register(null, parentId, topLevel); if (parent != null) { mapping.Parents ??= new List(); mapping.Parents.Add(parent); parent.Children ??= new List(); parent.Children.Add(mapping); } } return mapping; } } private void OnPlacementChanged(object? sender, EventArgs e) { _updatePlacement = true; } protected override void OnKeyBindStateChanged(ViewportBoundKeyEventArgs args) { if (args.Viewport == null) base.OnKeyBindStateChanged(new ViewportBoundKeyEventArgs(args.KeyEventArgs, Viewport.Viewport)); else base.OnKeyBindStateChanged(args); } private void OnSearch(LineEditEventArgs args) { if (string.IsNullOrEmpty(args.Text)) { Screen.Prototypes.PrototypeList.Visible = true; Screen.Prototypes.SearchList.Visible = false; return; } var matches = new List(); foreach (var prototype in _allPrototypes) { if (prototype.Name.Contains(args.Text, OrdinalIgnoreCase)) matches.Add(prototype); } matches.Sort(static (a, b) => string.Compare(a.Name, b.Name, OrdinalIgnoreCase)); Screen.Prototypes.PrototypeList.Visible = false; Screen.Prototypes.SearchList.Visible = true; Screen.Prototypes.Search(matches); } private void OnCollapseAll(ButtonEventArgs args) { foreach (var child in Screen.Prototypes.PrototypeList.Children) { if (child is not MappingSpawnButton button) continue; Collapse(button); } Screen.Prototypes.ScrollContainer.SetScrollValue(new Vector2(0, 0)); } private void OnClearSearch(ButtonEventArgs obj) { Screen.Prototypes.SearchBar.Text = string.Empty; OnSearch(new LineEditEventArgs(Screen.Prototypes.SearchBar, string.Empty)); } private void OnGetData(IPrototype prototype, List textures) { switch (prototype) { case EntityPrototype entity: textures.AddRange(_sprite.GetPrototypeTextures(entity).Select(t => t.Default)); break; case DecalPrototype decal: textures.Add(_sprite.Frame0(decal.Sprite)); break; case ContentTileDefinition tile: if (tile.Sprite?.ToString() is { } sprite) textures.Add(_resources.GetResource(sprite).Texture); break; } } private void OnSelected(MappingPrototype mapping) { if (mapping.Prototype == null) return; var chain = new Stack(); chain.Push(mapping); var parent = mapping.Parents?.FirstOrDefault(); while (parent != null) { chain.Push(parent); parent = parent.Parents?.FirstOrDefault(); } _lastClicked = null; Control? last = null; var children = Screen.Prototypes.PrototypeList.Children; foreach (var prototype in chain) { foreach (var child in children) { if (child is MappingSpawnButton button && button.Prototype == prototype) { UnCollapse(button); OnSelected(button, prototype.Prototype); children = button.ChildrenPrototypes.Children; last = child; break; } } } if (last != null && Screen.Prototypes.PrototypeList.Visible) _scrollTo = last; } private void OnSelected(MappingSpawnButton button, IPrototype? prototype) { var time = _timing.CurTime; if (prototype is DecalPrototype) Screen.SelectDecal(prototype.ID); // Double-click functionality if it's collapsible. if (_lastClicked is { } lastClicked && lastClicked.Button == button && lastClicked.At > time - TimeSpan.FromSeconds(0.333) && string.IsNullOrEmpty(Screen.Prototypes.SearchBar.Text) && button.CollapseButton.Visible) { button.CollapseButton.Pressed = !button.CollapseButton.Pressed; ToggleCollapse(button); button.Button.Pressed = true; Screen.Prototypes.Selected = button; _lastClicked = null; return; } // Toggle if it's the same button (at least if we just unclicked it). if (!button.Button.Pressed && button.Prototype?.Prototype != null && _lastClicked?.Button == button) { _lastClicked = null; Deselect(); return; } _lastClicked = (time, button); if (button.Prototype == null) return; if (Screen.Prototypes.Selected is { } oldButton && oldButton != button) { Deselect(); } Screen.EntityContainer.Visible = false; Screen.DecalContainer.Visible = false; switch (prototype) { case EntityPrototype entity: { var placementId = Screen.EntityPlacementMode.SelectedId; var placement = new PlacementInformation { PlacementOption = placementId > 0 ? EntitySpawnWindow.InitOpts[placementId] : entity.PlacementMode, EntityType = entity.ID, IsTile = false }; Screen.EntityContainer.Visible = true; _decal.SetActive(false); _placement.BeginPlacing(placement); break; } case DecalPrototype decal: _placement.Clear(); _decal.SetActive(true); _decal.UpdateDecalInfo(decal.ID, Color.White, 0, true, 0, false); Screen.DecalContainer.Visible = true; break; case ContentTileDefinition tile: { var placement = new PlacementInformation { PlacementOption = "AlignTileAny", TileType = tile.TileId, IsTile = true }; _decal.SetActive(false); _placement.BeginPlacing(placement); break; } default: _placement.Clear(); break; } Screen.Prototypes.Selected = button; button.Button.Pressed = true; } private void Deselect() { if (Screen.Prototypes.Selected is { } selected) { selected.Button.Pressed = false; Screen.Prototypes.Selected = null; if (selected.Prototype?.Prototype is DecalPrototype) { _decal.SetActive(false); Screen.DecalContainer.Visible = false; } if (selected.Prototype?.Prototype is EntityPrototype) { _placement.Clear(); } if (selected.Prototype?.Prototype is ContentTileDefinition) { _placement.Clear(); } } } private void OnCollapseToggled(MappingSpawnButton button, ButtonToggledEventArgs args) { ToggleCollapse(button); } private void OnPickPressed(ButtonEventArgs args) { if (args.Button.Pressed) EnablePick(); else DisablePick(); } private void OnDeletePressed(ButtonEventArgs obj) { if (obj.Button.Pressed) EnableDelete(); else DisableDelete(); } private void OnEntityReplacePressed(ButtonToggledEventArgs args) { _placement.Replacement = args.Pressed; } private void OnEntityPlacementSelected(ItemSelectedEventArgs args) { Screen.EntityPlacementMode.SelectId(args.Id); if (_placement.CurrentMode != null) { var placement = new PlacementInformation { PlacementOption = EntitySpawnWindow.InitOpts[args.Id], EntityType = _placement.CurrentPermission!.EntityType, TileType = _placement.CurrentPermission.TileType, Range = 2, IsTile = _placement.CurrentPermission.IsTile, }; _placement.BeginPlacing(placement); } } private void OnEraseEntityPressed(ButtonEventArgs args) { if (args.Button.Pressed == _placement.Eraser) return; if (args.Button.Pressed) EnableEraser(); else DisableEraser(); } private void OnEraseDecalPressed(ButtonToggledEventArgs args) { _placement.Clear(); Deselect(); Screen.EraseEntityButton.Pressed = false; _updatePlacement = true; _updateEraseDecal = args.Pressed; } private void EnableEraser() { if (_placement.Eraser) return; _placement.Clear(); _placement.ToggleEraser(); Screen.EntityPlacementMode.Disabled = true; Screen.EraseDecalButton.Pressed = false; Deselect(); } private void DisableEraser() { if (!_placement.Eraser) return; _placement.ToggleEraser(); Screen.EntityPlacementMode.Disabled = false; } private void EnablePick() { Screen.UnPressActionsExcept(Screen.Pick); State = CursorState.Pick; } private void DisablePick() { Screen.Pick.Pressed = false; State = CursorState.None; } private void EnableDelete() { Screen.UnPressActionsExcept(Screen.Delete); State = CursorState.Delete; EnableEraser(); } private void DisableDelete() { Screen.Delete.Pressed = false; State = CursorState.None; DisableEraser(); } private bool HandleMappingUnselect(in PointerInputCmdArgs args) { if (Screen.Prototypes.Selected is not { Prototype.Prototype: DecalPrototype }) return false; Deselect(); return true; } private bool HandleSaveMap(in PointerInputCmdArgs args) { #if FULL_RELEASE return false; #endif if (!_admin.IsAdmin(true) || !_admin.HasFlag(AdminFlags.Host)) return false; SaveMap(); return true; } private bool HandleEnablePick(ICommonSession? session, EntityCoordinates coords, EntityUid uid) { EnablePick(); return true; } private bool HandleDisablePick(ICommonSession? session, EntityCoordinates coords, EntityUid uid) { DisablePick(); return true; } private bool HandleEnableDelete(ICommonSession? session, EntityCoordinates coords, EntityUid uid) { EnableDelete(); return true; } private bool HandleDisableDelete(ICommonSession? session, EntityCoordinates coords, EntityUid uid) { DisableDelete(); return true; } private bool HandlePick(ICommonSession? session, EntityCoordinates coords, EntityUid uid) { if (State != CursorState.Pick) return false; MappingPrototype? button = null; // Try and get tile under it // TODO: Separate mode for decals. if (!uid.IsValid()) { var mapPos = _transform.ToMapCoordinates(coords); if (_mapMan.TryFindGridAt(mapPos, out var gridUid, out var grid) && _entityManager.System().TryGetTileRef(gridUid, grid, coords, out var tileRef) && _allPrototypesDict.TryGetValue(tileRef.GetContentTileDefinition(), out button)) { OnSelected(button); return true; } } if (button == null) { if (uid == EntityUid.Invalid || _entityManager.GetComponentOrNull(uid) is not { EntityPrototype: { } prototype } || !_allPrototypesDict.TryGetValue(prototype, out button)) { // we always block other input handlers if pick mode is enabled // this makes you not accidentally place something in space because you // miss-clicked while holding down the pick hotkey return true; } // Selected an entity OnSelected(button); // Match rotation _placement.Direction = _entityManager.GetComponent(uid).LocalRotation.GetDir(); } return true; } private bool HandleEditorCancelPlace(ICommonSession? session, EntityCoordinates coords, EntityUid uid) { if (!Screen.EraseDecalButton.Pressed) return false; _entityNetwork.SendSystemNetworkMessage(new RequestDecalRemovalEvent(_entityManager.GetNetCoordinates(coords))); return true; } private bool HandleCancelEraseDecal(in PointerInputCmdArgs args) { if (!Screen.EraseDecalButton.Pressed) return false; Screen.EraseDecalButton.Pressed = false; return true; } private async void SaveMap() { await _mapping.SaveMap(); } private void ToggleCollapse(MappingSpawnButton button) { if (button.CollapseButton.Pressed) { if (button.Prototype?.Children != null) { foreach (var child in button.Prototype.Children) { Screen.Prototypes.Insert(button.ChildrenPrototypes, child, true); } } button.CollapseButton.Label.Text = "▼"; } else { button.ChildrenPrototypes.DisposeAllChildren(); button.CollapseButton.Label.Text = "▶"; } } private void Collapse(MappingSpawnButton button) { if (!button.CollapseButton.Pressed) return; button.CollapseButton.Pressed = false; ToggleCollapse(button); } private void UnCollapse(MappingSpawnButton button) { if (button.CollapseButton.Pressed) return; button.CollapseButton.Pressed = true; ToggleCollapse(button); } public EntityUid? GetHoveredEntity() { if (UserInterfaceManager.CurrentlyHovered is not IViewportControl viewport || _input.MouseScreenPosition is not { IsValid: true } position) { return null; } var mapPos = viewport.PixelToMap(position.Position); return GetClickedEntity(mapPos); } public override void FrameUpdate(FrameEventArgs e) { if (_updatePlacement) { _updatePlacement = false; if (!_placement.IsActive && _decal.GetActiveDecal().Decal == null) Deselect(); Screen.EraseEntityButton.Pressed = _placement.Eraser; Screen.EraseDecalButton.Pressed = _updateEraseDecal; Screen.EntityPlacementMode.Disabled = _placement.Eraser; } if (_scrollTo is not { } scrollTo) return; // this is not ideal but we wait until the control's height is computed to use // its position to scroll to if (scrollTo.Height > 0 && Screen.Prototypes.PrototypeList.Visible) { var y = scrollTo.GlobalPosition.Y - Screen.Prototypes.ScrollContainer.Height / 2 + scrollTo.Height; var scroll = Screen.Prototypes.ScrollContainer; scroll.SetScrollValue(scroll.GetScrollValue() + new Vector2(0, y)); _scrollTo = null; } } // TODO this doesn't handle pressing down multiple state hotkeys at the moment public enum CursorState { None, Pick, Delete } }