Add mapping editor (#23427)

* Add mapping editor (#757)

* Remove mapping actions, never again

* Cleanup actions system

* Jarvis, remove all references to CM14

* Fix InventoryUIController crashing when an InventoryGui is not found

* Rename mapping1 to mapping

* Clean up context calls

* Add doc comments

* Add delegate for hiding decals in the mapping screen

* Jarvis mission failed

* a

* Add test

* Fix not flushing save stream in mapping manager

* change

* Fix verbs

* fixes

* localise

---------

Co-authored-by: DrSmugleaf <DrSmugleaf@users.noreply.github.com>
Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
Co-authored-by: Pieter-Jan Briers <pieterjan.briers+git@gmail.com>
This commit is contained in:
DrSmugleaf
2024-08-03 20:31:45 -07:00
committed by GitHub
parent 6e8f8d706a
commit 54d5bd266c
37 changed files with 2024 additions and 47 deletions

View File

@@ -293,7 +293,7 @@ namespace Content.Client.Actions
continue; continue;
var action = _serialization.Read<BaseActionComponent>(actionNode, notNullableOverride: true); var action = _serialization.Read<BaseActionComponent>(actionNode, notNullableOverride: true);
var actionId = Spawn(null); var actionId = Spawn();
AddComp(actionId, action); AddComp(actionId, action);
AddActionDirect(user, actionId); AddActionDirect(user, actionId);

View File

@@ -1,3 +1,4 @@
using Content.Client.Actions;
using Content.Client.Actions; using Content.Client.Actions;
using Content.Client.Mapping; using Content.Client.Mapping;
using Content.Shared.Administration; using Content.Shared.Administration;
@@ -61,27 +62,3 @@ public sealed class LoadActionsCommand : LocalizedCommands
} }
} }
} }
[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"));
}
}
}

View File

@@ -1,6 +1,8 @@
using Content.Client.Mapping;
using Content.Client.Markers; using Content.Client.Markers;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.State;
using Robust.Shared.Console; using Robust.Shared.Console;
namespace Content.Client.Commands; namespace Content.Client.Commands;
@@ -10,6 +12,7 @@ internal sealed class MappingClientSideSetupCommand : LocalizedCommands
{ {
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly ILightManager _lightManager = default!; [Dependency] private readonly ILightManager _lightManager = default!;
[Dependency] private readonly IStateManager _stateManager = default!;
public override string Command => "mappingclientsidesetup"; public override string Command => "mappingclientsidesetup";
@@ -21,8 +24,8 @@ internal sealed class MappingClientSideSetupCommand : LocalizedCommands
{ {
_entitySystemManager.GetEntitySystem<MarkerSystem>().MarkersVisible = true; _entitySystemManager.GetEntitySystem<MarkerSystem>().MarkersVisible = true;
_lightManager.Enabled = false; _lightManager.Enabled = false;
shell.ExecuteCommand(ShowSubFloorForever.CommandName); shell.ExecuteCommand("showsubfloorforever");
shell.ExecuteCommand(LoadMappingActionsCommand.CommandName); _stateManager.RequestStateChange<MappingState>();
} }
} }
} }

View File

@@ -2,6 +2,7 @@ using System.Numerics;
using System.Threading; using System.Threading;
using Content.Client.CombatMode; using Content.Client.CombatMode;
using Content.Client.Gameplay; using Content.Client.Gameplay;
using Content.Client.Mapping;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers; using Robust.Client.UserInterface.Controllers;
using Timer = Robust.Shared.Timing.Timer; using Timer = Robust.Shared.Timing.Timer;
@@ -16,7 +17,7 @@ namespace Content.Client.ContextMenu.UI
/// <remarks> /// <remarks>
/// This largely involves setting up timers to open and close sub-menus when hovering over other menu elements. /// This largely involves setting up timers to open and close sub-menus when hovering over other menu elements.
/// </remarks> /// </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 static readonly TimeSpan HoverDelay = TimeSpan.FromSeconds(0.2);
@@ -42,18 +43,51 @@ namespace Content.Client.ContextMenu.UI
public Action<ContextMenuElement>? OnSubMenuOpened; public Action<ContextMenuElement>? OnSubMenuOpened;
public Action<ContextMenuElement, GUIBoundKeyEventArgs>? OnContextKeyEvent; public Action<ContextMenuElement, GUIBoundKeyEventArgs>? OnContextKeyEvent;
private bool _setup;
public void OnStateEntered(GameplayState state) 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 = new(this, null);
RootMenu.OnPopupHide += Close; RootMenu.OnPopupHide += Close;
Menus.Push(RootMenu); Menus.Push(RootMenu);
} }
public void OnStateExited(GameplayState state) public void Shutdown()
{ {
if (!_setup)
return;
_setup = false;
Close(); Close();
RootMenu.OnPopupHide -= Close; RootMenu.OnPopupHide -= Close;
RootMenu.Dispose(); RootMenu.Dispose();
RootMenu = default!;
} }
/// <summary> /// <summary>

View File

@@ -4,6 +4,7 @@ using Robust.Client.Graphics;
using Robust.Client.Input; using Robust.Client.Input;
using Robust.Shared.Enums; using Robust.Shared.Enums;
using Robust.Shared.Map; using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Client.Decals.Overlays; namespace Content.Client.Decals.Overlays;
@@ -16,7 +17,7 @@ public sealed class DecalPlacementOverlay : Overlay
private readonly SharedTransformSystem _transform; private readonly SharedTransformSystem _transform;
private readonly SpriteSystem _sprite; private readonly SpriteSystem _sprite;
public override OverlaySpace Space => OverlaySpace.WorldSpace; public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities;
public DecalPlacementOverlay(DecalPlacementSystem placement, SharedTransformSystem transform, SpriteSystem sprite) public DecalPlacementOverlay(DecalPlacementSystem placement, SharedTransformSystem transform, SpriteSystem sprite)
{ {
@@ -24,6 +25,7 @@ public sealed class DecalPlacementOverlay : Overlay
_placement = placement; _placement = placement;
_transform = transform; _transform = transform;
_sprite = sprite; _sprite = sprite;
ZIndex = 1000;
} }
protected override void Draw(in OverlayDrawArgs args) protected override void Draw(in OverlayDrawArgs args)
@@ -55,7 +57,7 @@ public sealed class DecalPlacementOverlay : Overlay
if (snap) if (snap)
{ {
localPos = (Vector2) localPos.Floored() + grid.TileSizeHalfVector; localPos = localPos.Floored() + grid.TileSizeHalfVector;
} }
// Nothing uses snap cardinals so probably don't need preview? // Nothing uses snap cardinals so probably don't need preview?

View File

@@ -4,23 +4,23 @@ using Content.Client.Chat.Managers;
using Content.Client.Clickable; using Content.Client.Clickable;
using Content.Client.DebugMon; using Content.Client.DebugMon;
using Content.Client.Eui; using Content.Client.Eui;
using Content.Client.Fullscreen;
using Content.Client.GhostKick; using Content.Client.GhostKick;
using Content.Client.Guidebook;
using Content.Client.Launcher; using Content.Client.Launcher;
using Content.Client.Mapping;
using Content.Client.Parallax.Managers; using Content.Client.Parallax.Managers;
using Content.Client.Players.PlayTimeTracking; using Content.Client.Players.PlayTimeTracking;
using Content.Client.Replay;
using Content.Client.Screenshot; using Content.Client.Screenshot;
using Content.Client.Fullscreen;
using Content.Client.Stylesheets; using Content.Client.Stylesheets;
using Content.Client.Viewport; using Content.Client.Viewport;
using Content.Client.Voting; using Content.Client.Voting;
using Content.Shared.Administration.Logs; using Content.Shared.Administration.Logs;
using Content.Client.Guidebook;
using Content.Client.Lobby; using Content.Client.Lobby;
using Content.Client.Replay;
using Content.Shared.Administration.Managers; using Content.Shared.Administration.Managers;
using Content.Shared.Players.PlayTimeTracking; using Content.Shared.Players.PlayTimeTracking;
namespace Content.Client.IoC namespace Content.Client.IoC
{ {
internal static class ClientContentIoC internal static class ClientContentIoC
@@ -49,6 +49,7 @@ namespace Content.Client.IoC
collection.Register<DocumentParsingManager>(); collection.Register<DocumentParsingManager>();
collection.Register<ContentReplayPlaybackManager, ContentReplayPlaybackManager>(); collection.Register<ContentReplayPlaybackManager, ContentReplayPlaybackManager>();
collection.Register<ISharedPlaytimeManager, JobRequirementsManager>(); collection.Register<ISharedPlaytimeManager, JobRequirementsManager>();
collection.Register<MappingManager>();
collection.Register<DebugMonitorManager>(); collection.Register<DebugMonitorManager>();
} }
} }

View File

@@ -0,0 +1,8 @@
<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>

View File

@@ -0,0 +1,15 @@
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);
}
}

View File

@@ -0,0 +1,4 @@
<mapping:MappingDoNotMeasure
xmlns="https://spacestation14.io"
xmlns:mapping="clr-namespace:Content.Client.Mapping">
</mapping:MappingDoNotMeasure>

View File

@@ -0,0 +1,21 @@
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;
}
}

View File

@@ -0,0 +1,69 @@
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;
}
}

View File

@@ -0,0 +1,84 @@
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);
}
}

View File

@@ -0,0 +1,39 @@
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;
}
}

View File

@@ -0,0 +1,21 @@
<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>

View File

@@ -0,0 +1,170 @@
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);
}
}
}

View File

@@ -0,0 +1,85 @@
<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>

View File

@@ -0,0 +1,197 @@
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;
}
}

View File

@@ -0,0 +1,26 @@
<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>

View File

@@ -0,0 +1,16 @@
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);
}
}

View File

@@ -0,0 +1,936 @@
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
}
}

View File

@@ -13,7 +13,6 @@ public sealed partial class MappingSystem : EntitySystem
{ {
[Dependency] private readonly IPlacementManager _placementMan = default!; [Dependency] private readonly IPlacementManager _placementMan = default!;
[Dependency] private readonly ITileDefinitionManager _tileMan = default!; [Dependency] private readonly ITileDefinitionManager _tileMan = default!;
[Dependency] private readonly ActionsSystem _actionsSystem = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly MetaDataSystem _metaData = default!;
/// <summary> /// <summary>
@@ -26,8 +25,6 @@ public sealed partial class MappingSystem : EntitySystem
/// </summary> /// </summary>
private readonly SpriteSpecifier _deleteIcon = new Texture(new ("Interface/VerbIcons/delete.svg.192dpi.png")); private readonly SpriteSpecifier _deleteIcon = new Texture(new ("Interface/VerbIcons/delete.svg.192dpi.png"));
public string DefaultMappingActions = "/mapping_actions.yml";
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
@@ -36,11 +33,6 @@ public sealed partial class MappingSystem : EntitySystem
SubscribeLocalEvent<StartPlacementActionEvent>(OnStartPlacementAction); SubscribeLocalEvent<StartPlacementActionEvent>(OnStartPlacementAction);
} }
public void LoadMappingActions()
{
_actionsSystem.LoadActionAssignments(DefaultMappingActions, false);
}
/// <summary> /// <summary>
/// This checks if the placement manager is currently active, and attempts to copy the placement information for /// 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 /// some entity or tile into an action. This is somewhat janky, but it seem to work well enough. Though I'd

View File

@@ -736,7 +736,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
private void LoadGui() private void LoadGui()
{ {
DebugTools.Assert(_window == null); UnloadGui();
_window = UIManager.CreateWindow<ActionsWindow>(); _window = UIManager.CreateWindow<ActionsWindow>();
LayoutContainer.SetAnchorPreset(_window, LayoutContainer.LayoutPreset.CenterTop); LayoutContainer.SetAnchorPreset(_window, LayoutContainer.LayoutPreset.CenterTop);

View File

@@ -3,6 +3,7 @@ using System.Numerics;
using Content.Client.CombatMode; using Content.Client.CombatMode;
using Content.Client.ContextMenu.UI; using Content.Client.ContextMenu.UI;
using Content.Client.Gameplay; using Content.Client.Gameplay;
using Content.Client.Mapping;
using Content.Shared.Input; using Content.Shared.Input;
using Content.Shared.Verbs; using Content.Shared.Verbs;
using Robust.Client.Player; using Robust.Client.Player;
@@ -22,7 +23,9 @@ namespace Content.Client.Verbs.UI
/// open a verb menu for a given entity, add verbs to it, and add server-verbs when the server response is /// open a verb menu for a given entity, add verbs to it, and add server-verbs when the server response is
/// received. /// received.
/// </remarks> /// </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 IPlayerManager _playerManager = default!;
[Dependency] private readonly ContextMenuUIController _context = default!; [Dependency] private readonly ContextMenuUIController _context = default!;
@@ -44,7 +47,6 @@ namespace Content.Client.Verbs.UI
{ {
_context.OnContextKeyEvent += OnKeyBindDown; _context.OnContextKeyEvent += OnKeyBindDown;
_context.OnContextClosed += Close; _context.OnContextClosed += Close;
_verbSystem.OnVerbsResponse += HandleVerbsResponse;
} }
public void OnStateExited(GameplayState state) public void OnStateExited(GameplayState state)
@@ -56,6 +58,17 @@ namespace Content.Client.Verbs.UI
Close(); Close();
} }
public void OnStateEntered(MappingState state)
{
_verbSystem.OnVerbsResponse += HandleVerbsResponse;
}
public void OnStateExited(MappingState state)
{
if (_verbSystem != null)
_verbSystem.OnVerbsResponse -= HandleVerbsResponse;
}
/// <summary> /// <summary>
/// Open a verb menu and fill it with verbs applicable to the given target entity. /// Open a verb menu and fill it with verbs applicable to the given target entity.
/// </summary> /// </summary>

View File

@@ -0,0 +1,41 @@
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();
}
}

View File

@@ -10,6 +10,7 @@ using Content.Server.Discord;
using Content.Server.EUI; using Content.Server.EUI;
using Content.Server.GhostKick; using Content.Server.GhostKick;
using Content.Server.Info; using Content.Server.Info;
using Content.Server.Mapping;
using Content.Server.Maps; using Content.Server.Maps;
using Content.Server.MoMMI; using Content.Server.MoMMI;
using Content.Server.NodeContainer.NodeGroups; using Content.Server.NodeContainer.NodeGroups;
@@ -66,6 +67,7 @@ namespace Content.Server.IoC
IoCManager.Register<ServerApi>(); IoCManager.Register<ServerApi>();
IoCManager.Register<JobWhitelistManager>(); IoCManager.Register<JobWhitelistManager>();
IoCManager.Register<PlayerRateLimitManager>(); IoCManager.Register<PlayerRateLimitManager>();
IoCManager.Register<MappingManager>();
} }
} }
} }

View File

@@ -0,0 +1,76 @@
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
}
}

View File

@@ -1,10 +1,11 @@
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Array;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Shared.Decals namespace Content.Shared.Decals
{ {
[Prototype("decal")] [Prototype("decal")]
public sealed partial class DecalPrototype : IPrototype public sealed partial class DecalPrototype : IPrototype, IInheritingPrototype
{ {
[IdDataField] public string ID { get; } = null!; [IdDataField] public string ID { get; } = null!;
[DataField("sprite")] public SpriteSpecifier Sprite { get; private set; } = SpriteSpecifier.Invalid; [DataField("sprite")] public SpriteSpecifier Sprite { get; private set; } = SpriteSpecifier.Invalid;
@@ -33,5 +34,13 @@ namespace Content.Shared.Decals
/// </summary> /// </summary>
[DataField] [DataField]
public bool DefaultSnap = true; public bool DefaultSnap = true;
[ParentDataField(typeof(AbstractPrototypeIdArraySerializer<DecalPrototype>))]
public string[]? Parents { get; }
[NeverPushInheritance]
[AbstractDataField]
public bool Abstract { get; }
} }
} }

View File

@@ -104,5 +104,14 @@ namespace Content.Shared.Input
public static readonly BoundKeyFunction EditorCopyObject = "EditorCopyObject"; public static readonly BoundKeyFunction EditorCopyObject = "EditorCopyObject";
public static readonly BoundKeyFunction EditorFlipObject = "EditorFlipObject"; public static readonly BoundKeyFunction EditorFlipObject = "EditorFlipObject";
public static readonly BoundKeyFunction InspectEntity = "InspectEntity"; 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";
} }
} }

View File

@@ -0,0 +1,46 @@
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));
}
}

View File

@@ -0,0 +1,19 @@
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)
{
}
}

View File

@@ -0,0 +1,19 @@
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)
{
}
}

View File

@@ -0,0 +1,7 @@
mapping-entities = Entities
mapping-tiles = Tiles
mapping-decals = Decals
mapping-replace = Replace
mapping-erase-entity = Erase Entity
mapping-erase-decal = Erase Decal

View File

@@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 511 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 464 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -540,3 +540,33 @@ binds:
- function: Hotbar9 - function: Hotbar9
type: State type: State
key: Num9 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