continue;
var action = _serialization.Read<BaseActionComponent>(actionNode, notNullableOverride: true);
- var actionId = Spawn(null);
+ var actionId = Spawn();
AddComp(actionId, action);
AddActionDirect(user, actionId);
+using Content.Client.Actions;
using Content.Client.Actions;
using Content.Client.Mapping;
using Content.Shared.Administration;
}
}
}
-
-[AnyCommand]
-public sealed class LoadMappingActionsCommand : LocalizedCommands
-{
- [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
-
- public const string CommandName = "loadmapacts";
-
- public override string Command => CommandName;
-
- public override string Help => LocalizationManager.GetString($"cmd-{Command}-help", ("command", Command));
-
- public override void Execute(IConsoleShell shell, string argStr, string[] args)
- {
- try
- {
- _entitySystemManager.GetEntitySystem<MappingSystem>().LoadMappingActions();
- }
- catch
- {
- shell.WriteError(LocalizationManager.GetString($"cmd-{Command}-error"));
- }
- }
-}
+using Content.Client.Mapping;
using Content.Client.Markers;
using JetBrains.Annotations;
using Robust.Client.Graphics;
+using Robust.Client.State;
using Robust.Shared.Console;
namespace Content.Client.Commands;
{
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly ILightManager _lightManager = default!;
+ [Dependency] private readonly IStateManager _stateManager = default!;
public override string Command => "mappingclientsidesetup";
{
_entitySystemManager.GetEntitySystem<MarkerSystem>().MarkersVisible = true;
_lightManager.Enabled = false;
- shell.ExecuteCommand(ShowSubFloorForever.CommandName);
- shell.ExecuteCommand(LoadMappingActionsCommand.CommandName);
+ shell.ExecuteCommand("showsubfloorforever");
+ _stateManager.RequestStateChange<MappingState>();
}
}
}
using System.Threading;
using Content.Client.CombatMode;
using Content.Client.Gameplay;
+using Content.Client.Mapping;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Timer = Robust.Shared.Timing.Timer;
/// <remarks>
/// This largely involves setting up timers to open and close sub-menus when hovering over other menu elements.
/// </remarks>
- public sealed class ContextMenuUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>, IOnSystemChanged<CombatModeSystem>
+ public sealed class ContextMenuUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>, IOnSystemChanged<CombatModeSystem>, IOnStateEntered<MappingState>, IOnStateExited<MappingState>
{
public static readonly TimeSpan HoverDelay = TimeSpan.FromSeconds(0.2);
public Action<ContextMenuElement>? OnSubMenuOpened;
public Action<ContextMenuElement, GUIBoundKeyEventArgs>? OnContextKeyEvent;
+ private bool _setup;
+
public void OnStateEntered(GameplayState state)
{
+ Setup();
+ }
+
+ public void OnStateExited(GameplayState state)
+ {
+ Shutdown();
+ }
+
+ public void OnStateEntered(MappingState state)
+ {
+ Setup();
+ }
+
+ public void OnStateExited(MappingState state)
+ {
+ Shutdown();
+ }
+
+ public void Setup()
+ {
+ if (_setup)
+ return;
+
+ _setup = true;
+
RootMenu = new(this, null);
RootMenu.OnPopupHide += Close;
Menus.Push(RootMenu);
}
- public void OnStateExited(GameplayState state)
+ public void Shutdown()
{
+ if (!_setup)
+ return;
+
+ _setup = false;
+
Close();
RootMenu.OnPopupHide -= Close;
RootMenu.Dispose();
+ RootMenu = default!;
}
/// <summary>
using Robust.Client.Input;
using Robust.Shared.Enums;
using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
namespace Content.Client.Decals.Overlays;
private readonly SharedTransformSystem _transform;
private readonly SpriteSystem _sprite;
- public override OverlaySpace Space => OverlaySpace.WorldSpace;
+ public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities;
public DecalPlacementOverlay(DecalPlacementSystem placement, SharedTransformSystem transform, SpriteSystem sprite)
{
_placement = placement;
_transform = transform;
_sprite = sprite;
+ ZIndex = 1000;
}
protected override void Draw(in OverlayDrawArgs args)
if (snap)
{
- localPos = (Vector2) localPos.Floored() + grid.TileSizeHalfVector;
+ localPos = localPos.Floored() + grid.TileSizeHalfVector;
}
// Nothing uses snap cardinals so probably don't need preview?
using Content.Client.Clickable;
using Content.Client.DebugMon;
using Content.Client.Eui;
+using Content.Client.Fullscreen;
using Content.Client.GhostKick;
+using Content.Client.Guidebook;
using Content.Client.Launcher;
+using Content.Client.Mapping;
using Content.Client.Parallax.Managers;
using Content.Client.Players.PlayTimeTracking;
+using Content.Client.Replay;
using Content.Client.Screenshot;
-using Content.Client.Fullscreen;
using Content.Client.Stylesheets;
using Content.Client.Viewport;
using Content.Client.Voting;
using Content.Shared.Administration.Logs;
-using Content.Client.Guidebook;
using Content.Client.Lobby;
-using Content.Client.Replay;
using Content.Shared.Administration.Managers;
using Content.Shared.Players.PlayTimeTracking;
-
namespace Content.Client.IoC
{
internal static class ClientContentIoC
collection.Register<DocumentParsingManager>();
collection.Register<ContentReplayPlaybackManager, ContentReplayPlaybackManager>();
collection.Register<ISharedPlaytimeManager, JobRequirementsManager>();
+ collection.Register<MappingManager>();
collection.Register<DebugMonitorManager>();
}
}
--- /dev/null
+<mapping:MappingActionsButton
+ xmlns="https://spacestation14.io"
+ xmlns:mapping="clr-namespace:Content.Client.Mapping"
+ StyleClasses="ButtonSquare" ToggleMode="True" SetSize="32 32" Margin="0 0 5 0"
+ TooltipDelay="0">
+ <TextureRect Name="Texture" Access="Public" Stretch="Scale" SetSize="16 16"
+ HorizontalAlignment="Center" VerticalAlignment="Center" />
+</mapping:MappingActionsButton>
--- /dev/null
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Mapping;
+
+[GenerateTypedNameReferences]
+public sealed partial class MappingActionsButton : Button
+{
+ public MappingActionsButton()
+ {
+ RobustXamlLoader.Load(this);
+ }
+}
+
--- /dev/null
+<mapping:MappingDoNotMeasure
+ xmlns="https://spacestation14.io"
+ xmlns:mapping="clr-namespace:Content.Client.Mapping">
+</mapping:MappingDoNotMeasure>
--- /dev/null
+using System.Numerics;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Mapping;
+
+[GenerateTypedNameReferences]
+public sealed partial class MappingDoNotMeasure : Control
+{
+ public MappingDoNotMeasure()
+ {
+ RobustXamlLoader.Load(this);
+ }
+
+ protected override Vector2 MeasureOverride(Vector2 availableSize)
+ {
+ return Vector2.Zero;
+ }
+}
+
--- /dev/null
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Content.Shared.Mapping;
+using Robust.Client.UserInterface;
+using Robust.Shared.Network;
+
+namespace Content.Client.Mapping;
+
+public sealed class MappingManager : IPostInjectInit
+{
+ [Dependency] private readonly IFileDialogManager _file = default!;
+ [Dependency] private readonly IClientNetManager _net = default!;
+
+ private Stream? _saveStream;
+ private MappingMapDataMessage? _mapData;
+
+ public void PostInject()
+ {
+ _net.RegisterNetMessage<MappingSaveMapMessage>();
+ _net.RegisterNetMessage<MappingSaveMapErrorMessage>(OnSaveError);
+ _net.RegisterNetMessage<MappingMapDataMessage>(OnMapData);
+ }
+
+ private void OnSaveError(MappingSaveMapErrorMessage message)
+ {
+ _saveStream?.DisposeAsync();
+ _saveStream = null;
+ }
+
+ private async void OnMapData(MappingMapDataMessage message)
+ {
+ if (_saveStream == null)
+ {
+ _mapData = message;
+ return;
+ }
+
+ await _saveStream.WriteAsync(Encoding.ASCII.GetBytes(message.Yml));
+ await _saveStream.DisposeAsync();
+
+ _saveStream = null;
+ _mapData = null;
+ }
+
+ public async Task SaveMap()
+ {
+ if (_saveStream != null)
+ await _saveStream.DisposeAsync();
+
+ var request = new MappingSaveMapMessage();
+ _net.ClientSendMessage(request);
+
+ var path = await _file.SaveFile();
+ if (path is not { fileStream: var stream })
+ return;
+
+ if (_mapData != null)
+ {
+ await stream.WriteAsync(Encoding.ASCII.GetBytes(_mapData.Yml));
+ _mapData = null;
+ await stream.FlushAsync();
+ await stream.DisposeAsync();
+ return;
+ }
+
+ _saveStream = stream;
+ }
+}
--- /dev/null
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Client.Player;
+using Robust.Client.UserInterface;
+using Robust.Shared.Enums;
+using Robust.Shared.Prototypes;
+using static Content.Client.Mapping.MappingState;
+
+namespace Content.Client.Mapping;
+
+public sealed class MappingOverlay : Overlay
+{
+ [Dependency] private readonly IEntityManager _entities = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] private readonly IPrototypeManager _prototypes = default!;
+
+ // 1 off in case something else uses these colors since we use them to compare
+ private static readonly Color PickColor = new(1, 255, 0);
+ private static readonly Color DeleteColor = new(255, 1, 0);
+
+ private readonly Dictionary<EntityUid, Color> _oldColors = new();
+
+ private readonly MappingState _state;
+ private readonly ShaderInstance _shader;
+
+ public override OverlaySpace Space => OverlaySpace.WorldSpace;
+
+ public MappingOverlay(MappingState state)
+ {
+ IoCManager.InjectDependencies(this);
+
+ _state = state;
+ _shader = _prototypes.Index<ShaderPrototype>("unshaded").Instance();
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ foreach (var (id, color) in _oldColors)
+ {
+ if (!_entities.TryGetComponent(id, out SpriteComponent? sprite))
+ continue;
+
+ if (sprite.Color == DeleteColor || sprite.Color == PickColor)
+ sprite.Color = color;
+ }
+
+ _oldColors.Clear();
+
+ if (_player.LocalEntity == null)
+ return;
+
+ var handle = args.WorldHandle;
+ handle.UseShader(_shader);
+
+ switch (_state.State)
+ {
+ case CursorState.Pick:
+ {
+ if (_state.GetHoveredEntity() is { } entity &&
+ _entities.TryGetComponent(entity, out SpriteComponent? sprite))
+ {
+ _oldColors[entity] = sprite.Color;
+ sprite.Color = PickColor;
+ }
+
+ break;
+ }
+ case CursorState.Delete:
+ {
+ if (_state.GetHoveredEntity() is { } entity &&
+ _entities.TryGetComponent(entity, out SpriteComponent? sprite))
+ {
+ _oldColors[entity] = sprite.Color;
+ sprite.Color = DeleteColor;
+ }
+
+ break;
+ }
+ }
+
+ handle.UseShader(null);
+ }
+}
--- /dev/null
+using Content.Shared.Decals;
+using Content.Shared.Maps;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Mapping;
+
+/// <summary>
+/// Used to represent a button's data in the mapping editor.
+/// </summary>
+public sealed class MappingPrototype
+{
+ /// <summary>
+ /// The prototype instance, if any.
+ /// Can be one of <see cref="EntityPrototype"/>, <see cref="ContentTileDefinition"/> or <see cref="DecalPrototype"/>
+ /// If null, this is a top-level button (such as Entities, Tiles or Decals)
+ /// </summary>
+ public readonly IPrototype? Prototype;
+
+ /// <summary>
+ /// The text to display on the UI for this button.
+ /// </summary>
+ public readonly string Name;
+
+ /// <summary>
+ /// Which other prototypes (buttons) this one is nested inside of.
+ /// </summary>
+ public List<MappingPrototype>? Parents;
+
+ /// <summary>
+ /// Which other prototypes (buttons) are nested inside this one.
+ /// </summary>
+ public List<MappingPrototype>? Children;
+
+ public MappingPrototype(IPrototype? prototype, string name)
+ {
+ Prototype = prototype;
+ Name = name;
+ }
+}
--- /dev/null
+<mapping:MappingPrototypeList
+ xmlns="https://spacestation14.io"
+ xmlns:mapping="clr-namespace:Content.Client.Mapping">
+ <BoxContainer Orientation="Vertical">
+ <BoxContainer Orientation="Horizontal">
+ <Button Name="CollapseAllButton" Access="Public" Text="-" SetSize="48 48"
+ StyleClasses="ButtonSquare" ToolTip="Collapse All" TooltipDelay="0" />
+ <LineEdit Name="SearchBar" SetHeight="48" HorizontalExpand="True" Access="Public" />
+ <Button Name="ClearSearchButton" Access="Public" Text="X" SetSize="48 48"
+ StyleClasses="ButtonSquare" />
+ </BoxContainer>
+ <ScrollContainer Name="ScrollContainer" Access="Public" VerticalExpand="True"
+ ReserveScrollbarSpace="True">
+ <BoxContainer Name="PrototypeList" Access="Public" Orientation="Vertical" />
+ <PrototypeListContainer Name="SearchList" Access="Public" Visible="False" />
+ </ScrollContainer>
+ <mapping:MappingDoNotMeasure Visible="False">
+ <mapping:MappingSpawnButton Name="MeasureButton" Access="Public" />
+ </mapping:MappingDoNotMeasure>
+ </BoxContainer>
+</mapping:MappingPrototypeList>
--- /dev/null
+using System.Numerics;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using static Robust.Client.UserInterface.Controls.BaseButton;
+
+namespace Content.Client.Mapping;
+
+[GenerateTypedNameReferences]
+public sealed partial class MappingPrototypeList : Control
+{
+ private (int start, int end) _lastIndices;
+ private readonly List<MappingPrototype> _prototypes = new();
+ private readonly List<Texture> _insertTextures = new();
+ private readonly List<MappingPrototype> _search = new();
+
+ public MappingSpawnButton? Selected;
+ public Action<IPrototype, List<Texture>>? GetPrototypeData;
+ public event Action<MappingSpawnButton, IPrototype?>? SelectionChanged;
+ public event Action<MappingSpawnButton, ButtonToggledEventArgs>? CollapseToggled;
+
+ public MappingPrototypeList()
+ {
+ RobustXamlLoader.Load(this);
+
+ MeasureButton.Measure(Vector2Helpers.Infinity);
+
+ ScrollContainer.OnScrolled += UpdateSearch;
+ OnResized += UpdateSearch;
+ }
+
+ public void UpdateVisible(List<MappingPrototype> prototypes)
+ {
+ _prototypes.Clear();
+
+ PrototypeList.DisposeAllChildren();
+
+ _prototypes.AddRange(prototypes);
+
+ Selected = null;
+ ScrollContainer.SetScrollValue(new Vector2(0, 0));
+
+ foreach (var prototype in _prototypes)
+ {
+ Insert(PrototypeList, prototype, true);
+ }
+ }
+
+ public MappingSpawnButton Insert(Container list, MappingPrototype mapping, bool includeChildren)
+ {
+ var prototype = mapping.Prototype;
+
+ _insertTextures.Clear();
+
+ if (prototype != null)
+ GetPrototypeData?.Invoke(prototype, _insertTextures);
+
+ var button = new MappingSpawnButton { Prototype = mapping };
+ button.Label.Text = mapping.Name;
+
+ if (_insertTextures.Count > 0)
+ {
+ button.Texture.Textures.AddRange(_insertTextures);
+ button.Texture.InvalidateMeasure();
+ }
+ else
+ {
+ button.Texture.Visible = false;
+ }
+
+ if (prototype != null && button.Prototype == Selected?.Prototype)
+ {
+ Selected = button;
+ button.Button.Pressed = true;
+ }
+
+ list.AddChild(button);
+
+ button.Button.OnToggled += _ => SelectionChanged?.Invoke(button, prototype);
+
+ if (includeChildren && mapping.Children?.Count > 0)
+ {
+ button.CollapseButton.Visible = true;
+ button.CollapseButton.OnToggled += args => CollapseToggled?.Invoke(button, args);
+ }
+ else
+ {
+ button.CollapseButtonWrapper.Visible = false;
+ button.CollapseButton.Visible = false;
+ }
+
+ return button;
+ }
+
+ public void Search(List<MappingPrototype> prototypes)
+ {
+ _search.Clear();
+ SearchList.DisposeAllChildren();
+ _lastIndices = (0, -1);
+
+ _search.AddRange(prototypes);
+ SearchList.TotalItemCount = _search.Count;
+ ScrollContainer.SetScrollValue(new Vector2(0, 0));
+
+ UpdateSearch();
+ }
+
+ /// <summary>
+ /// Constructs a virtual list where not all buttons exist at one time, since there may be thousands of them.
+ /// </summary>
+ private void UpdateSearch()
+ {
+ if (!SearchList.Visible)
+ return;
+
+ var height = MeasureButton.DesiredSize.Y + PrototypeListContainer.Separation;
+ var offset = Math.Max(-SearchList.Position.Y, 0);
+ var startIndex = (int) Math.Floor(offset / height);
+ SearchList.ItemOffset = startIndex;
+
+ var (prevStart, prevEnd) = _lastIndices;
+ var endIndex = startIndex - 1;
+ var spaceUsed = -height;
+
+ // calculate how far down we are scrolled
+ while (spaceUsed < SearchList.Parent!.Height)
+ {
+ spaceUsed += height;
+ endIndex += 1;
+ }
+
+ endIndex = Math.Min(endIndex, _search.Count - 1);
+
+ // nothing changed in terms of which buttons are visible now and before
+ if (endIndex == prevEnd && startIndex == prevStart)
+ return;
+
+ _lastIndices = (startIndex, endIndex);
+
+ // remove previously seen but now unseen buttons from the top
+ for (var i = prevStart; i < startIndex && i <= prevEnd; i++)
+ {
+ var control = SearchList.GetChild(0);
+ SearchList.RemoveChild(control);
+ }
+
+ // remove previously seen but now unseen buttons from the bottom
+ for (var i = prevEnd; i > endIndex && i >= prevStart; i--)
+ {
+ var control = SearchList.GetChild(SearchList.ChildCount - 1);
+ SearchList.RemoveChild(control);
+ }
+
+ // insert buttons that can now be seen, from the start
+ for (var i = Math.Min(prevStart - 1, endIndex); i >= startIndex; i--)
+ {
+ Insert(SearchList, _search[i], false).SetPositionInParent(0);
+ }
+
+ // insert buttons that can now be seen, from the end
+ for (var i = Math.Max(prevEnd + 1, startIndex); i <= endIndex; i++)
+ {
+ Insert(SearchList, _search[i], false);
+ }
+ }
+}
--- /dev/null
+<mapping:MappingScreen
+ xmlns="https://spacestation14.io"
+ xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+ xmlns:widgets="clr-namespace:Content.Client.UserInterface.Systems.Chat.Widgets"
+ xmlns:hotbar="clr-namespace:Content.Client.UserInterface.Systems.Hotbar.Widgets"
+ xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+ xmlns:mapping="clr-namespace:Content.Client.Mapping"
+ VerticalExpand="False"
+ VerticalAlignment="Bottom"
+ HorizontalAlignment="Center">
+ <controls:RecordedSplitContainer Name="ScreenContainer" HorizontalExpand="True"
+ VerticalExpand="True" SplitWidth="0"
+ StretchDirection="TopLeft">
+ <BoxContainer Orientation="Vertical" VerticalExpand="True" Name="SpawnContainer" MinWidth="200" SetWidth="600">
+ <mapping:MappingPrototypeList Name="Prototypes" Access="Public" VerticalExpand="True" />
+ <BoxContainer Name="DecalContainer" Access="Public" Orientation="Horizontal"
+ Visible="False">
+ <BoxContainer Orientation="Vertical" HorizontalExpand="True">
+ <ColorSelectorSliders Name="DecalColorPicker" IsAlphaVisible="True" />
+ <Button Name="DecalPickerOpen" Text="{Loc decal-placer-window-palette}"
+ StyleClasses="ButtonSquare" />
+ </BoxContainer>
+ <BoxContainer Orientation="Vertical" HorizontalExpand="True">
+ <CheckBox Name="DecalEnableAuto" Margin="0 0 0 10"
+ Text="{Loc decal-placer-window-enable-auto}" />
+ <CheckBox Name="DecalEnableSnap"
+ Text="{Loc decal-placer-window-enable-snap}" />
+ <CheckBox Name="DecalEnableCleanable"
+ Text="{Loc decal-placer-window-enable-cleanable}" />
+ <BoxContainer Name="DecalSpinBoxContainer" Orientation="Horizontal">
+ <Label Text="{Loc decal-placer-window-rotation}" Margin="0 0 0 1" />
+ </BoxContainer>
+ <BoxContainer Orientation="Horizontal">
+ <Label Text="{Loc decal-placer-window-zindex}" Margin="0 0 0 1" />
+ <SpinBox Name="DecalZIndexSpinBox" HorizontalExpand="True" />
+ </BoxContainer>
+ </BoxContainer>
+ </BoxContainer>
+ <BoxContainer Name="EntityContainer" Access="Public" Orientation="Horizontal"
+ Visible="False">
+ <Button Name="EntityReplaceButton" Access="Public" ToggleMode="True"
+ SetHeight="48"
+ StyleClasses="ButtonSquare" Text="{Loc 'mapping-replace'}" HorizontalExpand="True" />
+ <OptionButton Name="EntityPlacementMode" Access="Public"
+ SetHeight="48"
+ StyleClasses="ButtonSquare" TooltipDelay="0"
+ ToolTip="{Loc entity-spawn-window-override-menu-tooltip}"
+ HorizontalExpand="True" />
+ </BoxContainer>
+ <BoxContainer Orientation="Horizontal">
+ <Button Name="EraseEntityButton" Access="Public" HorizontalExpand="True"
+ SetHeight="48"
+ ToggleMode="True" Text="{Loc 'mapping-erase-entity'}" StyleClasses="ButtonSquare" />
+ <Button Name="EraseDecalButton" Access="Public" HorizontalExpand="True"
+ SetHeight="48"
+ ToggleMode="True" Text="{Loc 'mapping-erase-decal'}" StyleClasses="ButtonSquare" />
+ </BoxContainer>
+ <widgets:ChatBox Visible="False" />
+ </BoxContainer>
+ <LayoutContainer Name="ViewportContainer" HorizontalExpand="True" VerticalExpand="True">
+ <controls:MainViewport Name="MainViewport"/>
+ <hotbar:HotbarGui Name="Hotbar" />
+ <PanelContainer Name="Actions" VerticalExpand="True" HorizontalExpand="True"
+ MaxHeight="48">
+ <PanelContainer.PanelOverride>
+ <graphics:StyleBoxFlat BackgroundColor="#222222AA" />
+ </PanelContainer.PanelOverride>
+ <BoxContainer Orientation="Horizontal" Margin="15 10">
+ <mapping:MappingActionsButton
+ Name="Add" Access="Public" Disabled="True" ToolTip="" Visible="False" />
+ <mapping:MappingActionsButton Name="Fill" Access="Public"
+ ToolTip="" Visible="False" />
+ <mapping:MappingActionsButton Name="Grab" Access="Public"
+ ToolTip="" Visible="False" />
+ <mapping:MappingActionsButton Name="Move" Access="Public"
+ ToolTip="" Visible="False" />
+ <mapping:MappingActionsButton Name="Pick" Access="Public"
+ ToolTip="Pick (Hold 5)" />
+ <mapping:MappingActionsButton Name="Delete" Access="Public"
+ ToolTip="Delete (Hold 6)" />
+ </BoxContainer>
+ </PanelContainer>
+ </LayoutContainer>
+ </controls:RecordedSplitContainer>
+</mapping:MappingScreen>
--- /dev/null
+using System.Linq;
+using System.Numerics;
+using Content.Client.Decals;
+using Content.Client.Decals.UI;
+using Content.Client.UserInterface.Screens;
+using Content.Client.UserInterface.Systems.Chat.Widgets;
+using Content.Shared.Decals;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using static Robust.Client.UserInterface.Controls.BaseButton;
+
+namespace Content.Client.Mapping;
+
+[GenerateTypedNameReferences]
+public sealed partial class MappingScreen : InGameScreen
+{
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+
+ public DecalPlacementSystem DecalSystem = default!;
+
+ private PaletteColorPicker? _picker;
+
+ private ProtoId<DecalPrototype>? _id;
+ private Color _decalColor = Color.White;
+ private float _decalRotation;
+ private bool _decalSnap;
+ private int _decalZIndex;
+ private bool _decalCleanable;
+
+ private bool _decalAuto;
+
+ public override ChatBox ChatBox => GetWidget<ChatBox>()!;
+
+ public event Func<MappingSpawnButton, bool>? IsDecalVisible;
+
+ public MappingScreen()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ AutoscaleMaxResolution = new Vector2i(1080, 770);
+
+ SetAnchorPreset(ScreenContainer, LayoutPreset.Wide);
+ SetAnchorPreset(ViewportContainer, LayoutPreset.Wide);
+ SetAnchorPreset(SpawnContainer, LayoutPreset.Wide);
+ SetAnchorPreset(MainViewport, LayoutPreset.Wide);
+ SetAnchorAndMarginPreset(Hotbar, LayoutPreset.BottomWide, margin: 5);
+ SetAnchorAndMarginPreset(Actions, LayoutPreset.TopWide, margin: 5);
+
+ ScreenContainer.OnSplitResizeFinished += () =>
+ OnChatResized?.Invoke(new Vector2(ScreenContainer.SplitFraction, 0));
+
+ var rotationSpinBox = new FloatSpinBox(90.0f, 0)
+ {
+ HorizontalExpand = true
+ };
+ DecalSpinBoxContainer.AddChild(rotationSpinBox);
+
+ DecalColorPicker.OnColorChanged += OnDecalColorPicked;
+ DecalPickerOpen.OnPressed += OnDecalPickerOpenPressed;
+ rotationSpinBox.OnValueChanged += args =>
+ {
+ _decalRotation = args.Value;
+ UpdateDecal();
+ };
+ DecalEnableAuto.OnToggled += args =>
+ {
+ _decalAuto = args.Pressed;
+ if (_id is { } id)
+ SelectDecal(id);
+ };
+ DecalEnableSnap.OnToggled += args =>
+ {
+ _decalSnap = args.Pressed;
+ UpdateDecal();
+ };
+ DecalEnableCleanable.OnToggled += args =>
+ {
+ _decalCleanable = args.Pressed;
+ UpdateDecal();
+ };
+ DecalZIndexSpinBox.ValueChanged += args =>
+ {
+ _decalZIndex = args.Value;
+ UpdateDecal();
+ };
+
+ for (var i = 0; i < EntitySpawnWindow.InitOpts.Length; i++)
+ {
+ EntityPlacementMode.AddItem(EntitySpawnWindow.InitOpts[i], i);
+ }
+
+ Pick.Texture.TexturePath = "/Textures/Interface/eyedropper.svg.png";
+ Delete.Texture.TexturePath = "/Textures/Interface/eraser.svg.png";
+ }
+
+ private void OnDecalColorPicked(Color color)
+ {
+ _decalColor = color;
+ DecalColorPicker.Color = color;
+ UpdateDecal();
+ }
+
+ private void OnDecalPickerOpenPressed(ButtonEventArgs obj)
+ {
+ if (_picker == null)
+ {
+ _picker = new PaletteColorPicker();
+ _picker.OpenToLeft();
+ _picker.PaletteList.OnItemSelected += args =>
+ {
+ var color = ((Color?) args.ItemList.GetSelected().First().Metadata)!.Value;
+ OnDecalColorPicked(color);
+ };
+
+ return;
+ }
+
+ if (_picker.IsOpen)
+ _picker.Close();
+ else
+ _picker.Open();
+ }
+
+ private void UpdateDecal()
+ {
+ if (_id is not { } id)
+ return;
+
+ DecalSystem.UpdateDecalInfo(id, _decalColor, _decalRotation, _decalSnap, _decalZIndex, _decalCleanable);
+ }
+
+ public void SelectDecal(string decalId)
+ {
+ if (!_prototype.TryIndex<DecalPrototype>(decalId, out var decal))
+ return;
+
+ _id = decalId;
+
+ if (_decalAuto)
+ {
+ _decalColor = Color.White;
+ _decalCleanable = decal.DefaultCleanable;
+ _decalSnap = decal.DefaultSnap;
+
+ DecalColorPicker.Color = _decalColor;
+ DecalEnableCleanable.Pressed = _decalCleanable;
+ DecalEnableSnap.Pressed = _decalSnap;
+ }
+
+ UpdateDecal();
+ RefreshList();
+ }
+
+ private void RefreshList()
+ {
+ foreach (var control in Prototypes.Children)
+ {
+ if (control is not MappingSpawnButton button ||
+ button.Prototype?.Prototype is not DecalPrototype)
+ {
+ continue;
+ }
+
+ foreach (var child in button.Children)
+ {
+ if (child is not MappingSpawnButton { Prototype.Prototype: DecalPrototype } childButton)
+ {
+ continue;
+ }
+
+ childButton.Texture.Modulate = _decalColor;
+ childButton.Visible = IsDecalVisible?.Invoke(childButton) ?? true;
+ }
+ }
+ }
+
+ public override void SetChatSize(Vector2 size)
+ {
+ ScreenContainer.DesiredSplitCenter = size.X;
+ ScreenContainer.ResizeMode = SplitContainer.SplitResizeMode.RespectChildrenMinSize;
+ }
+
+ public void UnPressActionsExcept(Control except)
+ {
+ Add.Pressed = Add == except;
+ Fill.Pressed = Fill == except;
+ Grab.Pressed = Grab == except;
+ Move.Pressed = Move == except;
+ Pick.Pressed = Pick == except;
+ Delete.Pressed = Delete == except;
+ }
+}
--- /dev/null
+<mapping:MappingSpawnButton
+ xmlns="https://spacestation14.io"
+ xmlns:mapping="clr-namespace:Content.Client.Mapping">
+ <BoxContainer Orientation="Vertical">
+ <Control>
+ <Button Name="Button" Access="Public" ToggleMode="True" StyleClasses="ButtonSquare" />
+ <BoxContainer Orientation="Horizontal">
+ <LayeredTextureRect Name="Texture" Access="Public" MinSize="48 48"
+ HorizontalAlignment="Center" VerticalAlignment="Center"
+ Stretch="KeepAspectCentered" CanShrink="True" />
+ <Control SetSize="48 48" Access="Public" Name="CollapseButtonWrapper">
+ <Button Name="CollapseButton" Access="Public" Text="▶"
+ ToggleMode="True" StyleClasses="ButtonSquare" SetSize="48 48" />
+ </Control>
+ <Label Name="Label" Access="Public"
+ VAlign="Center"
+ VerticalExpand="True"
+ MinHeight="48"
+ Margin="5 0"
+ HorizontalExpand="True" ClipText="True" />
+ </BoxContainer>
+ </Control>
+ <BoxContainer Name="ChildrenPrototypes" Access="Public" Orientation="Vertical"
+ Margin="24 0 0 0" />
+ </BoxContainer>
+</mapping:MappingSpawnButton>
--- /dev/null
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Mapping;
+
+[GenerateTypedNameReferences]
+public sealed partial class MappingSpawnButton : Control
+{
+ public MappingPrototype? Prototype;
+
+ public MappingSpawnButton()
+ {
+ RobustXamlLoader.Load(this);
+ }
+}
--- /dev/null
+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<MappingPrototype> _allPrototypes = new();
+ private readonly Dictionary<IPrototype, MappingPrototype> _allPrototypesDict = new();
+ private readonly Dictionary<Type, Dictionary<string, MappingPrototype>> _idDict = new();
+ private readonly List<MappingPrototype> _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<MainViewport>()!;
+
+ public CursorState State { get; set; }
+
+ public MappingState()
+ {
+ IoCManager.InjectDependencies(this);
+
+ _sawmill = _log.GetSawmill("mapping");
+ _loadController = UserInterfaceManager.GetUIController<GameplayStateLoadController>();
+ }
+
+ protected override void Startup()
+ {
+ EnsureSetup();
+ base.Startup();
+
+ UserInterfaceManager.LoadScreen<MappingScreen>();
+ _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<MappingState>();
+
+ _overlays.AddOverlay(new MappingOverlay(this));
+
+ _prototypeManager.PrototypesReloaded += OnPrototypesReloaded;
+
+ Screen.Prototypes.UpdateVisible(_prototypes);
+ }
+
+ private void OnPrototypesReloaded(PrototypesReloadedEventArgs obj)
+ {
+ if (!obj.WasModified<EntityPrototype>() &&
+ !obj.WasModified<ContentTileDefinition>() &&
+ !obj.WasModified<DecalPrototype>())
+ {
+ return;
+ }
+
+ ReloadPrototypes();
+ }
+
+ private bool HandleOpenContextMenu(in PointerInputCmdArgs args)
+ {
+ Deselect();
+
+ var coords = args.Coordinates.ToMap(_entityManager, _transform);
+ if (_verbs.TryGetEntityMenuEntities(coords, out var entities))
+ _entityMenuController.OpenRootMenu(entities);
+
+ return true;
+ }
+
+ protected override void Shutdown()
+ {
+ CommandBinds.Unregister<MappingState>();
+
+ 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<MappingOverlay>();
+
+ base.Shutdown();
+ }
+
+ private void EnsureSetup()
+ {
+ if (_setup)
+ return;
+
+ _setup = true;
+
+ _entityMenuController = UserInterfaceManager.GetUIController<EntityMenuUIController>();
+
+ _decal = _entityManager.System<DecalPlacementSystem>();
+ _sprite = _entityManager.System<SpriteSystem>();
+ _transform = _entityManager.System<TransformSystem>();
+ _verbs = _entityManager.System<VerbSystem>();
+ ReloadPrototypes();
+ }
+
+ private void ReloadPrototypes()
+ {
+ var entities = new MappingPrototype(null, Loc.GetString("mapping-entities")) { Children = new List<MappingPrototype>() };
+ _prototypes.Add(entities);
+
+ var mappings = new Dictionary<string, MappingPrototype>();
+ foreach (var entity in _prototypeManager.EnumeratePrototypes<EntityPrototype>())
+ {
+ Register(entity, entity.ID, entities);
+ }
+
+ Sort(mappings, entities);
+ mappings.Clear();
+
+ var tiles = new MappingPrototype(null, Loc.GetString("mapping-tiles")) { Children = new List<MappingPrototype>() };
+ _prototypes.Add(tiles);
+
+ foreach (var tile in _prototypeManager.EnumeratePrototypes<ContentTileDefinition>())
+ {
+ Register(tile, tile.ID, tiles);
+ }
+
+ Sort(mappings, tiles);
+ mappings.Clear();
+
+ var decals = new MappingPrototype(null, Loc.GetString("mapping-decals")) { Children = new List<MappingPrototype>() };
+ _prototypes.Add(decals);
+
+ foreach (var decal in _prototypeManager.EnumeratePrototypes<DecalPrototype>())
+ {
+ Register(decal, decal.ID, decals);
+ }
+
+ Sort(mappings, decals);
+ mappings.Clear();
+ }
+
+ private void Sort(Dictionary<string, MappingPrototype> prototypes, MappingPrototype topLevel)
+ {
+ static int Compare(MappingPrototype a, MappingPrototype b)
+ {
+ return string.Compare(a.Name, b.Name, OrdinalIgnoreCase);
+ }
+
+ topLevel.Children ??= new List<MappingPrototype>();
+
+ foreach (var prototype in prototypes.Values)
+ {
+ if (prototype.Parents == null && prototype != topLevel)
+ {
+ prototype.Parents = new List<MappingPrototype> { topLevel };
+ topLevel.Children.Add(prototype);
+ }
+
+ prototype.Parents?.Sort(Compare);
+ prototype.Children?.Sort(Compare);
+ }
+
+ topLevel.Children.Sort(Compare);
+ }
+
+ private MappingPrototype? Register<T>(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<T>(null, parentValue.Value, topLevel);
+
+ if (parent != null)
+ {
+ mapping.Parents ??= new List<MappingPrototype>();
+ mapping.Parents.Add(parent);
+ parent.Children ??= new List<MappingPrototype>();
+ parent.Children.Add(mapping);
+ }
+ }
+ else if (node.TryGet("parent", out SequenceDataNode? parentSequence))
+ {
+ foreach (var parentNode in parentSequence.Cast<ValueDataNode>())
+ {
+ var parent = Register<T>(null, parentNode.Value, topLevel);
+
+ if (parent != null)
+ {
+ mapping.Parents ??= new List<MappingPrototype>();
+ mapping.Parents.Add(parent);
+ parent.Children ??= new List<MappingPrototype>();
+ parent.Children.Add(mapping);
+ }
+ }
+ }
+ else
+ {
+ topLevel.Children ??= new List<MappingPrototype>();
+ topLevel.Children.Add(mapping);
+ mapping.Parents ??= new List<MappingPrototype>();
+ 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<MappingPrototype>();
+ topLevel.Children.Add(mapping);
+ mapping.Parents ??= new List<MappingPrototype>();
+ mapping.Parents.Add(topLevel);
+ return mapping;
+ }
+
+ foreach (var parentId in prototype.Parents)
+ {
+ var parent = Register<T>(null, parentId, topLevel);
+
+ if (parent != null)
+ {
+ mapping.Parents ??= new List<MappingPrototype>();
+ mapping.Parents.Add(parent);
+ parent.Children ??= new List<MappingPrototype>();
+ 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<MappingPrototype>();
+ 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<Texture> textures)
+ {
+ switch (prototype)
+ {
+ case EntityPrototype entity:
+ textures.AddRange(SpriteComponent.GetPrototypeTextures(entity, _resources).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<TextureResource>(sprite).Texture);
+ break;
+ }
+ }
+
+ private void OnSelected(MappingPrototype mapping)
+ {
+ if (mapping.Prototype == null)
+ return;
+
+ var chain = new Stack<MappingPrototype>();
+ 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<SharedMapSystem>().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<MetaDataComponent>(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<TransformComponent>(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
+ }
+}
{
[Dependency] private readonly IPlacementManager _placementMan = default!;
[Dependency] private readonly ITileDefinitionManager _tileMan = default!;
- [Dependency] private readonly ActionsSystem _actionsSystem = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
/// <summary>
/// </summary>
private readonly SpriteSpecifier _deleteIcon = new Texture(new ("Interface/VerbIcons/delete.svg.192dpi.png"));
- public string DefaultMappingActions = "/mapping_actions.yml";
-
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StartPlacementActionEvent>(OnStartPlacementAction);
}
- public void LoadMappingActions()
- {
- _actionsSystem.LoadActionAssignments(DefaultMappingActions, false);
- }
-
/// <summary>
/// This checks if the placement manager is currently active, and attempts to copy the placement information for
/// some entity or tile into an action. This is somewhat janky, but it seem to work well enough. Though I'd
private void LoadGui()
{
- DebugTools.Assert(_window == null);
+ UnloadGui();
_window = UIManager.CreateWindow<ActionsWindow>();
LayoutContainer.SetAnchorPreset(_window, LayoutContainer.LayoutPreset.CenterTop);
using Content.Client.CombatMode;
using Content.Client.ContextMenu.UI;
using Content.Client.Gameplay;
+using Content.Client.Mapping;
using Content.Shared.Input;
using Content.Shared.Verbs;
using Robust.Client.Player;
/// open a verb menu for a given entity, add verbs to it, and add server-verbs when the server response is
/// received.
/// </remarks>
- public sealed class VerbMenuUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>
+ public sealed class VerbMenuUIController : UIController,
+ IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>,
+ IOnStateEntered<MappingState>, IOnStateExited<MappingState>
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly ContextMenuUIController _context = default!;
{
_context.OnContextKeyEvent += OnKeyBindDown;
_context.OnContextClosed += Close;
- _verbSystem.OnVerbsResponse += HandleVerbsResponse;
}
public void OnStateExited(GameplayState state)
Close();
}
+ public void OnStateEntered(MappingState state)
+ {
+ _verbSystem.OnVerbsResponse += HandleVerbsResponse;
+ }
+
+ public void OnStateExited(MappingState state)
+ {
+ if (_verbSystem != null)
+ _verbSystem.OnVerbsResponse -= HandleVerbsResponse;
+ }
+
/// <summary>
/// Open a verb menu and fill it with verbs applicable to the given target entity.
/// </summary>
--- /dev/null
+using Content.Client.Gameplay;
+using Content.Client.Mapping;
+using Robust.Client.State;
+
+namespace Content.IntegrationTests.Tests;
+
+[TestFixture]
+public sealed class MappingEditorTest
+{
+ [Test]
+ public async Task StopHardCodingWidgetsJesusChristTest()
+ {
+ await using var pair = await PoolManager.GetServerClient(new PoolSettings
+ {
+ Connected = true
+ });
+ var client = pair.Client;
+ var state = client.ResolveDependency<IStateManager>();
+
+ await client.WaitPost(() =>
+ {
+ Assert.DoesNotThrow(() =>
+ {
+ state.RequestStateChange<MappingState>();
+ });
+ });
+
+ // arbitrary short time
+ await client.WaitRunTicks(30);
+
+ await client.WaitPost(() =>
+ {
+ Assert.DoesNotThrow(() =>
+ {
+ state.RequestStateChange<GameplayState>();
+ });
+ });
+
+ await pair.CleanReturnAsync();
+ }
+}
using Content.Server.EUI;
using Content.Server.GhostKick;
using Content.Server.Info;
+using Content.Server.Mapping;
using Content.Server.Maps;
using Content.Server.MoMMI;
using Content.Server.NodeContainer.NodeGroups;
IoCManager.Register<ServerApi>();
IoCManager.Register<JobWhitelistManager>();
IoCManager.Register<PlayerRateLimitManager>();
+ IoCManager.Register<MappingManager>();
}
}
}
--- /dev/null
+using System.IO;
+using Content.Server.Administration.Managers;
+using Content.Shared.Administration;
+using Content.Shared.Mapping;
+using Robust.Server.GameObjects;
+using Robust.Server.Player;
+using Robust.Shared.Map;
+using Robust.Shared.Network;
+using Robust.Shared.Serialization;
+using Robust.Shared.Utility;
+using YamlDotNet.Core;
+using YamlDotNet.RepresentationModel;
+
+namespace Content.Server.Mapping;
+
+public sealed class MappingManager : IPostInjectInit
+{
+ [Dependency] private readonly IAdminManager _admin = default!;
+ [Dependency] private readonly ILogManager _log = default!;
+ [Dependency] private readonly IMapManager _map = default!;
+ [Dependency] private readonly IServerNetManager _net = default!;
+ [Dependency] private readonly IPlayerManager _players = default!;
+ [Dependency] private readonly IEntitySystemManager _systems = default!;
+
+ private ISawmill _sawmill = default!;
+ private ZStdCompressionContext _zstd = default!;
+
+ public void PostInject()
+ {
+#if !FULL_RELEASE
+ _net.RegisterNetMessage<MappingSaveMapMessage>(OnMappingSaveMap);
+ _net.RegisterNetMessage<MappingSaveMapErrorMessage>();
+ _net.RegisterNetMessage<MappingMapDataMessage>();
+
+ _sawmill = _log.GetSawmill("mapping");
+ _zstd = new ZStdCompressionContext();
+#endif
+ }
+
+ private void OnMappingSaveMap(MappingSaveMapMessage message)
+ {
+#if !FULL_RELEASE
+ try
+ {
+ if (!_players.TryGetSessionByChannel(message.MsgChannel, out var session) ||
+ !_admin.IsAdmin(session, true) ||
+ !_admin.HasAdminFlag(session, AdminFlags.Host) ||
+ session.AttachedEntity is not { } player)
+ {
+ return;
+ }
+
+ var mapId = _systems.GetEntitySystem<TransformSystem>().GetMapCoordinates(player).MapId;
+ var mapEntity = _map.GetMapEntityIdOrThrow(mapId);
+ var data = _systems.GetEntitySystem<MapLoaderSystem>().GetSaveData(mapEntity);
+ var document = new YamlDocument(data.ToYaml());
+ var stream = new YamlStream { document };
+ var writer = new StringWriter();
+ stream.Save(new YamlMappingFix(new Emitter(writer)), false);
+
+ var msg = new MappingMapDataMessage()
+ {
+ Context = _zstd,
+ Yml = writer.ToString()
+ };
+ _net.ServerSendMessage(msg, message.MsgChannel);
+ }
+ catch (Exception e)
+ {
+ _sawmill.Error($"Error saving map in mapping mode:\n{e}");
+ var msg = new MappingSaveMapErrorMessage();
+ _net.ServerSendMessage(msg, message.MsgChannel);
+ }
+#endif
+ }
+}
using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
using Robust.Shared.Utility;
namespace Content.Shared.Decals
{
[Prototype("decal")]
- public sealed partial class DecalPrototype : IPrototype
+ public sealed partial class DecalPrototype : IPrototype, IInheritingPrototype
{
[IdDataField] public string ID { get; } = null!;
[DataField("sprite")] public SpriteSpecifier Sprite { get; private set; } = SpriteSpecifier.Invalid;
/// </summary>
[DataField]
public bool DefaultSnap = true;
+
+ [ParentDataField(typeof(AbstractPrototypeIdArraySerializer<DecalPrototype>))]
+ public string[]? Parents { get; }
+
+ [NeverPushInheritance]
+ [AbstractDataField]
+ public bool Abstract { get; }
+
}
}
public static readonly BoundKeyFunction EditorCopyObject = "EditorCopyObject";
public static readonly BoundKeyFunction EditorFlipObject = "EditorFlipObject";
public static readonly BoundKeyFunction InspectEntity = "InspectEntity";
+
+ public static readonly BoundKeyFunction MappingUnselect = "MappingUnselect";
+ public static readonly BoundKeyFunction SaveMap = "SaveMap";
+ public static readonly BoundKeyFunction MappingEnablePick = "MappingEnablePick";
+ public static readonly BoundKeyFunction MappingEnableDelete = "MappingEnableDelete";
+ public static readonly BoundKeyFunction MappingPick = "MappingPick";
+ public static readonly BoundKeyFunction MappingRemoveDecal = "MappingRemoveDecal";
+ public static readonly BoundKeyFunction MappingCancelEraseDecal = "MappingCancelEraseDecal";
+ public static readonly BoundKeyFunction MappingOpenContextMenu = "MappingOpenContextMenu";
}
}
--- /dev/null
+using System.IO;
+using Lidgren.Network;
+using Robust.Shared.Network;
+using Robust.Shared.Serialization;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Mapping;
+
+public sealed class MappingMapDataMessage : NetMessage
+{
+ public override MsgGroups MsgGroup => MsgGroups.Command;
+ public override NetDeliveryMethod DeliveryMethod => NetDeliveryMethod.ReliableUnordered;
+
+ public ZStdCompressionContext Context = default!;
+ public string Yml = default!;
+
+ public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
+ {
+ MsgSize = buffer.LengthBytes;
+
+ var uncompressedLength = buffer.ReadVariableInt32();
+ var compressedLength = buffer.ReadVariableInt32();
+ var stream = new MemoryStream(compressedLength);
+ buffer.ReadAlignedMemory(stream, compressedLength);
+ using var decompress = new ZStdDecompressStream(stream);
+ using var decompressed = new MemoryStream(uncompressedLength);
+
+ decompress.CopyTo(decompressed, uncompressedLength);
+ decompressed.Position = 0;
+ serializer.DeserializeDirect(decompressed, out Yml);
+ }
+
+ public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
+ {
+ var stream = new MemoryStream();
+ serializer.SerializeDirect(stream, Yml);
+ buffer.WriteVariableInt32((int) stream.Length);
+
+ stream.Position = 0;
+ var buf = new byte[ZStd.CompressBound((int) stream.Length)];
+ var length = Context.Compress2(buf, stream.AsSpan());
+
+ buffer.WriteVariableInt32(length);
+ buffer.Write(buf.AsSpan(0, length));
+ }
+}
--- /dev/null
+using Lidgren.Network;
+using Robust.Shared.Network;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Mapping;
+
+public sealed class MappingSaveMapErrorMessage : NetMessage
+{
+ public override MsgGroups MsgGroup => MsgGroups.Command;
+ public override NetDeliveryMethod DeliveryMethod => NetDeliveryMethod.ReliableUnordered;
+
+ public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
+ {
+ }
+
+ public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
+ {
+ }
+}
--- /dev/null
+using Lidgren.Network;
+using Robust.Shared.Network;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Mapping;
+
+public sealed class MappingSaveMapMessage : NetMessage
+{
+ public override MsgGroups MsgGroup => MsgGroups.Command;
+ public override NetDeliveryMethod DeliveryMethod => NetDeliveryMethod.ReliableUnordered;
+
+ public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
+ {
+ }
+
+ public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
+ {
+ }
+}
--- /dev/null
+mapping-entities = Entities
+mapping-tiles = Tiles
+mapping-decals = Decals
+
+mapping-replace = Replace
+mapping-erase-entity = Erase Entity
+mapping-erase-decal = Erase Decal
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eraser" viewBox="0 0 16 16">
+ <path d="M8.086 2.207a2 2 0 0 1 2.828 0l3.879 3.879a2 2 0 0 1 0 2.828l-5.5 5.5A2 2 0 0 1 7.879 15H5.12a2 2 0 0 1-1.414-.586l-2.5-2.5a2 2 0 0 1 0-2.828l6.879-6.879zm2.121.707a1 1 0 0 0-1.414 0L4.16 7.547l5.293 5.293 4.633-4.633a1 1 0 0 0 0-1.414l-3.879-3.879zM8.746 13.547 3.453 8.254 1.914 9.793a1 1 0 0 0 0 1.414l2.5 2.5a1 1 0 0 0 .707.293H7.88a1 1 0 0 0 .707-.293l.16-.16z"/>
+</svg>
\ No newline at end of file
--- /dev/null
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-eyedropper" viewBox="0 0 16 16">
+ <path d="M13.354.646a1.207 1.207 0 0 0-1.708 0L8.5 3.793l-.646-.647a.5.5 0 1 0-.708.708L8.293 5l-7.147 7.146A.5.5 0 0 0 1 12.5v1.793l-.854.853a.5.5 0 1 0 .708.707L1.707 15H3.5a.5.5 0 0 0 .354-.146L11 7.707l1.146 1.147a.5.5 0 0 0 .708-.708l-.647-.646 3.147-3.146a1.207 1.207 0 0 0 0-1.708l-2-2zM2 12.707l7-7L10.293 7l-7 7H2z"/>
+</svg>
\ No newline at end of file
- function: Hotbar9
type: State
key: Num9
+- function: MappingUnselect
+ type: State
+ key: MouseRight
+ canFocus: true
+- function: SaveMap
+ type: State
+ key: S
+ mod1: Control
+- function: MappingEnablePick
+ type: State
+ key: Num5
+- function: MappingEnableDelete
+ type: State
+ key: Num6
+- function: MappingPick
+ type: State
+ key: MouseLeft
+ canFocus: true
+- function: MappingRemoveDecal
+ type: State
+ key: MouseLeft
+ canFocus: true
+- function: MappingCancelEraseDecal
+ type: State
+ key: MouseRight
+ canFocus: true
+- function: MappingOpenContextMenu
+ type: State
+ key: MouseRight
+ canFocus: true