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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,11 @@ namespace Content.Client.Administration.UI.Bwoink
|
||||
}
|
||||
};
|
||||
|
||||
OnOpen += () => Bwoink.PopulateList();
|
||||
OnOpen += () =>
|
||||
{
|
||||
Bwoink.ChannelSelector.StopFiltering();
|
||||
Bwoink.PopulateList();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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?
|
||||
|
||||
80
Content.Client/Drowsiness/DrowsinessOverlay.cs
Normal file
80
Content.Client/Drowsiness/DrowsinessOverlay.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
53
Content.Client/Drowsiness/DrowsinessSystem.cs
Normal file
53
Content.Client/Drowsiness/DrowsinessSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
8
Content.Client/Explosion/ExplosionSystem.cs
Normal file
8
Content.Client/Explosion/ExplosionSystem.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using Content.Shared.Explosion.EntitySystems;
|
||||
|
||||
namespace Content.Client.Explosion.EntitySystems;
|
||||
|
||||
public sealed class ExplosionSystem : SharedExplosionSystem
|
||||
{
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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))
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
8
Content.Client/Mapping/MappingActionsButton.xaml
Normal file
8
Content.Client/Mapping/MappingActionsButton.xaml
Normal 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>
|
||||
15
Content.Client/Mapping/MappingActionsButton.xaml.cs
Normal file
15
Content.Client/Mapping/MappingActionsButton.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
4
Content.Client/Mapping/MappingDoNotMeasure.xaml
Normal file
4
Content.Client/Mapping/MappingDoNotMeasure.xaml
Normal file
@@ -0,0 +1,4 @@
|
||||
<mapping:MappingDoNotMeasure
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:mapping="clr-namespace:Content.Client.Mapping">
|
||||
</mapping:MappingDoNotMeasure>
|
||||
21
Content.Client/Mapping/MappingDoNotMeasure.xaml.cs
Normal file
21
Content.Client/Mapping/MappingDoNotMeasure.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
69
Content.Client/Mapping/MappingManager.cs
Normal file
69
Content.Client/Mapping/MappingManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
84
Content.Client/Mapping/MappingOverlay.cs
Normal file
84
Content.Client/Mapping/MappingOverlay.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
39
Content.Client/Mapping/MappingPrototype.cs
Normal file
39
Content.Client/Mapping/MappingPrototype.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
21
Content.Client/Mapping/MappingPrototypeList.xaml
Normal file
21
Content.Client/Mapping/MappingPrototypeList.xaml
Normal 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>
|
||||
170
Content.Client/Mapping/MappingPrototypeList.xaml.cs
Normal file
170
Content.Client/Mapping/MappingPrototypeList.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
85
Content.Client/Mapping/MappingScreen.xaml
Normal file
85
Content.Client/Mapping/MappingScreen.xaml
Normal 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>
|
||||
197
Content.Client/Mapping/MappingScreen.xaml.cs
Normal file
197
Content.Client/Mapping/MappingScreen.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
26
Content.Client/Mapping/MappingSpawnButton.xaml
Normal file
26
Content.Client/Mapping/MappingSpawnButton.xaml
Normal 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>
|
||||
16
Content.Client/Mapping/MappingSpawnButton.xaml.cs
Normal file
16
Content.Client/Mapping/MappingSpawnButton.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
936
Content.Client/Mapping/MappingState.cs
Normal file
936
Content.Client/Mapping/MappingState.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
35
Content.Client/Paper/EnvelopeSystem.cs
Normal file
35
Content.Client/Paper/EnvelopeSystem.cs
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
using Content.Shared.Paper;
|
||||
|
||||
namespace Content.Client.Paper;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class PaperComponent : SharedPaperComponent;
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Client.Paper;
|
||||
namespace Content.Client.Paper.UI;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class PaperVisualsComponent : Component
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
218
Content.Client/Sprite/ContentSpriteSystem.cs
Normal file
218
Content.Client/Sprite/ContentSpriteSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
41
Content.IntegrationTests/Tests/MappingEditorTest.cs
Normal file
41
Content.IntegrationTests/Tests/MappingEditorTest.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -18,10 +18,6 @@ public sealed class LoadoutTests
|
||||
id: PlayTimeLoadoutTester
|
||||
|
||||
- type: loadout
|
||||
id: TestJumpsuit
|
||||
equipment: TestJumpsuit
|
||||
|
||||
- type: startingGear
|
||||
id: TestJumpsuit
|
||||
equipment:
|
||||
jumpsuit: ClothingUniformJumpsuitColorGrey
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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) &&
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace Content.Server.Antag.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public partial class AntagImmuneComponent : Component
|
||||
public sealed partial class AntagImmuneComponent : Component
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
18
Content.Server/Chat/SpeakOnUseComponent.cs
Normal file
18
Content.Server/Chat/SpeakOnUseComponent.cs
Normal 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; }
|
||||
|
||||
}
|
||||
42
Content.Server/Chat/Systems/SpeakOnUseSystem.cs
Normal file
42
Content.Server/Chat/Systems/SpeakOnUseSystem.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
50
Content.Server/Drowsiness/DrowsinessSystem.cs
Normal file
50
Content.Server/Drowsiness/DrowsinessSystem.cs
Normal 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
Reference in New Issue
Block a user