diff --git a/Content.Client/Actions/ActionsSystem.cs b/Content.Client/Actions/ActionsSystem.cs index 7f261f5df2..30f657a2b5 100644 --- a/Content.Client/Actions/ActionsSystem.cs +++ b/Content.Client/Actions/ActionsSystem.cs @@ -293,7 +293,7 @@ namespace Content.Client.Actions continue; var action = _serialization.Read(actionNode, notNullableOverride: true); - var actionId = Spawn(null); + var actionId = Spawn(); AddComp(actionId, action); AddActionDirect(user, actionId); diff --git a/Content.Client/Commands/ActionsCommands.cs b/Content.Client/Commands/ActionsCommands.cs index 3d8e906e09..593b8e8256 100644 --- a/Content.Client/Commands/ActionsCommands.cs +++ b/Content.Client/Commands/ActionsCommands.cs @@ -1,3 +1,4 @@ +using Content.Client.Actions; using Content.Client.Actions; using Content.Client.Mapping; 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().LoadMappingActions(); - } - catch - { - shell.WriteError(LocalizationManager.GetString($"cmd-{Command}-error")); - } - } -} diff --git a/Content.Client/Commands/MappingClientSideSetupCommand.cs b/Content.Client/Commands/MappingClientSideSetupCommand.cs index 39268c6284..eb2d13c954 100644 --- a/Content.Client/Commands/MappingClientSideSetupCommand.cs +++ b/Content.Client/Commands/MappingClientSideSetupCommand.cs @@ -1,6 +1,8 @@ +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; @@ -10,6 +12,7 @@ internal sealed class MappingClientSideSetupCommand : LocalizedCommands { [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; [Dependency] private readonly ILightManager _lightManager = default!; + [Dependency] private readonly IStateManager _stateManager = default!; public override string Command => "mappingclientsidesetup"; @@ -21,8 +24,8 @@ internal sealed class MappingClientSideSetupCommand : LocalizedCommands { _entitySystemManager.GetEntitySystem().MarkersVisible = true; _lightManager.Enabled = false; - shell.ExecuteCommand(ShowSubFloorForever.CommandName); - shell.ExecuteCommand(LoadMappingActionsCommand.CommandName); + shell.ExecuteCommand("showsubfloorforever"); + _stateManager.RequestStateChange(); } } } diff --git a/Content.Client/ContextMenu/UI/ContextMenuUIController.cs b/Content.Client/ContextMenu/UI/ContextMenuUIController.cs index 5b156644a7..2d94034bb9 100644 --- a/Content.Client/ContextMenu/UI/ContextMenuUIController.cs +++ b/Content.Client/ContextMenu/UI/ContextMenuUIController.cs @@ -2,6 +2,7 @@ using System.Numerics; 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; @@ -16,7 +17,7 @@ namespace Content.Client.ContextMenu.UI /// /// This largely involves setting up timers to open and close sub-menus when hovering over other menu elements. /// - public sealed class ContextMenuUIController : UIController, IOnStateEntered, IOnStateExited, IOnSystemChanged + public sealed class ContextMenuUIController : UIController, IOnStateEntered, IOnStateExited, IOnSystemChanged, IOnStateEntered, IOnStateExited { public static readonly TimeSpan HoverDelay = TimeSpan.FromSeconds(0.2); @@ -42,18 +43,51 @@ namespace Content.Client.ContextMenu.UI public Action? OnSubMenuOpened; public Action? 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!; } /// diff --git a/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs b/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs index 845bd7c03d..07b6f57bdb 100644 --- a/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs +++ b/Content.Client/Decals/Overlays/DecalPlacementOverlay.cs @@ -4,6 +4,7 @@ using Robust.Client.Graphics; using Robust.Client.Input; using Robust.Shared.Enums; using Robust.Shared.Map; +using Robust.Shared.Prototypes; namespace Content.Client.Decals.Overlays; @@ -16,7 +17,7 @@ public sealed class DecalPlacementOverlay : Overlay 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) { @@ -24,6 +25,7 @@ public sealed class DecalPlacementOverlay : Overlay _placement = placement; _transform = transform; _sprite = sprite; + ZIndex = 1000; } protected override void Draw(in OverlayDrawArgs args) @@ -55,7 +57,7 @@ public sealed class DecalPlacementOverlay : Overlay if (snap) { - localPos = (Vector2) localPos.Floored() + grid.TileSizeHalfVector; + localPos = localPos.Floored() + grid.TileSizeHalfVector; } // Nothing uses snap cardinals so probably don't need preview? diff --git a/Content.Client/IoC/ClientContentIoC.cs b/Content.Client/IoC/ClientContentIoC.cs index 328cf41d0d..1fd237cf3e 100644 --- a/Content.Client/IoC/ClientContentIoC.cs +++ b/Content.Client/IoC/ClientContentIoC.cs @@ -4,23 +4,23 @@ using Content.Client.Chat.Managers; 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 @@ -49,6 +49,7 @@ namespace Content.Client.IoC collection.Register(); collection.Register(); collection.Register(); + collection.Register(); collection.Register(); } } diff --git a/Content.Client/Mapping/MappingActionsButton.xaml b/Content.Client/Mapping/MappingActionsButton.xaml new file mode 100644 index 0000000000..099719a70e --- /dev/null +++ b/Content.Client/Mapping/MappingActionsButton.xaml @@ -0,0 +1,8 @@ + + + diff --git a/Content.Client/Mapping/MappingActionsButton.xaml.cs b/Content.Client/Mapping/MappingActionsButton.xaml.cs new file mode 100644 index 0000000000..1a2f2c069f --- /dev/null +++ b/Content.Client/Mapping/MappingActionsButton.xaml.cs @@ -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); + } +} + diff --git a/Content.Client/Mapping/MappingDoNotMeasure.xaml b/Content.Client/Mapping/MappingDoNotMeasure.xaml new file mode 100644 index 0000000000..08909636ee --- /dev/null +++ b/Content.Client/Mapping/MappingDoNotMeasure.xaml @@ -0,0 +1,4 @@ + + diff --git a/Content.Client/Mapping/MappingDoNotMeasure.xaml.cs b/Content.Client/Mapping/MappingDoNotMeasure.xaml.cs new file mode 100644 index 0000000000..c4cb560234 --- /dev/null +++ b/Content.Client/Mapping/MappingDoNotMeasure.xaml.cs @@ -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; + } +} + diff --git a/Content.Client/Mapping/MappingManager.cs b/Content.Client/Mapping/MappingManager.cs new file mode 100644 index 0000000000..1aac02be71 --- /dev/null +++ b/Content.Client/Mapping/MappingManager.cs @@ -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(); + _net.RegisterNetMessage(OnSaveError); + _net.RegisterNetMessage(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; + } +} diff --git a/Content.Client/Mapping/MappingOverlay.cs b/Content.Client/Mapping/MappingOverlay.cs new file mode 100644 index 0000000000..ef9f3e795e --- /dev/null +++ b/Content.Client/Mapping/MappingOverlay.cs @@ -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 _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("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); + } +} diff --git a/Content.Client/Mapping/MappingPrototype.cs b/Content.Client/Mapping/MappingPrototype.cs new file mode 100644 index 0000000000..eff2dfab15 --- /dev/null +++ b/Content.Client/Mapping/MappingPrototype.cs @@ -0,0 +1,39 @@ +using Content.Shared.Decals; +using Content.Shared.Maps; +using Robust.Shared.Prototypes; + +namespace Content.Client.Mapping; + +/// +/// Used to represent a button's data in the mapping editor. +/// +public sealed class MappingPrototype +{ + /// + /// The prototype instance, if any. + /// Can be one of , or + /// If null, this is a top-level button (such as Entities, Tiles or Decals) + /// + public readonly IPrototype? Prototype; + + /// + /// The text to display on the UI for this button. + /// + public readonly string Name; + + /// + /// Which other prototypes (buttons) this one is nested inside of. + /// + public List? Parents; + + /// + /// Which other prototypes (buttons) are nested inside this one. + /// + public List? Children; + + public MappingPrototype(IPrototype? prototype, string name) + { + Prototype = prototype; + Name = name; + } +} diff --git a/Content.Client/Mapping/MappingPrototypeList.xaml b/Content.Client/Mapping/MappingPrototypeList.xaml new file mode 100644 index 0000000000..de311240df --- /dev/null +++ b/Content.Client/Mapping/MappingPrototypeList.xaml @@ -0,0 +1,21 @@ + + + +