Merge remote-tracking branch 'upstream/master' into ed-05-08-2024-upstream

# Conflicts:
#	.github/PULL_REQUEST_TEMPLATE.md
#	Resources/Prototypes/Accents/word_replacements.yml
This commit is contained in:
Ed
2024-08-05 15:28:42 +03:00
883 changed files with 29778 additions and 25586 deletions

View File

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

View File

@@ -44,7 +44,7 @@ namespace Content.Client.Administration
}
// if not on the same map, continue
if (_entityManager.GetComponent<TransformComponent>(entity.Value).MapID != _eyeManager.CurrentMap)
if (_entityManager.GetComponent<TransformComponent>(entity.Value).MapID != args.MapId)
{
continue;
}

View File

@@ -88,26 +88,51 @@ namespace Content.Client.Administration.UI.Bwoink
var ach = AHelpHelper.EnsurePanel(a.SessionId);
var bch = AHelpHelper.EnsurePanel(b.SessionId);
// First, sort by unread. Any chat with unread messages appears first. We just sort based on unread
// status, not number of unread messages, so that more recent unread messages take priority.
// Pinned players first
if (a.IsPinned != b.IsPinned)
return a.IsPinned ? -1 : 1;
// First, sort by unread. Any chat with unread messages appears first.
var aUnread = ach.Unread > 0;
var bUnread = bch.Unread > 0;
if (aUnread != bUnread)
return aUnread ? -1 : 1;
// Sort by recent messages during the current round.
var aRecent = a.ActiveThisRound && ach.LastMessage != DateTime.MinValue;
var bRecent = b.ActiveThisRound && bch.LastMessage != DateTime.MinValue;
if (aRecent != bRecent)
return aRecent ? -1 : 1;
// Next, sort by connection status. Any disconnected players are grouped towards the end.
if (a.Connected != b.Connected)
return a.Connected ? -1 : 1;
// Next, group by whether or not the players have participated in this round.
// The ahelp window shows all players that have connected since server restart, this groups them all towards the bottom.
if (a.ActiveThisRound != b.ActiveThisRound)
return a.ActiveThisRound ? -1 : 1;
// Sort connected players by New Player status, then by Antag status
if (a.Connected && b.Connected)
{
var aNewPlayer = a.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold));
var bNewPlayer = b.OverallPlaytime <= TimeSpan.FromMinutes(_cfg.GetCVar(CCVars.NewPlayerThreshold));
if (aNewPlayer != bNewPlayer)
return aNewPlayer ? -1 : 1;
if (a.Antag != b.Antag)
return a.Antag ? -1 : 1;
}
// Sort disconnected players by participation in the round
if (!a.Connected && !b.Connected)
{
if (a.ActiveThisRound != b.ActiveThisRound)
return a.ActiveThisRound ? -1 : 1;
}
// Finally, sort by the most recent message.
return bch.LastMessage.CompareTo(ach.LastMessage);
};
Bans.OnPressed += _ =>
{
if (_currentPlayer is not null)
@@ -253,7 +278,20 @@ namespace Content.Client.Administration.UI.Bwoink
public void PopulateList()
{
// Maintain existing pin statuses
var pinnedPlayers = ChannelSelector.PlayerInfo.Where(p => p.IsPinned).ToDictionary(p => p.SessionId);
ChannelSelector.PopulateList();
// Restore pin statuses
foreach (var player in ChannelSelector.PlayerInfo)
{
if (pinnedPlayers.TryGetValue(player.SessionId, out var pinnedPlayer))
{
player.IsPinned = pinnedPlayer.IsPinned;
}
}
UpdateButtons();
}
}

View File

@@ -30,7 +30,11 @@ namespace Content.Client.Administration.UI.Bwoink
}
};
OnOpen += () => Bwoink.PopulateList();
OnOpen += () =>
{
Bwoink.ChannelSelector.StopFiltering();
Bwoink.PopulateList();
};
}
}
}

View File

@@ -4,154 +4,166 @@ using Content.Client.UserInterface.Controls;
using Content.Client.Verbs.UI;
using Content.Shared.Administration;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Input;
using Robust.Shared.Utility;
namespace Content.Client.Administration.UI.CustomControls
namespace Content.Client.Administration.UI.CustomControls;
[GenerateTypedNameReferences]
public sealed partial class PlayerListControl : BoxContainer
{
[GenerateTypedNameReferences]
public sealed partial class PlayerListControl : BoxContainer
private readonly AdminSystem _adminSystem;
private readonly IEntityManager _entManager;
private readonly IUserInterfaceManager _uiManager;
private PlayerInfo? _selectedPlayer;
private List<PlayerInfo> _playerList = new();
private List<PlayerInfo> _sortedPlayerList = new();
public Comparison<PlayerInfo>? Comparison;
public Func<PlayerInfo, string, string>? OverrideText;
public PlayerListControl()
{
private readonly AdminSystem _adminSystem;
private List<PlayerInfo> _playerList = new();
private readonly List<PlayerInfo> _sortedPlayerList = new();
public event Action<PlayerInfo?>? OnSelectionChanged;
public IReadOnlyList<PlayerInfo> PlayerInfo => _playerList;
public Func<PlayerInfo, string, string>? OverrideText;
public Comparison<PlayerInfo>? Comparison;
private IEntityManager _entManager;
private IUserInterfaceManager _uiManager;
private PlayerInfo? _selectedPlayer;
public PlayerListControl()
{
_entManager = IoCManager.Resolve<IEntityManager>();
_uiManager = IoCManager.Resolve<IUserInterfaceManager>();
_adminSystem = _entManager.System<AdminSystem>();
RobustXamlLoader.Load(this);
// Fill the Option data
PlayerListContainer.ItemPressed += PlayerListItemPressed;
PlayerListContainer.ItemKeyBindDown += PlayerListItemKeyBindDown;
PlayerListContainer.GenerateItem += GenerateButton;
PlayerListContainer.NoItemSelected += PlayerListNoItemSelected;
PopulateList(_adminSystem.PlayerList);
FilterLineEdit.OnTextChanged += _ => FilterList();
_adminSystem.PlayerListChanged += PopulateList;
BackgroundPanel.PanelOverride = new StyleBoxFlat {BackgroundColor = new Color(32, 32, 40)};
}
private void PlayerListNoItemSelected()
{
_selectedPlayer = null;
OnSelectionChanged?.Invoke(null);
}
private void PlayerListItemPressed(BaseButton.ButtonEventArgs? args, ListData? data)
{
if (args == null || data is not PlayerListData {Info: var selectedPlayer})
return;
if (selectedPlayer == _selectedPlayer)
return;
if (args.Event.Function != EngineKeyFunctions.UIClick)
return;
OnSelectionChanged?.Invoke(selectedPlayer);
_selectedPlayer = selectedPlayer;
// update label text. Only required if there is some override (e.g. unread bwoink count).
if (OverrideText != null && args.Button.Children.FirstOrDefault()?.Children?.FirstOrDefault() is Label label)
label.Text = GetText(selectedPlayer);
}
private void PlayerListItemKeyBindDown(GUIBoundKeyEventArgs? args, ListData? data)
{
if (args == null || data is not PlayerListData { Info: var selectedPlayer })
return;
if (args.Function != EngineKeyFunctions.UIRightClick || selectedPlayer.NetEntity == null)
return;
_uiManager.GetUIController<VerbMenuUIController>().OpenVerbMenu(selectedPlayer.NetEntity.Value, true);
args.Handle();
}
public void StopFiltering()
{
FilterLineEdit.Text = string.Empty;
}
private void FilterList()
{
_sortedPlayerList.Clear();
foreach (var info in _playerList)
{
var displayName = $"{info.CharacterName} ({info.Username})";
if (info.IdentityName != info.CharacterName)
displayName += $" [{info.IdentityName}]";
if (!string.IsNullOrEmpty(FilterLineEdit.Text)
&& !displayName.ToLowerInvariant().Contains(FilterLineEdit.Text.Trim().ToLowerInvariant()))
continue;
_sortedPlayerList.Add(info);
}
if (Comparison != null)
_sortedPlayerList.Sort((a, b) => Comparison(a, b));
PlayerListContainer.PopulateList(_sortedPlayerList.Select(info => new PlayerListData(info)).ToList());
if (_selectedPlayer != null)
PlayerListContainer.Select(new PlayerListData(_selectedPlayer));
}
public void PopulateList(IReadOnlyList<PlayerInfo>? players = null)
{
players ??= _adminSystem.PlayerList;
_playerList = players.ToList();
if (_selectedPlayer != null && !_playerList.Contains(_selectedPlayer))
_selectedPlayer = null;
FilterList();
}
private string GetText(PlayerInfo info)
{
var text = $"{info.CharacterName} ({info.Username})";
if (OverrideText != null)
text = OverrideText.Invoke(info, text);
return text;
}
private void GenerateButton(ListData data, ListContainerButton button)
{
if (data is not PlayerListData { Info: var info })
return;
button.AddChild(new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
Children =
{
new Label
{
ClipText = true,
Text = GetText(info)
}
}
});
button.AddStyleClass(ListContainer.StyleClassListContainerButton);
}
_entManager = IoCManager.Resolve<IEntityManager>();
_uiManager = IoCManager.Resolve<IUserInterfaceManager>();
_adminSystem = _entManager.System<AdminSystem>();
RobustXamlLoader.Load(this);
// Fill the Option data
PlayerListContainer.ItemPressed += PlayerListItemPressed;
PlayerListContainer.ItemKeyBindDown += PlayerListItemKeyBindDown;
PlayerListContainer.GenerateItem += GenerateButton;
PlayerListContainer.NoItemSelected += PlayerListNoItemSelected;
PopulateList(_adminSystem.PlayerList);
FilterLineEdit.OnTextChanged += _ => FilterList();
_adminSystem.PlayerListChanged += PopulateList;
BackgroundPanel.PanelOverride = new StyleBoxFlat { BackgroundColor = new Color(32, 32, 40) };
}
public record PlayerListData(PlayerInfo Info) : ListData;
public IReadOnlyList<PlayerInfo> PlayerInfo => _playerList;
public event Action<PlayerInfo?>? OnSelectionChanged;
private void PlayerListNoItemSelected()
{
_selectedPlayer = null;
OnSelectionChanged?.Invoke(null);
}
private void PlayerListItemPressed(BaseButton.ButtonEventArgs? args, ListData? data)
{
if (args == null || data is not PlayerListData { Info: var selectedPlayer })
return;
if (selectedPlayer == _selectedPlayer)
return;
if (args.Event.Function != EngineKeyFunctions.UIClick)
return;
OnSelectionChanged?.Invoke(selectedPlayer);
_selectedPlayer = selectedPlayer;
// update label text. Only required if there is some override (e.g. unread bwoink count).
if (OverrideText != null && args.Button.Children.FirstOrDefault()?.Children?.FirstOrDefault() is Label label)
label.Text = GetText(selectedPlayer);
}
private void PlayerListItemKeyBindDown(GUIBoundKeyEventArgs? args, ListData? data)
{
if (args == null || data is not PlayerListData { Info: var selectedPlayer })
return;
if (args.Function != EngineKeyFunctions.UIRightClick || selectedPlayer.NetEntity == null)
return;
_uiManager.GetUIController<VerbMenuUIController>().OpenVerbMenu(selectedPlayer.NetEntity.Value, true);
args.Handle();
}
public void StopFiltering()
{
FilterLineEdit.Text = string.Empty;
}
private void FilterList()
{
_sortedPlayerList.Clear();
foreach (var info in _playerList)
{
var displayName = $"{info.CharacterName} ({info.Username})";
if (info.IdentityName != info.CharacterName)
displayName += $" [{info.IdentityName}]";
if (!string.IsNullOrEmpty(FilterLineEdit.Text)
&& !displayName.ToLowerInvariant().Contains(FilterLineEdit.Text.Trim().ToLowerInvariant()))
continue;
_sortedPlayerList.Add(info);
}
if (Comparison != null)
_sortedPlayerList.Sort((a, b) => Comparison(a, b));
PlayerListContainer.PopulateList(_sortedPlayerList.Select(info => new PlayerListData(info)).ToList());
if (_selectedPlayer != null)
PlayerListContainer.Select(new PlayerListData(_selectedPlayer));
}
public void PopulateList(IReadOnlyList<PlayerInfo>? players = null)
{
// Maintain existing pin statuses
var pinnedPlayers = _playerList.Where(p => p.IsPinned).ToDictionary(p => p.SessionId);
players ??= _adminSystem.PlayerList;
_playerList = players.ToList();
// Restore pin statuses
foreach (var player in _playerList)
{
if (pinnedPlayers.TryGetValue(player.SessionId, out var pinnedPlayer))
{
player.IsPinned = pinnedPlayer.IsPinned;
}
}
if (_selectedPlayer != null && !_playerList.Contains(_selectedPlayer))
_selectedPlayer = null;
FilterList();
}
private string GetText(PlayerInfo info)
{
var text = $"{info.CharacterName} ({info.Username})";
if (OverrideText != null)
text = OverrideText.Invoke(info, text);
return text;
}
private void GenerateButton(ListData data, ListContainerButton button)
{
if (data is not PlayerListData { Info: var info })
return;
var entry = new PlayerListEntry();
entry.Setup(info, OverrideText);
entry.OnPinStatusChanged += _ =>
{
FilterList();
};
button.AddChild(entry);
button.AddStyleClass(ListContainer.StyleClassListContainerButton);
}
}
public record PlayerListData(PlayerInfo Info) : ListData;

View File

@@ -0,0 +1,6 @@
<BoxContainer xmlns="https://spacestation14.io"
Orientation="Horizontal" HorizontalExpand="true">
<Label Name="PlayerEntryLabel" Text="" ClipText="True" HorizontalExpand="True" />
<TextureButton Name="PlayerEntryPinButton"
HorizontalAlignment="Right" />
</BoxContainer>

View File

@@ -0,0 +1,58 @@
using Content.Client.Stylesheets;
using Content.Shared.Administration;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Utility;
namespace Content.Client.Administration.UI.CustomControls;
[GenerateTypedNameReferences]
public sealed partial class PlayerListEntry : BoxContainer
{
public PlayerListEntry()
{
RobustXamlLoader.Load(this);
}
public event Action<PlayerInfo>? OnPinStatusChanged;
public void Setup(PlayerInfo info, Func<PlayerInfo, string, string>? overrideText)
{
Update(info, overrideText);
PlayerEntryPinButton.OnPressed += HandlePinButtonPressed(info);
}
private Action<BaseButton.ButtonEventArgs> HandlePinButtonPressed(PlayerInfo info)
{
return args =>
{
info.IsPinned = !info.IsPinned;
UpdatePinButtonTexture(info.IsPinned);
OnPinStatusChanged?.Invoke(info);
};
}
private void Update(PlayerInfo info, Func<PlayerInfo, string, string>? overrideText)
{
PlayerEntryLabel.Text = overrideText?.Invoke(info, $"{info.CharacterName} ({info.Username})") ??
$"{info.CharacterName} ({info.Username})";
UpdatePinButtonTexture(info.IsPinned);
}
private void UpdatePinButtonTexture(bool isPinned)
{
if (isPinned)
{
PlayerEntryPinButton?.RemoveStyleClass(StyleNano.StyleClassPinButtonUnpinned);
PlayerEntryPinButton?.AddStyleClass(StyleNano.StyleClassPinButtonPinned);
}
else
{
PlayerEntryPinButton?.RemoveStyleClass(StyleNano.StyleClassPinButtonPinned);
PlayerEntryPinButton?.AddStyleClass(StyleNano.StyleClassPinButtonUnpinned);
}
}
}

View File

@@ -1,4 +1,5 @@
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.FixedPoint;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
@@ -9,11 +10,15 @@ namespace Content.Client.Chemistry.UI
[UsedImplicitly]
public sealed class TransferAmountBoundUserInterface : BoundUserInterface
{
private IEntityManager _entManager;
private EntityUid _owner;
[ViewVariables]
private TransferAmountWindow? _window;
public TransferAmountBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
_owner = owner;
_entManager = IoCManager.Resolve<IEntityManager>();
}
protected override void Open()
@@ -21,6 +26,9 @@ namespace Content.Client.Chemistry.UI
base.Open();
_window = this.CreateWindow<TransferAmountWindow>();
if (_entManager.TryGetComponent<SolutionTransferComponent>(_owner, out var comp))
_window.SetBounds(comp.MinimumTransferAmount.Int(), comp.MaximumTransferAmount.Int());
_window.ApplyButton.OnPressed += _ =>
{
if (int.TryParse(_window.AmountLineEdit.Text, out var i))

View File

@@ -6,6 +6,10 @@
<BoxContainer Orientation="Horizontal">
<LineEdit Name="AmountLineEdit" Access="Public" HorizontalExpand="True" PlaceHolder="{Loc 'ui-transfer-amount-line-edit-placeholder'}"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Name="MinimumAmount" Access="Public" HorizontalExpand="True" />
<Label Name="MaximumAmount" Access="Public" />
</BoxContainer>
<Button Name="ApplyButton" Access="Public" Text="{Loc 'ui-transfer-amount-apply'}"/>
</BoxContainer>
</DefaultWindow>

View File

@@ -8,9 +8,29 @@ namespace Content.Client.Chemistry.UI
[GenerateTypedNameReferences]
public sealed partial class TransferAmountWindow : DefaultWindow
{
private int _max = Int32.MaxValue;
private int _min = 1;
public TransferAmountWindow()
{
RobustXamlLoader.Load(this);
AmountLineEdit.OnTextChanged += OnValueChanged;
}
public void SetBounds(int min, int max)
{
_min = min;
_max = max;
MinimumAmount.Text = Loc.GetString("comp-solution-transfer-set-amount-min", ("amount", _min));
MaximumAmount.Text = Loc.GetString("comp-solution-transfer-set-amount-max", ("amount", _max));
}
private void OnValueChanged(LineEdit.LineEditEventArgs args)
{
if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount > _max || amount < _min)
ApplyButton.Disabled = true;
else
ApplyButton.Disabled = false;
}
}
}

View File

@@ -2,6 +2,8 @@ using Content.Client.Items.Systems;
using Content.Shared.Chemistry;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Clothing;
using Content.Shared.Clothing.Components;
using Content.Shared.Hands;
using Content.Shared.Item;
using Content.Shared.Rounding;
@@ -20,6 +22,7 @@ public sealed class SolutionContainerVisualsSystem : VisualizerSystem<SolutionCo
base.Initialize();
SubscribeLocalEvent<SolutionContainerVisualsComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<SolutionContainerVisualsComponent, GetInhandVisualsEvent>(OnGetHeldVisuals);
SubscribeLocalEvent<SolutionContainerVisualsComponent, GetEquipmentVisualsEvent>(OnGetClothingVisuals);
}
private void OnMapInit(EntityUid uid, SolutionContainerVisualsComponent component, MapInitEvent args)
@@ -174,4 +177,41 @@ public sealed class SolutionContainerVisualsSystem : VisualizerSystem<SolutionCo
args.Layers.Add((key, layer));
}
}
private void OnGetClothingVisuals(Entity<SolutionContainerVisualsComponent> ent, ref GetEquipmentVisualsEvent args)
{
if (ent.Comp.EquippedFillBaseName == null)
return;
if (!TryComp<AppearanceComponent>(ent, out var appearance))
return;
if (!TryComp<ClothingComponent>(ent, out var clothing))
return;
if (!AppearanceSystem.TryGetData<float>(ent, SolutionContainerVisuals.FillFraction, out var fraction, appearance))
return;
var closestFillSprite = ContentHelpers.RoundToLevels(fraction, 1, ent.Comp.EquippedMaxFillLevels + 1);
if (closestFillSprite > 0)
{
var layer = new PrototypeLayerData();
var equippedPrefix = clothing.EquippedPrefix == null ? $"equipped-{args.Slot}" : $" {clothing.EquippedPrefix}-equipped-{args.Slot}";
var key = equippedPrefix + ent.Comp.EquippedFillBaseName + closestFillSprite;
// Make sure the sprite state is valid so we don't show a big red error message
// This saves us from having to make fill level sprites for every possible slot the item could be in (including pockets).
if (!TryComp<SpriteComponent>(ent, out var sprite) || sprite.BaseRSI == null || !sprite.BaseRSI.TryGetState(key, out _))
return;
layer.State = key;
if (ent.Comp.ChangeColor && AppearanceSystem.TryGetData<Color>(ent, SolutionContainerVisuals.Color, out var color, appearance))
layer.Color = color;
args.Layers.Add((key, layer));
}
}
}

View File

@@ -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<MappingSystem>().LoadMappingActions();
}
catch
{
shell.WriteError(LocalizationManager.GetString($"cmd-{Command}-error"));
}
}
}

View File

@@ -30,7 +30,7 @@ public sealed class HideMechanismsCommand : LocalizedCommands
sprite.ContainerOccluded = false;
var tempParent = uid;
while (containerSys.TryGetContainingContainer(tempParent, out var container))
while (containerSys.TryGetContainingContainer((tempParent, null, null), out var container))
{
if (!container.ShowContents)
{

View File

@@ -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<MarkerSystem>().MarkersVisible = true;
_lightManager.Enabled = false;
shell.ExecuteCommand(ShowSubFloorForever.CommandName);
shell.ExecuteCommand(LoadMappingActionsCommand.CommandName);
shell.ExecuteCommand("showsubfloorforever");
_stateManager.RequestStateChange<MappingState>();
}
}
}

View File

@@ -130,7 +130,7 @@ namespace Content.Client.Communications.UI
EmergencyShuttleButton.Text = Loc.GetString("comms-console-menu-recall-shuttle");
var infoText = Loc.GetString($"comms-console-menu-time-remaining",
("time", diff.TotalSeconds.ToString(CultureInfo.CurrentCulture)));
("time", diff.ToString(@"hh\:mm\:ss", CultureInfo.CurrentCulture)));
CountdownLabel.SetMessage(infoText);
}
}

View File

@@ -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
/// <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);
@@ -42,18 +43,51 @@ namespace Content.Client.ContextMenu.UI
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>

View File

@@ -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?

View File

@@ -0,0 +1,80 @@
using Content.Shared.Drowsiness;
using Content.Shared.StatusEffect;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.Drowsiness;
public sealed class DrowsinessOverlay : Overlay
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public override bool RequestScreenTexture => true;
private readonly ShaderInstance _drowsinessShader;
public float CurrentPower = 0.0f;
private const float PowerDivisor = 250.0f;
private const float Intensity = 0.2f; // for adjusting the visual scale
private float _visualScale = 0; // between 0 and 1
public DrowsinessOverlay()
{
IoCManager.InjectDependencies(this);
_drowsinessShader = _prototypeManager.Index<ShaderPrototype>("Drowsiness").InstanceUnique();
}
protected override void FrameUpdate(FrameEventArgs args)
{
var playerEntity = _playerManager.LocalEntity;
if (playerEntity == null)
return;
if (!_entityManager.HasComponent<DrowsinessComponent>(playerEntity)
|| !_entityManager.TryGetComponent<StatusEffectsComponent>(playerEntity, out var status))
return;
var statusSys = _sysMan.GetEntitySystem<StatusEffectsSystem>();
if (!statusSys.TryGetTime(playerEntity.Value, SharedDrowsinessSystem.DrowsinessKey, out var time, status))
return;
var curTime = _timing.CurTime;
var timeLeft = (float)(time.Value.Item2 - curTime).TotalSeconds;
CurrentPower += 8f * (0.5f * timeLeft - CurrentPower) * args.DeltaSeconds / (timeLeft + 1);
}
protected override bool BeforeDraw(in OverlayDrawArgs args)
{
if (!_entityManager.TryGetComponent(_playerManager.LocalEntity, out EyeComponent? eyeComp))
return false;
if (args.Viewport.Eye != eyeComp.Eye)
return false;
_visualScale = Math.Clamp(CurrentPower / PowerDivisor, 0.0f, 1.0f);
return _visualScale > 0;
}
protected override void Draw(in OverlayDrawArgs args)
{
if (ScreenTexture == null)
return;
var handle = args.WorldHandle;
_drowsinessShader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
_drowsinessShader.SetParameter("Strength", _visualScale * Intensity);
handle.UseShader(_drowsinessShader);
handle.DrawRect(args.WorldBounds, Color.White);
handle.UseShader(null);
}
}

View File

@@ -0,0 +1,53 @@
using Content.Shared.Drowsiness;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Shared.Player;
namespace Content.Client.Drowsiness;
public sealed class DrowsinessSystem : SharedDrowsinessSystem
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IOverlayManager _overlayMan = default!;
private DrowsinessOverlay _overlay = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DrowsinessComponent, ComponentInit>(OnDrowsinessInit);
SubscribeLocalEvent<DrowsinessComponent, ComponentShutdown>(OnDrowsinessShutdown);
SubscribeLocalEvent<DrowsinessComponent, LocalPlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<DrowsinessComponent, LocalPlayerDetachedEvent>(OnPlayerDetached);
_overlay = new();
}
private void OnPlayerAttached(EntityUid uid, DrowsinessComponent component, LocalPlayerAttachedEvent args)
{
_overlayMan.AddOverlay(_overlay);
}
private void OnPlayerDetached(EntityUid uid, DrowsinessComponent component, LocalPlayerDetachedEvent args)
{
_overlay.CurrentPower = 0;
_overlayMan.RemoveOverlay(_overlay);
}
private void OnDrowsinessInit(EntityUid uid, DrowsinessComponent component, ComponentInit args)
{
if (_player.LocalEntity == uid)
_overlayMan.AddOverlay(_overlay);
}
private void OnDrowsinessShutdown(EntityUid uid, DrowsinessComponent component, ComponentShutdown args)
{
if (_player.LocalEntity == uid)
{
_overlay.CurrentPower = 0;
_overlayMan.RemoveOverlay(_overlay);
}
}
}

View File

@@ -16,15 +16,17 @@ public sealed class ExplosionOverlay : Overlay
[Dependency] private readonly IRobustRandom _robustRandom = default!;
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
private SharedAppearanceSystem _appearance;
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
private ShaderInstance _shader;
public ExplosionOverlay()
public ExplosionOverlay(SharedAppearanceSystem appearanceSystem)
{
IoCManager.InjectDependencies(this);
_shader = _proto.Index<ShaderPrototype>("unshaded").Instance();
_appearance = appearanceSystem;
}
protected override void Draw(in OverlayDrawArgs args)
@@ -33,15 +35,14 @@ public sealed class ExplosionOverlay : Overlay
drawHandle.UseShader(_shader);
var xforms = _entMan.GetEntityQuery<TransformComponent>();
var query = _entMan
.EntityQuery<ExplosionVisualsComponent, ExplosionVisualsTexturesComponent, AppearanceComponent>(true);
var query = _entMan.EntityQueryEnumerator<ExplosionVisualsComponent, ExplosionVisualsTexturesComponent>();
foreach (var (visuals, textures, appearance) in query)
while (query.MoveNext(out var uid, out var visuals, out var textures))
{
if (visuals.Epicenter.MapId != args.MapId)
continue;
if (!appearance.TryGetData(ExplosionAppearanceData.Progress, out int index))
if (!_appearance.TryGetData(uid, ExplosionAppearanceData.Progress, out int index))
continue;
index = Math.Min(index, visuals.Intensity.Count - 1);

View File

@@ -20,6 +20,7 @@ public sealed class ExplosionOverlaySystem : EntitySystem
[Dependency] private readonly IOverlayManager _overlayMan = default!;
[Dependency] private readonly SharedPointLightSystem _lights = default!;
[Dependency] private readonly IMapManager _mapMan = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
public override void Initialize()
{
@@ -28,7 +29,7 @@ public sealed class ExplosionOverlaySystem : EntitySystem
SubscribeLocalEvent<ExplosionVisualsComponent, ComponentInit>(OnExplosionInit);
SubscribeLocalEvent<ExplosionVisualsComponent, ComponentRemove>(OnCompRemove);
SubscribeLocalEvent<ExplosionVisualsComponent, ComponentHandleState>(OnExplosionHandleState);
_overlayMan.AddOverlay(new ExplosionOverlay());
_overlayMan.AddOverlay(new ExplosionOverlay(_appearance));
}
private void OnExplosionHandleState(EntityUid uid, ExplosionVisualsComponent component, ref ComponentHandleState args)

View File

@@ -0,0 +1,8 @@
using Content.Shared.Explosion.EntitySystems;
namespace Content.Client.Explosion.EntitySystems;
public sealed class ExplosionSystem : SharedExplosionSystem
{
}

View File

@@ -22,7 +22,7 @@ public sealed class GhostRoleRadioBoundUserInterface : BoundUserInterface
_ghostRoleRadioMenu.SendGhostRoleRadioMessageAction += SendGhostRoleRadioMessage;
}
public void SendGhostRoleRadioMessage(ProtoId<GhostRolePrototype> protoId)
private void SendGhostRoleRadioMessage(ProtoId<GhostRolePrototype> protoId)
{
SendMessage(new GhostRoleRadioMessage(protoId));
}

View File

@@ -1,7 +1,6 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Ghost.Roles;
using Content.Shared.Ghost.Roles.Components;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
@@ -33,7 +32,7 @@ public sealed partial class GhostRoleRadioMenu : RadialMenu
private void RefreshUI()
{
// The main control that will contain all of the clickable options
// The main control that will contain all the clickable options
var main = FindControl<RadialContainer>("Main");
// The purpose of this radial UI is for ghost role radios that allow you to select
@@ -70,7 +69,7 @@ public sealed partial class GhostRoleRadioMenu : RadialMenu
if (_prototypeManager.TryIndex(ghostRoleProto.IconPrototype, out var iconProto))
entProtoView.SetPrototype(iconProto);
else
entProtoView.SetPrototype(comp.Prototype);
entProtoView.SetPrototype(ghostRoleProto.EntityPrototype);
button.AddChild(entProtoView);
main.AddChild(button);

View File

@@ -206,7 +206,7 @@ namespace Content.Client.Instruments.UI
var container = _entManager.System<SharedContainerSystem>();
// If we're a handheld instrument, we might be in a container. Get it just in case.
container.TryGetContainingContainer(Entity, out var conMan);
container.TryGetContainingContainer((Entity, null, null), out var conMan);
// If the instrument is handheld and we're not holding it, we return.
if (instrument.Handheld && (conMan == null || conMan.Owner != localEntity))

View File

@@ -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<DocumentParsingManager>();
collection.Register<ContentReplayPlaybackManager, ContentReplayPlaybackManager>();
collection.Register<ISharedPlaytimeManager, JobRequirementsManager>();
collection.Register<MappingManager>();
collection.Register<DebugMonitorManager>();
}
}

View File

@@ -43,7 +43,7 @@ public sealed class ItemSystem : SharedItemSystem
public override void VisualsChanged(EntityUid uid)
{
// if the item is in a container, it might be equipped to hands or inventory slots --> update visuals.
if (Container.TryGetContainingContainer(uid, out var container))
if (Container.TryGetContainingContainer((uid, null, null), out var container))
RaiseLocalEvent(container.Owner, new VisualsChangedEvent(GetNetEntity(uid), container.ID));
}

View File

@@ -2,9 +2,11 @@ using System.Linq;
using System.Numerics;
using Content.Client.CrewManifest;
using Content.Client.GameTicking.Managers;
using Content.Client.Lobby;
using Content.Client.UserInterface.Controls;
using Content.Client.Players.PlayTimeTracking;
using Content.Shared.CCVar;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Content.Shared.StatusIcon;
using Robust.Client.Console;
@@ -26,6 +28,7 @@ namespace Content.Client.LateJoin
[Dependency] private readonly IConfigurationManager _configManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystem = default!;
[Dependency] private readonly JobRequirementsManager _jobRequirements = default!;
[Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
public event Action<(NetEntity, string)> SelectedId;
@@ -254,7 +257,7 @@ namespace Content.Client.LateJoin
jobButton.OnPressed += _ => SelectedId.Invoke((id, jobButton.JobId));
if (!_jobRequirements.IsAllowed(prototype, out var reason))
if (!_jobRequirements.IsAllowed(prototype, (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter, out var reason))
{
jobButton.Disabled = true;

View File

@@ -22,21 +22,29 @@ public sealed class LatheSystem : SharedLatheSystem
if (args.Sprite == null)
return;
// Lathe specific stuff
if (_appearance.TryGetData<bool>(uid, LatheVisuals.IsRunning, out var isRunning, args.Component))
{
if (args.Sprite.LayerMapTryGet(LatheVisualLayers.IsRunning, out var runningLayer) &&
component.RunningState != null &&
component.IdleState != null)
{
var state = isRunning ? component.RunningState : component.IdleState;
args.Sprite.LayerSetState(runningLayer, state);
}
}
if (_appearance.TryGetData<bool>(uid, PowerDeviceVisuals.Powered, out var powered, args.Component) &&
args.Sprite.LayerMapTryGet(PowerDeviceVisualLayers.Powered, out var powerLayer))
{
args.Sprite.LayerSetVisible(powerLayer, powered);
}
// Lathe specific stuff
if (_appearance.TryGetData<bool>(uid, LatheVisuals.IsRunning, out var isRunning, args.Component) &&
args.Sprite.LayerMapTryGet(LatheVisualLayers.IsRunning, out var runningLayer) &&
component.RunningState != null &&
component.IdleState != null)
{
var state = isRunning ? component.RunningState : component.IdleState;
args.Sprite.LayerSetAnimationTime(runningLayer, 0f);
args.Sprite.LayerSetState(runningLayer, state);
if (component.UnlitIdleState != null &&
component.UnlitRunningState != null)
{
var state = isRunning ? component.UnlitRunningState : component.UnlitIdleState;
args.Sprite.LayerSetState(powerLayer, state);
}
}
}

View File

@@ -100,11 +100,9 @@
Margin="5 0 0 0"
Text="{Loc 'lathe-menu-fabricating-message'}">
</Label>
<EntityPrototypeView
Name="FabricatingEntityProto"
HorizontalAlignment="Left"
Margin="100 0 0 0"
/>
<BoxContainer Name="FabricatingDisplayContainer"
HorizontalAlignment="Left"
Margin="100 0 0 0"/>
<Label
Name="NameLabel"
RectClipContent="True"

View File

@@ -1,18 +1,15 @@
using System.Buffers;
using System.Linq;
using System.Text;
using Content.Client.Materials;
using Content.Shared.Lathe;
using Content.Shared.Lathe.Prototypes;
using Content.Shared.Materials;
using Content.Shared.Research.Prototypes;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.ResourceManagement;
using Robust.Client.Graphics;
using Robust.Shared.Prototypes;
namespace Content.Client.Lathe.UI;
@@ -92,7 +89,7 @@ public sealed partial class LatheMenu : DefaultWindow
if (SearchBar.Text.Trim().Length != 0)
{
if (proto.Name.ToLowerInvariant().Contains(SearchBar.Text.Trim().ToLowerInvariant()))
if (_lathe.GetRecipeName(recipe).ToLowerInvariant().Contains(SearchBar.Text.Trim().ToLowerInvariant()))
recipesToShow.Add(proto);
}
else
@@ -104,19 +101,15 @@ public sealed partial class LatheMenu : DefaultWindow
if (!int.TryParse(AmountLineEdit.Text, out var quantity) || quantity <= 0)
quantity = 1;
var sortedRecipesToShow = recipesToShow.OrderBy(p => p.Name);
var sortedRecipesToShow = recipesToShow.OrderBy(_lathe.GetRecipeName);
RecipeList.Children.Clear();
_entityManager.TryGetComponent(Entity, out LatheComponent? lathe);
foreach (var prototype in sortedRecipesToShow)
{
EntityPrototype? recipeProto = null;
if (_prototypeManager.TryIndex(prototype.Result, out EntityPrototype? entityProto))
recipeProto = entityProto;
var canProduce = _lathe.CanProduce(Entity, prototype, quantity, component: lathe);
var control = new RecipeControl(prototype, () => GenerateTooltipText(prototype), canProduce, recipeProto);
var control = new RecipeControl(_lathe, prototype, () => GenerateTooltipText(prototype), canProduce, GetRecipeDisplayControl(prototype));
control.OnButtonPressed += s =>
{
if (!int.TryParse(AmountLineEdit.Text, out var amount) || amount <= 0)
@@ -132,9 +125,9 @@ public sealed partial class LatheMenu : DefaultWindow
StringBuilder sb = new();
var multiplier = _entityManager.GetComponent<LatheComponent>(Entity).MaterialUseMultiplier;
foreach (var (id, amount) in prototype.RequiredMaterials)
foreach (var (id, amount) in prototype.Materials)
{
if (!_prototypeManager.TryIndex<MaterialPrototype>(id, out var proto))
if (!_prototypeManager.TryIndex(id, out var proto))
continue;
var adjustedAmount = SharedLatheSystem.AdjustMaterial(amount, prototype.ApplyMaterialDiscount, multiplier);
@@ -163,8 +156,9 @@ public sealed partial class LatheMenu : DefaultWindow
sb.AppendLine(tooltipText);
}
if (!string.IsNullOrWhiteSpace(prototype.Description))
sb.AppendLine(Loc.GetString("lathe-menu-description-display", ("description", prototype.Description)));
var desc = _lathe.GetRecipeDescription(prototype);
if (!string.IsNullOrWhiteSpace(desc))
sb.AppendLine(Loc.GetString("lathe-menu-description-display", ("description", desc)));
// Remove last newline
if (sb.Length > 0)
@@ -222,13 +216,10 @@ public sealed partial class LatheMenu : DefaultWindow
var queuedRecipeBox = new BoxContainer();
queuedRecipeBox.Orientation = BoxContainer.LayoutOrientation.Horizontal;
var queuedRecipeProto = new EntityPrototypeView();
queuedRecipeBox.AddChild(queuedRecipeProto);
if (_prototypeManager.TryIndex(recipe.Result, out EntityPrototype? entityProto) && entityProto != null)
queuedRecipeProto.SetPrototype(entityProto);
queuedRecipeBox.AddChild(GetRecipeDisplayControl(recipe));
var queuedRecipeLabel = new Label();
queuedRecipeLabel.Text = $"{idx}. {recipe.Name}";
queuedRecipeLabel.Text = $"{idx}. {_lathe.GetRecipeName(recipe)}";
queuedRecipeBox.AddChild(queuedRecipeLabel);
QueueList.AddChild(queuedRecipeBox);
idx++;
@@ -241,10 +232,29 @@ public sealed partial class LatheMenu : DefaultWindow
if (recipe == null)
return;
if (_prototypeManager.TryIndex(recipe.Result, out EntityPrototype? entityProto) && entityProto != null)
FabricatingEntityProto.SetPrototype(entityProto);
FabricatingDisplayContainer.Children.Clear();
FabricatingDisplayContainer.AddChild(GetRecipeDisplayControl(recipe));
NameLabel.Text = $"{recipe.Name}";
NameLabel.Text = _lathe.GetRecipeName(recipe);
}
public Control GetRecipeDisplayControl(LatheRecipePrototype recipe)
{
if (recipe.Icon != null)
{
var textRect = new TextureRect();
textRect.Texture = _spriteSystem.Frame0(recipe.Icon);
return textRect;
}
if (recipe.Result is { } result)
{
var entProtoView = new EntityPrototypeView();
entProtoView.SetPrototype(result);
return entProtoView;
}
return new Control();
}
private void OnItemSelected(OptionButton.ItemSelectedEventArgs obj)

View File

@@ -5,8 +5,8 @@
Margin="0"
StyleClasses="ButtonSquare">
<BoxContainer Orientation="Horizontal">
<EntityPrototypeView
Name="RecipePrototype"
<BoxContainer
Name="RecipeDisplayContainer"
Margin="0 0 4 0"
HorizontalAlignment="Center"
VerticalAlignment="Center"

View File

@@ -1,10 +1,7 @@
using Content.Shared.Research.Prototypes;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
namespace Content.Client.Lathe.UI;
@@ -14,13 +11,12 @@ public sealed partial class RecipeControl : Control
public Action<string>? OnButtonPressed;
public Func<string> TooltipTextSupplier;
public RecipeControl(LatheRecipePrototype recipe, Func<string> tooltipTextSupplier, bool canProduce, EntityPrototype? entityPrototype = null)
public RecipeControl(LatheSystem latheSystem, LatheRecipePrototype recipe, Func<string> tooltipTextSupplier, bool canProduce, Control displayControl)
{
RobustXamlLoader.Load(this);
RecipeName.Text = recipe.Name;
if (entityPrototype != null)
RecipePrototype.SetPrototype(entityPrototype);
RecipeName.Text = latheSystem.GetRecipeName(recipe);
RecipeDisplayContainer.AddChild(displayControl);
Button.Disabled = !canProduce;
TooltipTextSupplier = tooltipTextSupplier;
Button.TooltipSupplier = SupplyTooltip;

View File

@@ -43,6 +43,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
[UISystemDependency] private readonly ClientInventorySystem _inventory = default!;
[UISystemDependency] private readonly StationSpawningSystem _spawn = default!;
[UISystemDependency] private readonly GuidebookSystem _guide = default!;
[UISystemDependency] private readonly LoadoutSystem _loadouts = default!;
private CharacterSetupGui? _characterSetup;
private HumanoidProfileEditor? _profileEditor;
@@ -272,6 +273,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
_logManager,
_playerManager,
_prototypeManager,
_resourceCache,
_requirements,
_markings);
@@ -364,7 +366,7 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
if (!_prototypeManager.TryIndex(loadout.Prototype, out var loadoutProto))
continue;
_spawn.EquipStartingGear(uid, _prototypeManager.Index(loadoutProto.Equipment));
_spawn.EquipStartingGear(uid, loadoutProto);
}
}
}
@@ -387,36 +389,51 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
if (!_prototypeManager.TryIndex(loadout.Prototype, out var loadoutProto))
continue;
// TODO: Need some way to apply starting gear to an entity coz holy fucking shit dude.
var loadoutGear = _prototypeManager.Index(loadoutProto.Equipment);
// TODO: Need some way to apply starting gear to an entity and replace existing stuff coz holy fucking shit dude.
foreach (var slot in slots)
{
var itemType = loadoutGear.GetGear(slot.Name);
if (_inventory.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
// Try startinggear first
if (_prototypeManager.TryIndex(loadoutProto.StartingGear, out var loadoutGear))
{
EntityManager.DeleteEntity(unequippedItem.Value);
var itemType = ((IEquipmentLoadout) loadoutGear).GetGear(slot.Name);
if (_inventory.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
{
EntityManager.DeleteEntity(unequippedItem.Value);
}
if (itemType != string.Empty)
{
var item = EntityManager.SpawnEntity(itemType, MapCoordinates.Nullspace);
_inventory.TryEquip(dummy, item, slot.Name, true, true);
}
}
if (itemType != string.Empty)
else
{
var item = EntityManager.SpawnEntity(itemType, MapCoordinates.Nullspace);
_inventory.TryEquip(dummy, item, slot.Name, true, true);
var itemType = ((IEquipmentLoadout) loadoutProto).GetGear(slot.Name);
if (_inventory.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
{
EntityManager.DeleteEntity(unequippedItem.Value);
}
if (itemType != string.Empty)
{
var item = EntityManager.SpawnEntity(itemType, MapCoordinates.Nullspace);
_inventory.TryEquip(dummy, item, slot.Name, true, true);
}
}
}
}
}
}
if (job.StartingGear == null)
if (!_prototypeManager.TryIndex(job.StartingGear, out var gear))
return;
var gear = _prototypeManager.Index<StartingGearPrototype>(job.StartingGear);
foreach (var slot in slots)
{
var itemType = gear.GetGear(slot.Name);
var itemType = ((IEquipmentLoadout) gear).GetGear(slot.Name);
if (_inventory.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
{

View File

@@ -38,6 +38,8 @@
<Button Name="ResetButton" Disabled="True" Text="{Loc 'humanoid-profile-editor-reset-button'}"/>
<Button Name="ImportButton" Text="{Loc 'humanoid-profile-editor-import-button'}"/>
<Button Name="ExportButton" Text="{Loc 'humanoid-profile-editor-export-button'}"/>
<Button Name="ExportImageButton" Text="{Loc 'humanoid-profile-editor-export-image-button'}"/>
<Button Name="OpenImagesButton" Text="{Loc 'humanoid-profile-editor-open-image-button'}"/>
</BoxContainer>
</ui:HighlightedContainer>
</BoxContainer>

View File

@@ -6,6 +6,7 @@ using Content.Client.Lobby.UI.Loadouts;
using Content.Client.Lobby.UI.Roles;
using Content.Client.Message;
using Content.Client.Players.PlayTimeTracking;
using Content.Client.Sprite;
using Content.Client.Stylesheets;
using Content.Client.UserInterface.Systems.Guidebook;
using Content.Shared._CP14.Humanoid;
@@ -28,6 +29,7 @@ using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.Utility;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Enums;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
@@ -44,6 +46,7 @@ namespace Content.Client.Lobby.UI
private readonly IFileDialogManager _dialogManager;
private readonly IPlayerManager _playerManager;
private readonly IPrototypeManager _prototypeManager;
private readonly IResourceManager _resManager;
private readonly MarkingManager _markingManager;
private readonly JobRequirementsManager _requirements;
private readonly LobbyUIController _controller;
@@ -55,6 +58,7 @@ namespace Content.Client.Lobby.UI
private LoadoutWindow? _loadoutWindow;
private bool _exporting;
private bool _imaging;
/// <summary>
/// If we're attempting to save.
@@ -108,6 +112,7 @@ namespace Content.Client.Lobby.UI
ILogManager logManager,
IPlayerManager playerManager,
IPrototypeManager prototypeManager,
IResourceManager resManager,
JobRequirementsManager requirements,
MarkingManager markings)
{
@@ -120,6 +125,7 @@ namespace Content.Client.Lobby.UI
_prototypeManager = prototypeManager;
_markingManager = markings;
_preferencesManager = preferencesManager;
_resManager = resManager;
_requirements = requirements;
_controller = UserInterfaceManager.GetUIController<LobbyUIController>();
@@ -133,6 +139,16 @@ namespace Content.Client.Lobby.UI
ExportProfile();
};
ExportImageButton.OnPressed += args =>
{
ExportImage();
};
OpenImagesButton.OnPressed += args =>
{
_resManager.UserData.OpenOsWindow(ContentSpriteSystem.Exports);
};
ResetButton.OnPressed += args =>
{
SetProfile((HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter, _preferencesManager.Preferences?.SelectedCharacterIndex);
@@ -425,7 +441,6 @@ namespace Content.Client.Lobby.UI
SpeciesInfoButton.OnPressed += OnSpeciesInfoButtonPressed;
UpdateSpeciesGuidebookIcon();
ReloadPreview();
IsDirty = false;
}
@@ -635,7 +650,7 @@ namespace Content.Client.Lobby.UI
selector.Select(Profile?.AntagPreferences.Contains(antag.ID) == true ? 0 : 1);
var requirements = _entManager.System<SharedRoleSystem>().GetAntagRequirement(antag);
if (!_requirements.CheckRoleTime(requirements, out var reason))
if (!_requirements.CheckRoleRequirements(requirements, (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter, out var reason))
{
selector.LockRequirements(reason);
Profile = Profile?.WithAntagPreference(antag.ID, false);
@@ -698,11 +713,12 @@ namespace Content.Client.Lobby.UI
_entManager.DeleteEntity(PreviewDummy);
PreviewDummy = EntityUid.Invalid;
if (Profile == null || !_prototypeManager.HasIndex<SpeciesPrototype>(Profile.Species))
if (Profile == null || !_prototypeManager.HasIndex(Profile.Species))
return;
PreviewDummy = _controller.LoadProfileEntity(Profile, JobOverride, ShowClothes.Pressed);
SpriteView.SetEntity(PreviewDummy);
_entManager.System<MetaDataSystem>().SetEntityName(PreviewDummy, Profile.Name);
}
/// <summary>
@@ -888,7 +904,7 @@ namespace Content.Client.Lobby.UI
icon.Texture = jobIcon.Icon.Frame0();
selector.Setup(items, job.LocalizedName, 200, job.LocalizedDescription, icon, job.Guides);
if (!_requirements.IsAllowed(job, out var reason))
if (!_requirements.IsAllowed(job, (HumanoidCharacterProfile?)_preferencesManager.Preferences?.SelectedCharacter, out var reason))
{
selector.LockRequirements(reason);
}
@@ -1139,6 +1155,17 @@ namespace Content.Client.Lobby.UI
_loadoutWindow?.Dispose();
_loadoutWindow = null;
}
protected override void EnteredTree()
{
base.EnteredTree();
ReloadPreview();
}
protected override void ExitedTree()
{
base.ExitedTree();
_entManager.DeleteEntity(PreviewDummy);
PreviewDummy = EntityUid.Invalid;
}
@@ -1199,6 +1226,11 @@ namespace Content.Client.Lobby.UI
{
Profile = Profile?.WithName(newName);
SetDirty();
if (!IsDirty)
return;
_entManager.System<MetaDataSystem>().SetEntityName(PreviewDummy, newName);
}
private void SetSpawnPriority(SpawnPriorityPreference newSpawnPriority)
@@ -1543,6 +1575,19 @@ namespace Content.Client.Lobby.UI
UpdateNameEdit();
}
private async void ExportImage()
{
if (_imaging)
return;
var dir = SpriteView.OverrideDirection ?? Direction.South;
// I tried disabling the button but it looks sorta goofy as it only takes a frame or two to save
_imaging = true;
await _entManager.System<ContentSpriteSystem>().Export(PreviewDummy, dir, includeId: false);
_imaging = false;
}
private async void ImportProfile()
{
if (_exporting || CharacterSlot == null || Profile == null)

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 ITileDefinitionManager _tileMan = default!;
[Dependency] private readonly ActionsSystem _actionsSystem = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
/// <summary>
@@ -26,8 +25,6 @@ public sealed partial class MappingSystem : EntitySystem
/// </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();
@@ -36,11 +33,6 @@ public sealed partial class MappingSystem : EntitySystem
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

View File

@@ -64,15 +64,15 @@ public sealed class JetpackSystem : SharedJetpackSystem
private void CreateParticles(EntityUid uid)
{
var uidXform = Transform(uid);
// Don't show particles unless the user is moving.
if (Container.TryGetContainingContainer(uid, out var container) &&
if (Container.TryGetContainingContainer((uid, uidXform, null), out var container) &&
TryComp<PhysicsComponent>(container.Owner, out var body) &&
body.LinearVelocity.LengthSquared() < 1f)
{
return;
}
var uidXform = Transform(uid);
var coordinates = uidXform.Coordinates;
var gridUid = _transform.GetGrid(coordinates);

View File

@@ -28,7 +28,7 @@ public sealed class SpriteMovementSystem : EntitySystem
return;
var oldMoving = (SharedMoverController.GetNormalizedMovement(args.OldMovement) & MoveButtons.AnyDirection) != MoveButtons.None;
var moving = (SharedMoverController.GetNormalizedMovement(args.Component.HeldMoveButtons) & MoveButtons.AnyDirection) != MoveButtons.None;
var moving = (SharedMoverController.GetNormalizedMovement(args.Entity.Comp.HeldMoveButtons) & MoveButtons.AnyDirection) != MoveButtons.None;
if (oldMoving == moving || !_spriteQuery.TryGetComponent(uid, out var sprite))
return;

View File

@@ -0,0 +1,35 @@
using Content.Shared.Paper;
using Robust.Client.GameObjects;
namespace Content.Client.Paper;
public sealed class EnvelopeSystem : VisualizerSystem<EnvelopeComponent>
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EnvelopeComponent, AfterAutoHandleStateEvent>(OnAfterAutoHandleState);
}
private void OnAfterAutoHandleState(Entity<EnvelopeComponent> ent, ref AfterAutoHandleStateEvent args)
{
UpdateAppearance(ent);
}
private void UpdateAppearance(Entity<EnvelopeComponent> ent, SpriteComponent? sprite = null)
{
if (!Resolve(ent.Owner, ref sprite))
return;
sprite.LayerSetVisible(EnvelopeVisualLayers.Open, ent.Comp.State == EnvelopeComponent.EnvelopeState.Open);
sprite.LayerSetVisible(EnvelopeVisualLayers.Sealed, ent.Comp.State == EnvelopeComponent.EnvelopeState.Sealed);
sprite.LayerSetVisible(EnvelopeVisualLayers.Torn, ent.Comp.State == EnvelopeComponent.EnvelopeState.Torn);
}
public enum EnvelopeVisualLayers : byte
{
Open,
Sealed,
Torn
}
}

View File

@@ -1,6 +0,0 @@
using Content.Shared.Paper;
namespace Content.Client.Paper;
[RegisterComponent]
public sealed partial class PaperComponent : SharedPaperComponent;

View File

@@ -2,7 +2,7 @@ using JetBrains.Annotations;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Utility;
using static Content.Shared.Paper.SharedPaperComponent;
using static Content.Shared.Paper.PaperComponent;
namespace Content.Client.Paper.UI;

View File

@@ -1,10 +1,10 @@
using Robust.Client.GameObjects;
using static Content.Shared.Paper.SharedPaperComponent;
using static Content.Shared.Paper.PaperComponent;
namespace Content.Client.Paper;
namespace Content.Client.Paper.UI;
public sealed class PaperSystem : VisualizerSystem<PaperVisualsComponent>
public sealed class PaperVisualizerSystem : VisualizerSystem<PaperVisualsComponent>
{
protected override void OnAppearanceChange(EntityUid uid, PaperVisualsComponent component, ref AppearanceChangeEvent args)
{

View File

@@ -1,6 +1,6 @@
using System.Numerics;
namespace Content.Client.Paper;
namespace Content.Client.Paper.UI;
[RegisterComponent]
public sealed partial class PaperVisualsComponent : Component

View File

@@ -215,9 +215,9 @@ namespace Content.Client.Paper.UI
/// Initialize the paper contents, i.e. the text typed by the
/// user and any stamps that have peen put on the page.
/// </summary>
public void Populate(SharedPaperComponent.PaperBoundUserInterfaceState state)
public void Populate(PaperComponent.PaperBoundUserInterfaceState state)
{
bool isEditing = state.Mode == SharedPaperComponent.PaperAction.Write;
bool isEditing = state.Mode == PaperComponent.PaperAction.Write;
bool wasEditing = InputContainer.Visible;
InputContainer.Visible = isEditing;
EditButtons.Visible = isEditing;

View File

@@ -3,6 +3,7 @@ using Content.Client.Message;
using Content.Client.Resources;
using Content.Client.UserInterface.Controls;
using Content.Shared.Singularity.Components;
using Content.Shared.Access.Systems;
using Robust.Client.Animations;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
@@ -13,6 +14,7 @@ using Robust.Client.UserInterface.XAML;
using Robust.Shared.Noise;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Client.Player;
namespace Content.Client.ParticleAccelerator.UI;
@@ -21,6 +23,11 @@ public sealed partial class ParticleAcceleratorControlMenu : FancyWindow
{
[Dependency] private readonly IResourceCache _cache = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPlayerManager _player = default!;
private readonly AccessReaderSystem _accessReader;
private readonly FastNoiseLite _drawNoiseGenerator;
private readonly Animation _alarmControlAnimation;
@@ -44,6 +51,7 @@ public sealed partial class ParticleAcceleratorControlMenu : FancyWindow
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_accessReader = _entityManager.System<AccessReaderSystem>();
_drawNoiseGenerator = new();
_drawNoiseGenerator.SetFractalType(FastNoiseLite.FractalType.FBm);
_drawNoiseGenerator.SetFrequency(0.5f);
@@ -150,7 +158,7 @@ public sealed partial class ParticleAcceleratorControlMenu : FancyWindow
private bool StrengthSpinBoxValid(int n)
{
return n >= 0 && n <= _maxStrength ;
return n >= 0 && n <= _maxStrength;
}
protected override DragMode GetDragModeFor(Vector2 relativeMousePos)
@@ -201,13 +209,16 @@ public sealed partial class ParticleAcceleratorControlMenu : FancyWindow
private void UpdateUI(bool assembled, bool blocked, bool enabled, bool powerBlock)
{
bool hasAccess = _player.LocalSession?.AttachedEntity is {} player
&& _accessReader.IsAllowed(player, _entity);
OnButton.Pressed = enabled;
OffButton.Pressed = !enabled;
var cantUse = !assembled || blocked || powerBlock;
var cantUse = !assembled || blocked || powerBlock || !hasAccess;
OnButton.Disabled = cantUse;
OffButton.Disabled = cantUse;
ScanButton.Disabled = blocked;
ScanButton.Disabled = blocked || !hasAccess;
var cantChangeLevel = !assembled || blocked || !enabled || cantUse;
StateSpinBox.SetButtonDisabled(cantChangeLevel);

View File

@@ -28,57 +28,57 @@ namespace Content.Client.Physics.Controllers
SubscribeLocalEvent<PullableComponent, UpdateIsPredictedEvent>(OnUpdatePullablePredicted);
}
private void OnUpdatePredicted(EntityUid uid, InputMoverComponent component, ref UpdateIsPredictedEvent args)
private void OnUpdatePredicted(Entity<InputMoverComponent> entity, ref UpdateIsPredictedEvent args)
{
// Enable prediction if an entity is controlled by the player
if (uid == _playerManager.LocalEntity)
if (entity.Owner == _playerManager.LocalEntity)
args.IsPredicted = true;
}
private void OnUpdateRelayTargetPredicted(EntityUid uid, MovementRelayTargetComponent component, ref UpdateIsPredictedEvent args)
private void OnUpdateRelayTargetPredicted(Entity<MovementRelayTargetComponent> entity, ref UpdateIsPredictedEvent args)
{
if (component.Source == _playerManager.LocalEntity)
if (entity.Comp.Source == _playerManager.LocalEntity)
args.IsPredicted = true;
}
private void OnUpdatePullablePredicted(EntityUid uid, PullableComponent component, ref UpdateIsPredictedEvent args)
private void OnUpdatePullablePredicted(Entity<PullableComponent> entity, ref UpdateIsPredictedEvent args)
{
// Enable prediction if an entity is being pulled by the player.
// Disable prediction if an entity is being pulled by some non-player entity.
if (component.Puller == _playerManager.LocalEntity)
if (entity.Comp.Puller == _playerManager.LocalEntity)
args.IsPredicted = true;
else if (component.Puller != null)
else if (entity.Comp.Puller != null)
args.BlockPrediction = true;
// TODO recursive pulling checks?
// What if the entity is being pulled by a vehicle controlled by the player?
}
private void OnRelayPlayerAttached(EntityUid uid, RelayInputMoverComponent component, LocalPlayerAttachedEvent args)
private void OnRelayPlayerAttached(Entity<RelayInputMoverComponent> entity, ref LocalPlayerAttachedEvent args)
{
Physics.UpdateIsPredicted(uid);
Physics.UpdateIsPredicted(component.RelayEntity);
if (MoverQuery.TryGetComponent(component.RelayEntity, out var inputMover))
SetMoveInput(inputMover, MoveButtons.None);
Physics.UpdateIsPredicted(entity.Owner);
Physics.UpdateIsPredicted(entity.Comp.RelayEntity);
if (MoverQuery.TryGetComponent(entity.Comp.RelayEntity, out var inputMover))
SetMoveInput((entity.Owner, inputMover), MoveButtons.None);
}
private void OnRelayPlayerDetached(EntityUid uid, RelayInputMoverComponent component, LocalPlayerDetachedEvent args)
private void OnRelayPlayerDetached(Entity<RelayInputMoverComponent> entity, ref LocalPlayerDetachedEvent args)
{
Physics.UpdateIsPredicted(uid);
Physics.UpdateIsPredicted(component.RelayEntity);
if (MoverQuery.TryGetComponent(component.RelayEntity, out var inputMover))
SetMoveInput(inputMover, MoveButtons.None);
Physics.UpdateIsPredicted(entity.Owner);
Physics.UpdateIsPredicted(entity.Comp.RelayEntity);
if (MoverQuery.TryGetComponent(entity.Comp.RelayEntity, out var inputMover))
SetMoveInput((entity.Owner, inputMover), MoveButtons.None);
}
private void OnPlayerAttached(EntityUid uid, InputMoverComponent component, LocalPlayerAttachedEvent args)
private void OnPlayerAttached(Entity<InputMoverComponent> entity, ref LocalPlayerAttachedEvent args)
{
SetMoveInput(component, MoveButtons.None);
SetMoveInput(entity, MoveButtons.None);
}
private void OnPlayerDetached(EntityUid uid, InputMoverComponent component, LocalPlayerDetachedEvent args)
private void OnPlayerDetached(Entity<InputMoverComponent> entity, ref LocalPlayerDetachedEvent args)
{
SetMoveInput(component, MoveButtons.None);
SetMoveInput(entity, MoveButtons.None);
}
public override void UpdateBeforeSolve(bool prediction, float frameTime)

View File

@@ -1,8 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using Content.Client.Lobby;
using Content.Shared.CCVar;
using Content.Shared.Players;
using Content.Shared.Players.JobWhitelist;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Client;
using Robust.Client.Player;
@@ -89,7 +91,7 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
Updated?.Invoke();
}
public bool IsAllowed(JobPrototype job, [NotNullWhen(false)] out FormattedMessage? reason)
public bool IsAllowed(JobPrototype job, HumanoidCharacterProfile? profile, [NotNullWhen(false)] out FormattedMessage? reason)
{
reason = null;
@@ -106,16 +108,16 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
if (player == null)
return true;
return CheckRoleTime(job, out reason);
return CheckRoleRequirements(job, profile, out reason);
}
public bool CheckRoleTime(JobPrototype job, [NotNullWhen(false)] out FormattedMessage? reason)
public bool CheckRoleRequirements(JobPrototype job, HumanoidCharacterProfile? profile, [NotNullWhen(false)] out FormattedMessage? reason)
{
var reqs = _entManager.System<SharedRoleSystem>().GetJobRequirement(job);
return CheckRoleTime(reqs, out reason);
return CheckRoleRequirements(reqs, profile, out reason);
}
public bool CheckRoleTime(HashSet<JobRequirement>? requirements, [NotNullWhen(false)] out FormattedMessage? reason)
public bool CheckRoleRequirements(HashSet<JobRequirement>? requirements, HumanoidCharacterProfile? profile, [NotNullWhen(false)] out FormattedMessage? reason)
{
reason = null;
@@ -125,7 +127,7 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
var reasons = new List<string>();
foreach (var requirement in requirements)
{
if (JobRequirements.TryRequirementMet(requirement, _roles, out var jobReason, _entManager, _prototypes))
if (requirement.Check(_entManager, _prototypes, profile, _roles, out var jobReason))
continue;
reasons.Add(jobReason.ToMarkup());

View File

@@ -1,6 +1,7 @@
using Content.Client.Power.Components;
using Content.Client.Power.Components;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Examine;
using Robust.Shared.GameStates;
namespace Content.Client.Power.EntitySystems;
@@ -10,9 +11,15 @@ public sealed class PowerReceiverSystem : SharedPowerReceiverSystem
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ApcPowerReceiverComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<ApcPowerReceiverComponent, ComponentHandleState>(OnHandleState);
}
private void OnExamined(Entity<ApcPowerReceiverComponent> ent, ref ExaminedEvent args)
{
args.PushMarkup(GetExamineText(ent.Comp.Powered));
}
private void OnHandleState(EntityUid uid, ApcPowerReceiverComponent component, ref ComponentHandleState args)
{
if (args.Current is not ApcPowerReceiverComponentState state)

View File

@@ -102,12 +102,21 @@ public sealed class SalvageExpeditionConsoleBoundUserInterface : BoundUserInterf
offering.AddContent(new Label
{
Text = faction,
Text = string.IsNullOrWhiteSpace(Loc.GetString(_protoManager.Index<SalvageFactionPrototype>(faction).Description))
? LogAndReturnDefaultFactionDescription(faction)
: Loc.GetString(_protoManager.Index<SalvageFactionPrototype>(faction).Description),
FontColorOverride = StyleNano.NanoGold,
HorizontalAlignment = Control.HAlignment.Left,
Margin = new Thickness(0f, 0f, 0f, 5f),
});
string LogAndReturnDefaultFactionDescription(string faction)
{
Logger.Error($"Description is null or white space for SalvageFactionPrototype: {faction}");
return Loc.GetString(_protoManager.Index<SalvageFactionPrototype>(faction).ID);
}
// Duration
offering.AddContent(new Label
{
@@ -132,12 +141,20 @@ public sealed class SalvageExpeditionConsoleBoundUserInterface : BoundUserInterf
offering.AddContent(new Label
{
Text = Loc.GetString(_protoManager.Index<SalvageBiomeModPrototype>(biome).ID),
Text = string.IsNullOrWhiteSpace(Loc.GetString(_protoManager.Index<SalvageBiomeModPrototype>(biome).Description))
? LogAndReturnDefaultBiomDescription(biome)
: Loc.GetString(_protoManager.Index<SalvageBiomeModPrototype>(biome).Description),
FontColorOverride = StyleNano.NanoGold,
HorizontalAlignment = Control.HAlignment.Left,
Margin = new Thickness(0f, 0f, 0f, 5f),
});
string LogAndReturnDefaultBiomDescription(string biome)
{
Logger.Error($"Description is null or white space for SalvageBiomeModPrototype: {biome}");
return Loc.GetString(_protoManager.Index<SalvageBiomeModPrototype>(biome).ID);
}
// Modifiers
offering.AddContent(new Label
{

View File

@@ -100,6 +100,8 @@ namespace Content.Client.Singularity
/// </summary>
private void OnProjectFromScreenToMap(ref PixelToMapEvent args)
{ // Mostly copypasta from the singularity shader.
if (args.Viewport.Eye == null)
return;
var maxDistance = MaxDistance * EyeManager.PixelsPerMeter;
var finalCoords = args.VisiblePosition;
@@ -112,10 +114,11 @@ namespace Content.Client.Singularity
// and in local space 'Y' is measured in pixels from the top of the viewport.
// As a minor optimization the locations of the singularities are transformed into fragment space in BeforeDraw so the shader doesn't need to.
// We need to undo that here or this will transform the cursor position as if the singularities were mirrored vertically relative to the center of the viewport.
var localPosition = _positions[i];
localPosition.Y = args.Viewport.Size.Y - localPosition.Y;
var delta = args.VisiblePosition - localPosition;
var distance = (delta / args.Viewport.RenderScale).Length();
var distance = (delta / (args.Viewport.RenderScale * args.Viewport.Eye.Scale)).Length();
var deformation = _intensities[i] / MathF.Pow(distance, _falloffPowers[i]);

View File

@@ -0,0 +1,218 @@
using System.IO;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
using Content.Client.Administration.Managers;
using Content.Shared.Database;
using Content.Shared.Verbs;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Shared.ContentPack;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using Color = Robust.Shared.Maths.Color;
namespace Content.Client.Sprite;
public sealed class ContentSpriteSystem : EntitySystem
{
[Dependency] private readonly IClientAdminManager _adminManager = default!;
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IResourceManager _resManager = default!;
[Dependency] private readonly IUserInterfaceManager _ui = default!;
private ContentSpriteControl _control = new();
public static readonly ResPath Exports = new ResPath("/Exports");
public override void Initialize()
{
base.Initialize();
_resManager.UserData.CreateDir(Exports);
_ui.RootControl.AddChild(_control);
SubscribeLocalEvent<GetVerbsEvent<Verb>>(GetVerbs);
}
public override void Shutdown()
{
base.Shutdown();
foreach (var queued in _control._queuedTextures)
{
queued.Tcs.SetCanceled();
}
_control._queuedTextures.Clear();
_ui.RootControl.RemoveChild(_control);
}
/// <summary>
/// Exports sprites for all directions
/// </summary>
public async Task Export(EntityUid entity, bool includeId = true, CancellationToken cancelToken = default)
{
var tasks = new Task[4];
var i = 0;
foreach (var dir in new Direction[]
{
Direction.South,
Direction.East,
Direction.North,
Direction.West,
})
{
tasks[i++] = Export(entity, dir, includeId: includeId, cancelToken);
}
await Task.WhenAll(tasks);
}
/// <summary>
/// Exports the sprite for a particular direction.
/// </summary>
public async Task Export(EntityUid entity, Direction direction, bool includeId = true, CancellationToken cancelToken = default)
{
if (!_timing.IsFirstTimePredicted)
return;
if (!TryComp(entity, out SpriteComponent? spriteComp))
return;
// Don't want to wait for engine pr
var size = Vector2i.Zero;
foreach (var layer in spriteComp.AllLayers)
{
if (!layer.Visible)
continue;
size = Vector2i.ComponentMax(size, layer.PixelSize);
}
// Stop asserts
if (size.Equals(Vector2i.Zero))
return;
var texture = _clyde.CreateRenderTarget(new Vector2i(size.X, size.Y), new RenderTargetFormatParameters(RenderTargetColorFormat.Rgba8Srgb), name: "export");
var tcs = new TaskCompletionSource(cancelToken);
_control._queuedTextures.Enqueue((texture, direction, entity, includeId, tcs));
await tcs.Task;
}
private void GetVerbs(GetVerbsEvent<Verb> ev)
{
if (!_adminManager.IsAdmin())
return;
Verb verb = new()
{
Text = Loc.GetString("export-entity-verb-get-data-text"),
Category = VerbCategory.Debug,
Act = () =>
{
Export(ev.Target);
},
};
ev.Verbs.Add(verb);
}
/// <summary>
/// This is horrible. I asked PJB if there's an easy way to render straight to a texture outside of the render loop
/// and she also mentioned this as a bad possibility.
/// </summary>
private sealed class ContentSpriteControl : Control
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly ILogManager _logMan = default!;
[Dependency] private readonly IResourceManager _resManager = default!;
internal Queue<(
IRenderTexture Texture,
Direction Direction,
EntityUid Entity,
bool IncludeId,
TaskCompletionSource Tcs)> _queuedTextures = new();
private ISawmill _sawmill;
public ContentSpriteControl()
{
IoCManager.InjectDependencies(this);
_sawmill = _logMan.GetSawmill("sprite.export");
}
protected override void Draw(DrawingHandleScreen handle)
{
base.Draw(handle);
while (_queuedTextures.TryDequeue(out var queued))
{
if (queued.Tcs.Task.IsCanceled)
continue;
try
{
if (!_entManager.TryGetComponent(queued.Entity, out MetaDataComponent? metadata))
continue;
var filename = metadata.EntityName;
var result = queued;
handle.RenderInRenderTarget(queued.Texture, () =>
{
handle.DrawEntity(result.Entity, result.Texture.Size / 2, Vector2.One, Angle.Zero,
overrideDirection: result.Direction);
}, Color.Transparent);
ResPath fullFileName;
if (queued.IncludeId)
{
fullFileName = Exports / $"{filename}-{queued.Direction}-{queued.Entity}.png";
}
else
{
fullFileName = Exports / $"{filename}-{queued.Direction}.png";
}
queued.Texture.CopyPixelsToMemory<Rgba32>(image =>
{
if (_resManager.UserData.Exists(fullFileName))
{
_sawmill.Info($"Found existing file {fullFileName} to replace.");
_resManager.UserData.Delete(fullFileName);
}
using var file =
_resManager.UserData.Open(fullFileName, FileMode.CreateNew, FileAccess.Write,
FileShare.None);
image.SaveAsPng(file);
});
_sawmill.Info($"Saved screenshot to {fullFileName}");
queued.Tcs.SetResult();
}
catch (Exception exc)
{
queued.Texture.Dispose();
if (!string.IsNullOrEmpty(exc.StackTrace))
_sawmill.Fatal(exc.StackTrace);
queued.Tcs.SetException(exc);
}
}
}
}
}

View File

@@ -151,6 +151,11 @@ namespace Content.Client.Stylesheets
public static readonly Color ChatBackgroundColor = Color.FromHex("#25252ADD");
//Bwoink
public const string StyleClassPinButtonPinned = "pinButtonPinned";
public const string StyleClassPinButtonUnpinned = "pinButtonUnpinned";
public override Stylesheet Stylesheet { get; }
public StyleNano(IResourceCache resCache) : base(resCache)
@@ -1608,6 +1613,21 @@ namespace Content.Client.Stylesheets
{
BackgroundColor = FancyTreeSelectedRowColor,
}),
// Pinned button style
new StyleRule(
new SelectorElement(typeof(TextureButton), new[] { StyleClassPinButtonPinned }, null, null),
new[]
{
new StyleProperty(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Bwoink/pinned.png"))
}),
// Unpinned button style
new StyleRule(
new SelectorElement(typeof(TextureButton), new[] { StyleClassPinButtonUnpinned }, null, null),
new[]
{
new StyleProperty(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/Bwoink/un_pinned.png"))
})
}).ToList());
}
}

View File

@@ -1,4 +1,4 @@
using Content.Client.Paper;
using Content.Client.Paper.UI;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;

View File

@@ -1,21 +1,17 @@
using Content.Client.Gameplay;
using System.Numerics;
using Content.Client.Message;
using Content.Client.Paper;
using Content.Client.Paper.UI;
using Content.Shared.CCVar;
using Content.Shared.Movement.Components;
using Content.Shared.Tips;
using Robust.Client.GameObjects;
using Robust.Client.ResourceManagement;
using Robust.Client.State;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controllers;
using Robust.Client.UserInterface.Controls;
using Robust.Client.Audio;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using static Content.Client.Tips.TippyUI;

View File

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

View File

@@ -93,7 +93,7 @@ namespace Content.Client.UserInterface.Systems.Ghost.Controls.Roles
bool hasAccess = true;
FormattedMessage? reason;
if (!requirementsManager.CheckRoleTime(group.Key.Requirements, out reason))
if (!requirementsManager.CheckRoleRequirements(group.Key.Requirements, null, out reason))
{
hasAccess = false;
}

View File

@@ -3,6 +3,7 @@ using System.Numerics;
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;
@@ -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
/// 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!;
@@ -56,6 +59,22 @@ namespace Content.Client.Verbs.UI
Close();
}
public void OnStateEntered(MappingState state)
{
_context.OnContextKeyEvent += OnKeyBindDown;
_context.OnContextClosed += Close;
_verbSystem.OnVerbsResponse += HandleVerbsResponse;
}
public void OnStateExited(MappingState state)
{
_context.OnContextKeyEvent -= OnKeyBindDown;
_context.OnContextClosed -= Close;
if (_verbSystem != null)
_verbSystem.OnVerbsResponse -= HandleVerbsResponse;
Close();
}
/// <summary>
/// Open a verb menu and fill it with verbs applicable to the given target entity.
/// </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

@@ -193,7 +193,7 @@ public sealed class MaterialArbitrageTest
{
foreach (var recipe in recipes)
{
foreach (var (matId, amount) in recipe.RequiredMaterials)
foreach (var (matId, amount) in recipe.Materials)
{
var actualAmount = SharedLatheSystem.AdjustMaterial(amount, recipe.ApplyMaterialDiscount, multiplier);
if (spawnedMats.TryGetValue(matId, out var numSpawned))
@@ -273,7 +273,7 @@ public sealed class MaterialArbitrageTest
{
foreach (var recipe in recipes)
{
foreach (var (matId, amount) in recipe.RequiredMaterials)
foreach (var (matId, amount) in recipe.Materials)
{
var actualAmount = SharedLatheSystem.AdjustMaterial(amount, recipe.ApplyMaterialDiscount, multiplier);
if (deconstructedMats.TryGetValue(matId, out var numSpawned))
@@ -328,7 +328,7 @@ public sealed class MaterialArbitrageTest
{
foreach (var recipe in recipes)
{
foreach (var (matId, amount) in recipe.RequiredMaterials)
foreach (var (matId, amount) in recipe.Materials)
{
var actualAmount = SharedLatheSystem.AdjustMaterial(amount, recipe.ApplyMaterialDiscount, multiplier);
if (compositionComponent.MaterialComposition.TryGetValue(matId, out var numSpawned))

View File

@@ -45,6 +45,10 @@ public sealed class NPCTest
var count = counts.GetOrNew(compound.ID);
count++;
// Compound tasks marked with AllowRecursion are only evaluated once
if (counts.ContainsKey(compound.ID) && compound.AllowRecursion)
continue;
Assert.That(count, Is.LessThan(50));
counts[compound.ID] = count;
Count(protoManager.Index<HTNCompoundPrototype>(compoundTask.Task), counts, htnSystem, protoManager);

View File

@@ -0,0 +1,43 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Prototypes;
namespace Content.IntegrationTests.Tests.Physics;
[TestFixture]
public sealed class AnchorPrototypeTest
{
/// <summary>
/// Asserts that entityprototypes marked as anchored are also static physics bodies.
/// </summary>
[Test]
public async Task TestStaticAnchorPrototypes()
{
await using var pair = await PoolManager.GetServerClient();
var protoManager = pair.Server.ResolveDependency<IPrototypeManager>();
await pair.Server.WaitAssertion(() =>
{
foreach (var ent in protoManager.EnumeratePrototypes<EntityPrototype>())
{
if (!ent.Components.TryGetComponent("Transform", out var xformComp) ||
!ent.Components.TryGetComponent("Physics", out var physicsComp))
{
continue;
}
var xform = (TransformComponent)xformComp;
var physics = (PhysicsComponent)physicsComp;
if (!xform.Anchored)
continue;
Assert.That(physics.BodyType, Is.EqualTo(BodyType.Static), $"Found entity prototype {ent} marked as anchored but not static for physics.");
}
});
await pair.CleanReturnAsync();
}
}

View File

@@ -18,10 +18,6 @@ public sealed class LoadoutTests
id: PlayTimeLoadoutTester
- type: loadout
id: TestJumpsuit
equipment: TestJumpsuit
- type: startingGear
id: TestJumpsuit
equipment:
jumpsuit: ClothingUniformJumpsuitColorGrey

View File

@@ -35,7 +35,7 @@ public sealed class StartingGearPrototypeStorageTest
{
foreach (var gearProto in protos)
{
var backpackProto = gearProto.GetGear("back");
var backpackProto = ((IEquipmentLoadout) gearProto).GetGear("back");
if (backpackProto == string.Empty)
continue;

View File

@@ -25,7 +25,7 @@ public sealed partial class LogWireAction : ComponentWireAction<AccessReaderComp
return comp.LoggingDisabled ? StatusLightState.Off : StatusLightState.On;
}
public override object StatusKey => AccessWireActionKey.Status;
public override object StatusKey => LogWireActionKey.Status;
public override void Initialize()
{

View File

@@ -97,11 +97,12 @@ namespace Content.Server.Administration.Commands
foreach (var slot in slots)
{
invSystem.TryUnequip(target, slot.Name, true, true, false, inventoryComponent);
var gearStr = startingGear.GetGear(slot.Name);
var gearStr = ((IEquipmentLoadout) startingGear).GetGear(slot.Name);
if (gearStr == string.Empty)
{
continue;
}
var equipmentEntity = entityManager.SpawnEntity(gearStr, entityManager.GetComponent<TransformComponent>(target).Coordinates);
if (slot.Name == "id" &&
entityManager.TryGetComponent(equipmentEntity, out PdaComponent? pdaComponent) &&

View File

@@ -458,7 +458,7 @@ namespace Content.Server.Administration.Managers
Flags = flags
};
if (dbData.Title != null)
if (dbData.Title != null && _cfg.GetCVar(CCVars.AdminUseCustomNamesAdminRank))
{
data.Title = dbData.Title;
}

View File

@@ -180,7 +180,7 @@ namespace Content.Server.Administration.Systems
if (targetMind != null)
{
_mindSystem.TransferTo(targetMind.Value, mobUid);
_mindSystem.TransferTo(targetMind.Value, mobUid, true);
}
},
ConfirmationPopup = true,

View File

@@ -7,11 +7,13 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Content.Server.Administration.Managers;
using Content.Server.Afk;
using Content.Server.Database;
using Content.Server.Discord;
using Content.Server.GameTicking;
using Content.Server.Players.RateLimiting;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.Mind;
using JetBrains.Annotations;
using Robust.Server.Player;
@@ -38,6 +40,7 @@ namespace Content.Server.Administration.Systems
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly SharedMindSystem _minds = default!;
[Dependency] private readonly IAfkManager _afkManager = default!;
[Dependency] private readonly IServerDbManager _dbManager = default!;
[Dependency] private readonly PlayerRateLimitManager _rateLimit = default!;
[GeneratedRegex(@"^https://discord\.com/api/webhooks/(\d+)/((?!.*/).*)$")]
@@ -50,7 +53,11 @@ namespace Content.Server.Administration.Systems
private string _footerIconUrl = string.Empty;
private string _avatarUrl = string.Empty;
private string _serverName = string.Empty;
private readonly Dictionary<NetUserId, (string? id, string username, string description, string? characterName, GameRunLevel lastRunLevel)> _relayMessages = new();
private readonly
Dictionary<NetUserId, (string? id, string username, string description, string? characterName, GameRunLevel
lastRunLevel)> _relayMessages = new();
private Dictionary<NetUserId, string> _oldMessageIds = new();
private readonly Dictionary<NetUserId, Queue<string>> _messageQueues = new();
private readonly HashSet<NetUserId> _processingChannels = new();
@@ -69,6 +76,7 @@ namespace Content.Server.Administration.Systems
private const string TooLongText = "... **(too long)**";
private int _maxAdditionalChars;
private readonly Dictionary<NetUserId, DateTime> _activeConversations = new();
public override void Initialize()
{
@@ -79,13 +87,22 @@ namespace Content.Server.Administration.Systems
Subs.CVar(_config, CVars.GameHostName, OnServerNameChanged, true);
Subs.CVar(_config, CCVars.AdminAhelpOverrideClientName, OnOverrideChanged, true);
_sawmill = IoCManager.Resolve<ILogManager>().GetSawmill("AHELP");
_maxAdditionalChars = GenerateAHelpMessage("", "", true, _gameTicker.RoundDuration().ToString("hh\\:mm\\:ss"), _gameTicker.RunLevel, playedSound: false).Length;
var defaultParams = new AHelpMessageParams(
string.Empty,
string.Empty,
true,
_gameTicker.RoundDuration().ToString("hh\\:mm\\:ss"),
_gameTicker.RunLevel,
playedSound: false
);
_maxAdditionalChars = GenerateAHelpMessage(defaultParams).Length;
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
SubscribeLocalEvent<GameRunLevelChangedEvent>(OnGameRunLevelChanged);
SubscribeNetworkEvent<BwoinkClientTypingUpdated>(OnClientTypingUpdated);
SubscribeLocalEvent<RoundRestartCleanupEvent>(_ => _activeConversations.Clear());
_rateLimit.Register(
_rateLimit.Register(
RateLimitKey,
new RateLimitRegistration
{
@@ -107,14 +124,129 @@ namespace Content.Server.Administration.Systems
_overrideClientName = obj;
}
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
{
if (e.NewStatus == SessionStatus.Disconnected)
{
if (_activeConversations.TryGetValue(e.Session.UserId, out var lastMessageTime))
{
var timeSinceLastMessage = DateTime.Now - lastMessageTime;
if (timeSinceLastMessage > TimeSpan.FromMinutes(5))
{
_activeConversations.Remove(e.Session.UserId);
return; // Do not send disconnect message if timeout exceeded
}
}
// Check if the user has been banned
var ban = await _dbManager.GetServerBanAsync(null, e.Session.UserId, null);
if (ban != null)
{
var banMessage = Loc.GetString("bwoink-system-player-banned", ("banReason", ban.Reason));
NotifyAdmins(e.Session, banMessage, PlayerStatusType.Banned);
_activeConversations.Remove(e.Session.UserId);
return;
}
}
// Notify all admins if a player disconnects or reconnects
var message = e.NewStatus switch
{
SessionStatus.Connected => Loc.GetString("bwoink-system-player-reconnecting"),
SessionStatus.Disconnected => Loc.GetString("bwoink-system-player-disconnecting"),
_ => null
};
if (message != null)
{
var statusType = e.NewStatus == SessionStatus.Connected
? PlayerStatusType.Connected
: PlayerStatusType.Disconnected;
NotifyAdmins(e.Session, message, statusType);
}
if (e.NewStatus != SessionStatus.InGame)
return;
RaiseNetworkEvent(new BwoinkDiscordRelayUpdated(!string.IsNullOrWhiteSpace(_webhookUrl)), e.Session);
}
private void NotifyAdmins(ICommonSession session, string message, PlayerStatusType statusType)
{
if (!_activeConversations.ContainsKey(session.UserId))
{
// If the user is not part of an active conversation, do not notify admins.
return;
}
// Get the current timestamp
var timestamp = DateTime.Now.ToString("HH:mm:ss");
var roundTime = _gameTicker.RoundDuration().ToString("hh\\:mm\\:ss");
// Determine the icon based on the status type
string icon = statusType switch
{
PlayerStatusType.Connected => ":green_circle:",
PlayerStatusType.Disconnected => ":red_circle:",
PlayerStatusType.Banned => ":no_entry:",
_ => ":question:"
};
// Create the message parameters for Discord
var messageParams = new AHelpMessageParams(
session.Name,
message,
true,
roundTime,
_gameTicker.RunLevel,
playedSound: true,
icon: icon
);
// Create the message for in-game with username
var color = statusType switch
{
PlayerStatusType.Connected => Color.Green.ToHex(),
PlayerStatusType.Disconnected => Color.Yellow.ToHex(),
PlayerStatusType.Banned => Color.Orange.ToHex(),
_ => Color.Gray.ToHex(),
};
var inGameMessage = $"[color={color}]{session.Name} {message}[/color]";
var bwoinkMessage = new BwoinkTextMessage(
userId: session.UserId,
trueSender: SystemUserId,
text: inGameMessage,
sentAt: DateTime.Now,
playSound: false
);
var admins = GetTargetAdmins();
foreach (var admin in admins)
{
RaiseNetworkEvent(bwoinkMessage, admin);
}
// Enqueue the message for Discord relay
if (_webhookUrl != string.Empty)
{
// if (!_messageQueues.ContainsKey(session.UserId))
// _messageQueues[session.UserId] = new Queue<string>();
//
// var escapedText = FormattedMessage.EscapeText(message);
// messageParams.Message = escapedText;
//
// var discordMessage = GenerateAHelpMessage(messageParams);
// _messageQueues[session.UserId].Enqueue(discordMessage);
var queue = _messageQueues.GetOrNew(session.UserId);
var escapedText = FormattedMessage.EscapeText(message);
messageParams.Message = escapedText;
var discordMessage = GenerateAHelpMessage(messageParams);
queue.Enqueue(discordMessage);
}
}
private void OnGameRunLevelChanged(GameRunLevelChangedEvent args)
{
// Don't make a new embed if we
@@ -209,7 +341,8 @@ namespace Content.Server.Administration.Systems
var content = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
_sawmill.Log(LogLevel.Error, $"Discord returned bad status code when trying to get webhook data (perhaps the webhook URL is invalid?): {response.StatusCode}\nResponse: {content}");
_sawmill.Log(LogLevel.Error,
$"Discord returned bad status code when trying to get webhook data (perhaps the webhook URL is invalid?): {response.StatusCode}\nResponse: {content}");
return;
}
@@ -233,7 +366,7 @@ namespace Content.Server.Administration.Systems
// Whether the message will become too long after adding these new messages
var tooLong = exists && messages.Sum(msg => Math.Min(msg.Length, MessageLengthCap) + "\n".Length)
+ existingEmbed.description.Length > DescriptionMax;
+ existingEmbed.description.Length > DescriptionMax;
// If there is no existing embed, or it is getting too long, we create a new embed
if (!exists || tooLong)
@@ -242,7 +375,8 @@ namespace Content.Server.Administration.Systems
if (lookup == null)
{
_sawmill.Log(LogLevel.Error, $"Unable to find player for NetUserId {userId} when sending discord webhook.");
_sawmill.Log(LogLevel.Error,
$"Unable to find player for NetUserId {userId} when sending discord webhook.");
_relayMessages.Remove(userId);
return;
}
@@ -254,11 +388,13 @@ namespace Content.Server.Administration.Systems
{
if (tooLong && existingEmbed.id != null)
{
linkToPrevious = $"**[Go to previous embed of this round](https://discord.com/channels/{guildId}/{channelId}/{existingEmbed.id})**\n";
linkToPrevious =
$"**[Go to previous embed of this round](https://discord.com/channels/{guildId}/{channelId}/{existingEmbed.id})**\n";
}
else if (_oldMessageIds.TryGetValue(userId, out var id) && !string.IsNullOrEmpty(id))
{
linkToPrevious = $"**[Go to last round's conversation with this player](https://discord.com/channels/{guildId}/{channelId}/{id})**\n";
linkToPrevious =
$"**[Go to last round's conversation with this player](https://discord.com/channels/{guildId}/{channelId}/{id})**\n";
}
}
@@ -274,7 +410,8 @@ namespace Content.Server.Administration.Systems
GameRunLevel.PreRoundLobby => "\n\n:arrow_forward: _**Pre-round lobby started**_\n",
GameRunLevel.InRound => "\n\n:arrow_forward: _**Round started**_\n",
GameRunLevel.PostRound => "\n\n:stop_button: _**Post-round started**_\n",
_ => throw new ArgumentOutOfRangeException(nameof(_gameTicker.RunLevel), $"{_gameTicker.RunLevel} was not matched."),
_ => throw new ArgumentOutOfRangeException(nameof(_gameTicker.RunLevel),
$"{_gameTicker.RunLevel} was not matched."),
};
existingEmbed.lastRunLevel = _gameTicker.RunLevel;
@@ -290,7 +427,9 @@ namespace Content.Server.Administration.Systems
existingEmbed.description += $"\n{message}";
}
var payload = GeneratePayload(existingEmbed.description, existingEmbed.username, existingEmbed.characterName);
var payload = GeneratePayload(existingEmbed.description,
existingEmbed.username,
existingEmbed.characterName);
// If there is no existing embed, create a new one
// Otherwise patch (edit) it
@@ -302,7 +441,8 @@ namespace Content.Server.Administration.Systems
var content = await request.Content.ReadAsStringAsync();
if (!request.IsSuccessStatusCode)
{
_sawmill.Log(LogLevel.Error, $"Discord returned bad status code when posting message (perhaps the message is too long?): {request.StatusCode}\nResponse: {content}");
_sawmill.Log(LogLevel.Error,
$"Discord returned bad status code when posting message (perhaps the message is too long?): {request.StatusCode}\nResponse: {content}");
_relayMessages.Remove(userId);
return;
}
@@ -310,7 +450,8 @@ namespace Content.Server.Administration.Systems
var id = JsonNode.Parse(content)?["id"];
if (id == null)
{
_sawmill.Log(LogLevel.Error, $"Could not find id in json-content returned from discord webhook: {content}");
_sawmill.Log(LogLevel.Error,
$"Could not find id in json-content returned from discord webhook: {content}");
_relayMessages.Remove(userId);
return;
}
@@ -325,7 +466,8 @@ namespace Content.Server.Administration.Systems
if (!request.IsSuccessStatusCode)
{
var content = await request.Content.ReadAsStringAsync();
_sawmill.Log(LogLevel.Error, $"Discord returned bad status code when patching message (perhaps the message is too long?): {request.StatusCode}\nResponse: {content}");
_sawmill.Log(LogLevel.Error,
$"Discord returned bad status code when patching message (perhaps the message is too long?): {request.StatusCode}\nResponse: {content}");
_relayMessages.Remove(userId);
return;
}
@@ -355,7 +497,8 @@ namespace Content.Server.Administration.Systems
: $"pre-round lobby for round {_gameTicker.RoundId + 1}",
GameRunLevel.InRound => $"round {_gameTicker.RoundId}",
GameRunLevel.PostRound => $"post-round {_gameTicker.RoundId}",
_ => throw new ArgumentOutOfRangeException(nameof(_gameTicker.RunLevel), $"{_gameTicker.RunLevel} was not matched."),
_ => throw new ArgumentOutOfRangeException(nameof(_gameTicker.RunLevel),
$"{_gameTicker.RunLevel} was not matched."),
};
return new WebhookPayload
@@ -401,6 +544,7 @@ namespace Content.Server.Administration.Systems
protected override void OnBwoinkTextMessage(BwoinkTextMessage message, EntitySessionEventArgs eventArgs)
{
base.OnBwoinkTextMessage(message, eventArgs);
_activeConversations[message.UserId] = DateTime.Now;
var senderSession = eventArgs.SenderSession;
// TODO: Sanitize text?
@@ -421,14 +565,23 @@ namespace Content.Server.Administration.Systems
var escapedText = FormattedMessage.EscapeText(message.Text);
string bwoinkText;
string adminPrefix = "";
if (senderAdmin is not null && senderAdmin.Flags == AdminFlags.Adminhelp) // Mentor. Not full admin. That's why it's colored differently.
//Getting an administrator position
if (_config.GetCVar(CCVars.AhelpAdminPrefix) && senderAdmin is not null && senderAdmin.Title is not null)
{
bwoinkText = $"[color=purple]{senderSession.Name}[/color]";
adminPrefix = $"[bold]\\[{senderAdmin.Title}\\][/bold] ";
}
if (senderAdmin is not null &&
senderAdmin.Flags ==
AdminFlags.Adminhelp) // Mentor. Not full admin. That's why it's colored differently.
{
bwoinkText = $"[color=purple]{adminPrefix}{senderSession.Name}[/color]";
}
else if (senderAdmin is not null && senderAdmin.HasFlag(AdminFlags.Adminhelp))
{
bwoinkText = $"[color=red]{senderSession.Name}[/color]";
bwoinkText = $"[color=red]{adminPrefix}{senderSession.Name}[/color]";
}
else
{
@@ -451,6 +604,13 @@ namespace Content.Server.Administration.Systems
RaiseNetworkEvent(msg, channel);
}
string adminPrefixWebhook = "";
if (_config.GetCVar(CCVars.AhelpAdminPrefixWebhook) && senderAdmin is not null && senderAdmin.Title is not null)
{
adminPrefixWebhook = $"[bold]\\[{senderAdmin.Title}\\][/bold] ";
}
// Notify player
if (_playerManager.TryGetSessionById(message.UserId, out var session))
{
@@ -461,13 +621,15 @@ namespace Content.Server.Administration.Systems
{
string overrideMsgText;
// Doing the same thing as above, but with the override name. Theres probably a better way to do this.
if (senderAdmin is not null && senderAdmin.Flags == AdminFlags.Adminhelp) // Mentor. Not full admin. That's why it's colored differently.
if (senderAdmin is not null &&
senderAdmin.Flags ==
AdminFlags.Adminhelp) // Mentor. Not full admin. That's why it's colored differently.
{
overrideMsgText = $"[color=purple]{_overrideClientName}[/color]";
overrideMsgText = $"[color=purple]{adminPrefixWebhook}{_overrideClientName}[/color]";
}
else if (senderAdmin is not null && senderAdmin.HasFlag(AdminFlags.Adminhelp))
{
overrideMsgText = $"[color=red]{_overrideClientName}[/color]";
overrideMsgText = $"[color=red]{adminPrefixWebhook}{_overrideClientName}[/color]";
}
else
{
@@ -476,7 +638,11 @@ namespace Content.Server.Administration.Systems
overrideMsgText = $"{(message.PlaySound ? "" : "(S) ")}{overrideMsgText}: {escapedText}";
RaiseNetworkEvent(new BwoinkTextMessage(message.UserId, senderSession.UserId, overrideMsgText, playSound: playSound), session.Channel);
RaiseNetworkEvent(new BwoinkTextMessage(message.UserId,
senderSession.UserId,
overrideMsgText,
playSound: playSound),
session.Channel);
}
else
RaiseNetworkEvent(msg, session.Channel);
@@ -496,8 +662,18 @@ namespace Content.Server.Administration.Systems
{
str = str[..(DescriptionMax - _maxAdditionalChars - unameLength)];
}
var nonAfkAdmins = GetNonAfkAdmins();
_messageQueues[msg.UserId].Enqueue(GenerateAHelpMessage(senderSession.Name, str, !personalChannel, _gameTicker.RoundDuration().ToString("hh\\:mm\\:ss"), _gameTicker.RunLevel, playedSound: playSound, noReceivers: nonAfkAdmins.Count == 0));
var messageParams = new AHelpMessageParams(
senderSession.Name,
str,
!personalChannel,
_gameTicker.RoundDuration().ToString("hh\\:mm\\:ss"),
_gameTicker.RunLevel,
playedSound: playSound,
noReceivers: nonAfkAdmins.Count == 0
);
_messageQueues[msg.UserId].Enqueue(GenerateAHelpMessage(messageParams));
}
if (admins.Count != 0 || sendsWebhook)
@@ -512,7 +688,8 @@ namespace Content.Server.Administration.Systems
private IList<INetChannel> GetNonAfkAdmins()
{
return _adminManager.ActiveAdmins
.Where(p => (_adminManager.GetAdminData(p)?.HasFlag(AdminFlags.Adminhelp) ?? false) && !_afkManager.IsAfk(p))
.Where(p => (_adminManager.GetAdminData(p)?.HasFlag(AdminFlags.Adminhelp) ?? false) &&
!_afkManager.IsAfk(p))
.Select(p => p.Channel)
.ToList();
}
@@ -525,25 +702,69 @@ namespace Content.Server.Administration.Systems
.ToList();
}
private static string GenerateAHelpMessage(string username, string message, bool admin, string roundTime, GameRunLevel roundState, bool playedSound, bool noReceivers = false)
private static string GenerateAHelpMessage(AHelpMessageParams parameters)
{
var stringbuilder = new StringBuilder();
if (admin)
if (parameters.Icon != null)
stringbuilder.Append(parameters.Icon);
else if (parameters.IsAdmin)
stringbuilder.Append(":outbox_tray:");
else if (noReceivers)
else if (parameters.NoReceivers)
stringbuilder.Append(":sos:");
else
stringbuilder.Append(":inbox_tray:");
if(roundTime != string.Empty && roundState == GameRunLevel.InRound)
stringbuilder.Append($" **{roundTime}**");
if (!playedSound)
if (parameters.RoundTime != string.Empty && parameters.RoundState == GameRunLevel.InRound)
stringbuilder.Append($" **{parameters.RoundTime}**");
if (!parameters.PlayedSound)
stringbuilder.Append(" **(S)**");
stringbuilder.Append($" **{username}:** ");
stringbuilder.Append(message);
if (parameters.Icon == null)
stringbuilder.Append($" **{parameters.Username}:** ");
else
stringbuilder.Append($" **{parameters.Username}** ");
stringbuilder.Append(parameters.Message);
return stringbuilder.ToString();
}
}
}
public sealed class AHelpMessageParams
{
public string Username { get; set; }
public string Message { get; set; }
public bool IsAdmin { get; set; }
public string RoundTime { get; set; }
public GameRunLevel RoundState { get; set; }
public bool PlayedSound { get; set; }
public bool NoReceivers { get; set; }
public string? Icon { get; set; }
public AHelpMessageParams(
string username,
string message,
bool isAdmin,
string roundTime,
GameRunLevel roundState,
bool playedSound,
bool noReceivers = false,
string? icon = null)
{
Username = username;
Message = message;
IsAdmin = isAdmin;
RoundTime = roundTime;
RoundState = roundState;
PlayedSound = playedSound;
NoReceivers = noReceivers;
Icon = icon;
}
}
public enum PlayerStatusType
{
Connected,
Disconnected,
Banned,
}
}

View File

@@ -328,16 +328,13 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
if (session != null)
{
var curMind = session.GetMind();
if (curMind == null)
{
curMind = _mind.CreateMind(session.UserId, Name(antagEnt.Value));
_mind.SetUserId(curMind.Value, session.UserId);
}
var curMind = _mind.CreateMind(session.UserId, Name(antagEnt.Value));
_mind.SetUserId(curMind, session.UserId);
_mind.TransferTo(curMind, antagEnt, ghostCheckOverride: true);
_role.MindAddRoles(curMind, def.MindComponents, null, true);
ent.Comp.SelectedMinds.Add((curMind, Name(player)));
_mind.TransferTo(curMind.Value, antagEnt, ghostCheckOverride: true);
_role.MindAddRoles(curMind.Value, def.MindComponents, null, true);
ent.Comp.SelectedMinds.Add((curMind.Value, Name(player)));
SendBriefing(session, def.Briefing);
}

View File

@@ -1,7 +1,7 @@
namespace Content.Server.Antag.Components;
[RegisterComponent]
public partial class AntagImmuneComponent : Component
public sealed partial class AntagImmuneComponent : Component
{
}

View File

@@ -34,7 +34,7 @@ public sealed class RottingSystem : SharedRottingSystem
if (!TryComp<PerishableComponent>(uid, out var perishable))
return;
var molsToDump = perishable.MolsPerSecondPerUnitMass * physics.FixturesMass * (float) component.TotalRotTime.TotalSeconds;
var molsToDump = perishable.MolsPerSecondPerUnitMass * physics.FixturesMass * (float)component.TotalRotTime.TotalSeconds;
var tileMix = _atmosphere.GetTileMixture(uid, excite: true);
tileMix?.AdjustMoles(Gas.Ammonia, molsToDump);
}
@@ -54,7 +54,7 @@ public sealed class RottingSystem : SharedRottingSystem
/// <returns></returns>
private float GetRotRate(EntityUid uid)
{
if (_container.TryGetContainingContainer(uid, out var container) &&
if (_container.TryGetContainingContainer((uid, null, null), out var container) &&
TryComp<ProRottingContainerComponent>(container.Owner, out var rotContainer))
{
return rotContainer.DecayModifier;
@@ -124,7 +124,7 @@ public sealed class RottingSystem : SharedRottingSystem
continue;
// We need a way to get the mass of the mob alone without armor etc in the future
// or just remove the mass mechanics altogether because they aren't good.
var molRate = perishable.MolsPerSecondPerUnitMass * (float) rotting.RotUpdateRate.TotalSeconds;
var molRate = perishable.MolsPerSecondPerUnitMass * (float)rotting.RotUpdateRate.TotalSeconds;
var tileMix = _atmosphere.GetTileMixture(uid, excite: true);
tileMix?.AdjustMoles(Gas.Ammonia, molRate * physics.FixturesMass);
}

View File

@@ -34,7 +34,7 @@ public sealed class BodySystem : SharedBodySystem
private void OnRelayMoveInput(Entity<BodyComponent> ent, ref MoveInputEvent args)
{
// If they haven't actually moved then ignore it.
if ((args.Component.HeldMoveButtons &
if ((args.Entity.Comp.HeldMoveButtons &
(MoveButtons.Down | MoveButtons.Left | MoveButtons.Up | MoveButtons.Right)) == 0x0)
{
return;

View File

@@ -72,6 +72,9 @@ public sealed class InternalsSystem : EntitySystem
if (!args.CanAccess || !args.CanInteract || args.Hands is null)
return;
if (!AreInternalsWorking(ent) && ent.Comp.BreathTools.Count == 0)
return;
var user = args.User;
InteractionVerb verb = new()

View File

@@ -114,7 +114,7 @@ public sealed class RespiratorSystem : EntitySystem
if (!Resolve(uid, ref body, logMissing: false))
return;
var organs = _bodySystem.GetBodyOrganComponents<LungComponent>(uid, body);
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((uid, body));
// Inhale gas
var ev = new InhaleLocationEvent();
@@ -131,11 +131,11 @@ public sealed class RespiratorSystem : EntitySystem
var lungRatio = 1.0f / organs.Count;
var gas = organs.Count == 1 ? actualGas : actualGas.RemoveRatio(lungRatio);
foreach (var (lung, _) in organs)
foreach (var (organUid, lung, _) in organs)
{
// Merge doesn't remove gas from the giver.
_atmosSys.Merge(lung.Air, gas);
_lungSystem.GasToReagent(lung.Owner, lung);
_lungSystem.GasToReagent(organUid, lung);
}
}
@@ -144,7 +144,7 @@ public sealed class RespiratorSystem : EntitySystem
if (!Resolve(uid, ref body, logMissing: false))
return;
var organs = _bodySystem.GetBodyOrganComponents<LungComponent>(uid, body);
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((uid, body));
// exhale gas
@@ -161,12 +161,12 @@ public sealed class RespiratorSystem : EntitySystem
}
var outGas = new GasMixture(ev.Gas.Volume);
foreach (var (lung, _) in organs)
foreach (var (organUid, lung, _) in organs)
{
_atmosSys.Merge(outGas, lung.Air);
lung.Air.Clear();
if (_solutionContainerSystem.ResolveSolution(lung.Owner, lung.SolutionName, ref lung.Solution))
if (_solutionContainerSystem.ResolveSolution(organUid, lung.SolutionName, ref lung.Solution))
_solutionContainerSystem.RemoveAllSolution(lung.Solution.Value);
}
@@ -201,7 +201,7 @@ public sealed class RespiratorSystem : EntitySystem
if (!Resolve(ent, ref ent.Comp))
return false;
var organs = _bodySystem.GetBodyOrganComponents<LungComponent>(ent);
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
if (organs.Count == 0)
return false;
@@ -213,7 +213,7 @@ public sealed class RespiratorSystem : EntitySystem
float saturation = 0;
foreach (var organ in organs)
{
saturation += GetSaturation(solution, organ.Comp.Owner, out var toxic);
saturation += GetSaturation(solution, organ.Owner, out var toxic);
if (toxic)
return false;
}
@@ -287,10 +287,10 @@ public sealed class RespiratorSystem : EntitySystem
if (ent.Comp.SuffocationCycles >= ent.Comp.SuffocationCycleThreshold)
{
// TODO: This is not going work with multiple different lungs, if that ever becomes a possibility
var organs = _bodySystem.GetBodyOrganComponents<LungComponent>(ent);
foreach (var (comp, _) in organs)
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
foreach (var entity in organs)
{
_alertsSystem.ShowAlert(ent, comp.Alert);
_alertsSystem.ShowAlert(entity.Owner, entity.Comp1.Alert);
}
}
@@ -303,10 +303,10 @@ public sealed class RespiratorSystem : EntitySystem
_adminLogger.Add(LogType.Asphyxiation, $"{ToPrettyString(ent):entity} stopped suffocating");
// TODO: This is not going work with multiple different lungs, if that ever becomes a possibility
var organs = _bodySystem.GetBodyOrganComponents<LungComponent>(ent);
foreach (var (comp, _) in organs)
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
foreach (var entity in organs)
{
_alertsSystem.ClearAlert(ent, comp.Alert);
_alertsSystem.ClearAlert(entity.Owner, entity.Comp1.Alert);
}
_damageableSys.TryChangeDamage(ent, ent.Comp.DamageRecovery);

View File

@@ -3,13 +3,13 @@ using System.Linq;
using Content.Server.Cargo.Components;
using Content.Server.Labels;
using Content.Server.NameIdentifier;
using Content.Server.Paper;
using Content.Shared.Access.Components;
using Content.Shared.Cargo;
using Content.Shared.Cargo.Components;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.Database;
using Content.Shared.NameIdentifier;
using Content.Shared.Paper;
using Content.Shared.Stacks;
using Content.Shared.Whitelist;
using JetBrains.Annotations;
@@ -125,7 +125,7 @@ public sealed partial class CargoSystem
msg.PushNewline();
}
msg.AddMarkup(Loc.GetString("bounty-console-manifest-reward", ("reward", prototype.Reward)));
_paperSystem.SetContent(uid, msg.ToMarkup(), paper);
_paperSystem.SetContent((uid, paper), msg.ToMarkup());
}
/// <summary>
@@ -138,7 +138,7 @@ public sealed partial class CargoSystem
return;
// make sure this label was actually applied to a crate.
if (!_container.TryGetContainingContainer(uid, out var container) || container.ID != LabelSystem.ContainerName)
if (!_container.TryGetContainingContainer((uid, null, null), out var container) || container.ID != LabelSystem.ContainerName)
return;
if (component.AssociatedStationId is not { } station || !TryComp<StationCargoBountyDatabaseComponent>(station, out var database))

View File

@@ -1,7 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Cargo.Components;
using Content.Server.Labels.Components;
using Content.Server.Paper;
using Content.Server.Station.Components;
using Content.Shared.Cargo;
using Content.Shared.Cargo.BUI;
@@ -10,10 +9,9 @@ using Content.Shared.Cargo.Events;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.Database;
using Content.Shared.Interaction;
using Content.Shared.Paper;
using Robust.Shared.Map;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Cargo.Systems
@@ -177,6 +175,10 @@ namespace Content.Server.Cargo.Systems
RaiseLocalEvent(ref ev);
ev.FulfillmentEntity ??= station.Value;
_idCardSystem.TryFindIdCard(player, out var idCard);
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
order.SetApproverData(idCard.Comp?.FullName, idCard.Comp?.JobTitle);
if (!ev.Handled)
{
ev.FulfillmentEntity = TryFulfillOrder((station.Value, stationData), order, orderDatabase);
@@ -189,18 +191,13 @@ namespace Content.Server.Cargo.Systems
}
}
_idCardSystem.TryFindIdCard(player, out var idCard);
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
order.SetApproverData(idCard.Comp?.FullName, idCard.Comp?.JobTitle);
order.Approved = true;
_audio.PlayPvs(component.ConfirmSound, uid);
var approverName = idCard.Comp?.FullName ?? Loc.GetString("access-reader-unknown-id");
var approverJob = idCard.Comp?.JobTitle ?? Loc.GetString("access-reader-unknown-id");
var message = Loc.GetString("cargo-console-unlock-approved-order-broadcast",
("productName", Loc.GetString(order.ProductName)),
("orderAmount", order.OrderQuantity),
("approverName", approverName),
("approverJob", approverJob),
("approver", order.Approver ?? string.Empty),
("cost", cost));
_radio.SendRadioMessage(uid, message, component.AnnouncementChannel, uid, escapeMarkup: false);
ConsolePopup(args.Actor, Loc.GetString("cargo-console-trade-station", ("destination", MetaData(ev.FulfillmentEntity.Value).EntityName)));
@@ -421,6 +418,7 @@ namespace Content.Server.Cargo.Systems
// Approve it now
order.SetApproverData(dest, sender);
order.Approved = true;
// Log order addition
_adminLogger.Add(LogType.Action, LogImpact.Low,
@@ -512,15 +510,14 @@ namespace Content.Server.Cargo.Systems
var val = Loc.GetString("cargo-console-paper-print-name", ("orderNumber", order.OrderId));
_metaSystem.SetEntityName(printed, val);
_paperSystem.SetContent(printed, Loc.GetString(
_paperSystem.SetContent((printed, paper), Loc.GetString(
"cargo-console-paper-print-text",
("orderNumber", order.OrderId),
("itemName", MetaData(item).EntityName),
("orderQuantity", order.OrderQuantity),
("requester", order.Requester),
("reason", order.Reason),
("approver", order.Approver ?? string.Empty)),
paper);
("approver", order.Approver ?? string.Empty)));
// attempt to attach the label to the item
if (TryComp<PaperLabelComponent>(item, out var label))

View File

@@ -1,7 +1,6 @@
using Content.Server.Access.Systems;
using Content.Server.Cargo.Components;
using Content.Server.DeviceLinking.Systems;
using Content.Server.Paper;
using Content.Server.Popups;
using Content.Server.Shuttles.Systems;
using Content.Server.Stack;
@@ -13,6 +12,7 @@ using Content.Shared.Cargo;
using Content.Shared.Cargo.Components;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Mobs.Components;
using Content.Shared.Paper;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;

View File

@@ -16,6 +16,7 @@ using Robust.Shared.Map.Components;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using System.Linq;
using Content.Shared.Research.Prototypes;
namespace Content.Server.Cargo.Systems;
@@ -158,6 +159,26 @@ public sealed class PricingSystem : EntitySystem
return price;
}
public double GetLatheRecipePrice(LatheRecipePrototype recipe)
{
var price = 0.0;
if (recipe.Result is { } result)
{
price += GetEstimatedPrice(_prototypeManager.Index(result));
}
if (recipe.ResultReagents is { } resultReagents)
{
foreach (var (reagent, amount) in resultReagents)
{
price += (_prototypeManager.Index(reagent).PricePerUnit * amount).Double();
}
}
return price;
}
/// <summary>
/// Get a rough price for an entityprototype. Does not consider contained entities.
/// </summary>

View File

@@ -0,0 +1,18 @@
using Content.Shared.Dataset;
using Robust.Shared.Prototypes;
namespace Content.Server.Chat;
/// <summary>
/// Entity will say the things when activated
/// </summary>
[RegisterComponent]
public sealed partial class SpeakOnUseComponent : Component
{
/// <summary>
/// The identifier for the dataset prototype containing messages to be spoken by this entity.
/// </summary>
[DataField(required: true)]
public ProtoId<LocalizedDatasetPrototype> Pack { get; private set; }
}

View File

@@ -0,0 +1,42 @@
using Content.Server.Chat;
using Content.Shared.Dataset;
using Content.Shared.Interaction.Events;
using Content.Shared.Timing;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Chat.Systems;
/// <summary>
/// Handles the speech on activating an entity
/// </summary>
public sealed partial class SpeakOnUIClosedSystem : EntitySystem
{
[Dependency] private readonly UseDelaySystem _useDelay = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ChatSystem _chat = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SpeakOnUseComponent, UseInHandEvent>(OnUseInHand);
}
public void OnUseInHand(EntityUid uid, SpeakOnUseComponent? component, UseInHandEvent args)
{
if (!Resolve(uid, ref component))
return;
// Yes it won't work without UseDelayComponent, but we don't want any kind of spam
if (!TryComp(uid, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((uid, useDelay)))
return;
if (!_prototypeManager.TryIndex(component.Pack, out var messagePack))
return;
var message = Loc.GetString(_random.Pick(messagePack.Values));
_chat.TrySendInGameICMessage(uid, message, InGameICChatType.Speak, true);
_useDelay.TryResetDelay((uid, useDelay));
}
}

View File

@@ -2,7 +2,6 @@ using System.Linq;
using Content.Server.Administration.Logs;
using Content.Server.Construction.Components;
using Content.Server.Temperature.Components;
using Content.Server.Temperature.Systems;
using Content.Shared.Construction;
using Content.Shared.Construction.Components;
using Content.Shared.Construction.EntitySystems;
@@ -11,6 +10,7 @@ using Content.Shared.DoAfter;
using Content.Shared.Interaction;
using Content.Shared.Prying.Systems;
using Content.Shared.Radio.EntitySystems;
using Content.Shared.Temperature;
using Content.Shared.Tools.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Utility;

View File

@@ -30,8 +30,6 @@ public sealed class ThrowInsertContainerSystem : EntitySystem
if (!_containerSystem.CanInsert(args.Thrown, container))
return;
var rand = _random.NextFloat();
if (_random.Prob(ent.Comp.Probability))
{
_audio.PlayPvs(ent.Comp.MissSound, ent);

View File

@@ -0,0 +1,89 @@
using System.Numerics;
using Content.Server.Spawners.Components;
using Content.Server.Spawners.EntitySystems;
using Content.Shared.Random;
using Content.Shared.Random.Helpers;
using Robust.Server.GameObjects;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Spawners;
namespace Content.Server.Destructible.Thresholds.Behaviors;
/// <summary>
/// Behavior that can be assigned to a trigger that that takes a <see cref="WeightedRandomEntityPrototype"/>
/// and spawns a number of the same entity between a given min and max
/// at a random offset from the final position of the entity.
/// </summary>
[Serializable]
[DataDefinition]
public sealed partial class WeightedSpawnEntityBehavior : IThresholdBehavior
{
/// <summary>
/// A table of entities with assigned weights to randomly pick from
/// </summary>
[DataField(required: true)]
public ProtoId<WeightedRandomEntityPrototype> WeightedEntityTable;
/// <summary>
/// How far away to spawn the entity from the parent position
/// </summary>
[DataField]
public float SpawnOffset = 1;
/// <summary>
/// The mininum number of entities to spawn randomly
/// </summary>
[DataField]
public int MinSpawn = 1;
/// <summary>
/// The max number of entities to spawn randomly
/// </summary>
[DataField]
public int MaxSpawn = 1;
/// <summary>
/// Time in seconds to wait before spawning entities
/// </summary>
[DataField]
public float SpawnAfter;
public void Execute(EntityUid uid, DestructibleSystem system, EntityUid? cause = null)
{
// Get the position at which to start initially spawning entities
var transform = system.EntityManager.System<TransformSystem>();
var position = transform.GetMapCoordinates(uid);
// Helper function used to randomly get an offset to apply to the original position
Vector2 GetRandomVector() => new (system.Random.NextFloat(-SpawnOffset, SpawnOffset), system.Random.NextFloat(-SpawnOffset, SpawnOffset));
// Randomly pick the entity to spawn and randomly pick how many to spawn
var entity = system.PrototypeManager.Index(WeightedEntityTable).Pick(system.Random);
var amountToSpawn = system.Random.NextFloat(MinSpawn, MaxSpawn);
// Different behaviors for delayed spawning and immediate spawning
if (SpawnAfter != 0)
{
// if it fails to get the spawner, this won't ever work so just return
if (!system.PrototypeManager.TryIndex("TemporaryEntityForTimedDespawnSpawners", out var tempSpawnerProto))
return;
// spawn the spawner, assign it a lifetime, and assign the entity that it will spawn when despawned
for (var i = 0; i < amountToSpawn; i++)
{
var spawner = system.EntityManager.SpawnEntity(tempSpawnerProto.ID, position.Offset(GetRandomVector()));
system.EntityManager.EnsureComponent<TimedDespawnComponent>(spawner, out var timedDespawnComponent);
timedDespawnComponent.Lifetime = SpawnAfter;
system.EntityManager.EnsureComponent<SpawnOnDespawnComponent>(spawner, out var spawnOnDespawnComponent);
system.EntityManager.System<SpawnOnDespawnSystem>().SetPrototype((spawner, spawnOnDespawnComponent), entity);
}
}
else
{
// directly spawn the desired entities
for (var i = 0; i < amountToSpawn; i++)
{
system.EntityManager.SpawnEntity(entity, position.Offset(GetRandomVector()));
}
}
}
}

View File

@@ -25,7 +25,6 @@ using Content.Shared.Interaction;
using Content.Shared.Item;
using Content.Shared.Movement.Events;
using Content.Shared.Popups;
using Content.Shared.Throwing;
using Content.Shared.Verbs;
using Robust.Server.Audio;
using Robust.Server.GameObjects;
@@ -35,7 +34,6 @@ using Robust.Shared.Map.Components;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Events;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Disposal.Unit.EntitySystems;
@@ -331,12 +329,13 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
{
var currentTime = GameTiming.CurTime;
if (!_actionBlockerSystem.CanMove(args.Entity))
return;
if (!TryComp(args.Entity, out HandsComponent? hands) ||
hands.Count == 0 ||
currentTime < component.LastExitAttempt + ExitAttemptDelay)
{
return;
}
component.LastExitAttempt = currentTime;
Remove(uid, component, args.Entity);

View File

@@ -0,0 +1,50 @@
using Content.Shared.Bed.Sleep;
using Content.Shared.Drowsiness;
using Content.Shared.StatusEffect;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Drowsiness;
public sealed class DrowsinessSystem : SharedDrowsinessSystem
{
[ValidatePrototypeId<StatusEffectPrototype>]
private const string SleepKey = "ForcedSleep"; // Same one used by N2O and other sleep chems.
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<DrowsinessComponent, ComponentStartup>(OnInit);
}
private void OnInit(EntityUid uid, DrowsinessComponent component, ComponentStartup args)
{
component.NextIncidentTime = _timing.CurTime + TimeSpan.FromSeconds(_random.NextFloat(component.TimeBetweenIncidents.X, component.TimeBetweenIncidents.Y));
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<DrowsinessComponent>();
while (query.MoveNext(out var uid, out var component))
{
if (_timing.CurTime < component.NextIncidentTime)
continue;
// Set the new time.
component.NextIncidentTime = _timing.CurTime + TimeSpan.FromSeconds(_random.NextFloat(component.TimeBetweenIncidents.X, component.TimeBetweenIncidents.Y));
// sleep duration
var duration = TimeSpan.FromSeconds(_random.NextFloat(component.DurationOfIncident.X, component.DurationOfIncident.Y));
// Make sure the sleep time doesn't cut into the time to next incident.
component.NextIncidentTime += duration;
_statusEffects.TryAddStatusEffect<ForcedSleepingComponent>(uid, SleepKey, duration, false);
}
}
}

Some files were not shown because too many files have changed in this diff Show More