mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-02-15 00:54:51 +01:00
Merge remote-tracking branch 'refs/remotes/upstream/master' into upstream-sync
# Conflicts: # Content.Server/Chat/Systems/ChatSystem.cs # Resources/Prototypes/AlertLevels/alert_levels.yml # Resources/Prototypes/Catalog/Fills/Lockers/heads.yml # Resources/Prototypes/Datasets/Names/fortunes.yml # Resources/Prototypes/Datasets/ion_storm.yml # Resources/Prototypes/Entities/Clothing/Neck/mantles.yml # Resources/Prototypes/Entities/Clothing/Uniforms/jumpsuits.yml # Resources/Prototypes/Entities/Markers/Spawners/Random/maintenance.yml # Resources/Prototypes/Entities/Mobs/Cyborgs/base_borg_chassis.yml # Resources/Prototypes/Entities/Objects/Consumable/Food/produce.yml # Resources/Prototypes/Entities/Structures/Machines/lathe.yml # Resources/Prototypes/Recipes/Lathes/clothing.yml # Resources/Prototypes/StatusIcon/faction.yml # Resources/ServerInfo/Guidebook/Engineering/Singularity.xml # Resources/Textures/Clothing/Head/Helmets/security.rsi/equipped-HELMET.png # Resources/Textures/Clothing/Head/Helmets/security.rsi/icon.png # Resources/Textures/Clothing/Head/Helmets/security.rsi/inhand-left.png # Resources/Textures/Clothing/Head/Helmets/security.rsi/inhand-right.png # Resources/Textures/Clothing/Head/Helmets/security.rsi/meta.json # Resources/Textures/Clothing/Mask/joy.rsi/meta.json # Resources/Textures/Clothing/Mask/squadron.rsi/meta.json # Resources/Textures/Clothing/Neck/Cloaks/cmo.rsi/equipped-NECK.png # Resources/Textures/Clothing/Neck/Cloaks/cmo.rsi/meta.json # Resources/Textures/Clothing/OuterClothing/Armor/security.rsi/equipped-OUTERCLOTHING.png # Resources/Textures/Clothing/OuterClothing/Armor/security.rsi/icon.png # Resources/Textures/Clothing/OuterClothing/Armor/security.rsi/meta.json # Resources/Textures/Clothing/Uniforms/Jumpsuit/hos_alt.rsi/equipped-INNERCLOTHING.png # Resources/Textures/Objects/Specific/Hydroponics/aloe.rsi/produce.png # Resources/Textures/Objects/Specific/Hydroponics/apple.rsi/produce.png # Resources/Textures/Objects/Specific/Hydroponics/cannabis.rsi/produce.png # Resources/Textures/Objects/Tools/crowbar.rsi/meta.json # Resources/Textures/Objects/Tools/crowbar.rsi/red-icon.png # Resources/Textures/Objects/Tools/crowbar.rsi/red-storage.png # Resources/Textures/Structures/Wallmounts/Lighting/light_tube.rsi/base.png # Resources/Textures/Structures/Wallmounts/Lighting/light_tube.rsi/broken.png # Resources/Textures/Structures/Wallmounts/Lighting/light_tube.rsi/burned.png # Resources/Textures/Structures/Wallmounts/Lighting/light_tube.rsi/empty.png # Resources/Textures/Structures/Wallmounts/Lighting/light_tube.rsi/glow.png # Resources/Textures/Structures/Wallmounts/Lighting/light_tube.rsi/meta.json
This commit is contained in:
@@ -38,7 +38,7 @@ namespace Content.Client.Access.UI
|
||||
SendMessage(new AgentIDCardJobChangedMessage(newJob));
|
||||
}
|
||||
|
||||
public void OnJobIconChanged(ProtoId<StatusIconPrototype> newJobIconId)
|
||||
public void OnJobIconChanged(ProtoId<JobIconPrototype> newJobIconId)
|
||||
{
|
||||
SendMessage(new AgentIDCardJobIconChangedMessage(newJobIconId));
|
||||
}
|
||||
@@ -55,7 +55,7 @@ namespace Content.Client.Access.UI
|
||||
|
||||
_window.SetCurrentName(cast.CurrentName);
|
||||
_window.SetCurrentJob(cast.CurrentJob);
|
||||
_window.SetAllowedIcons(cast.Icons, cast.CurrentJobIconId);
|
||||
_window.SetAllowedIcons(cast.CurrentJobIconId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,9 @@
|
||||
<LineEdit Name="NameLineEdit" />
|
||||
<Label Name="CurrentJob" Text="{Loc 'agent-id-card-current-job'}" />
|
||||
<LineEdit Name="JobLineEdit" />
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'agent-id-card-job-icon-label'}"/>
|
||||
<Control HorizontalExpand="True" MinSize="50 0"/>
|
||||
<GridContainer Name="IconGrid" Columns="10">
|
||||
<!-- Job icon buttons are generated in the code -->
|
||||
</GridContainer>
|
||||
</BoxContainer>
|
||||
<Label Text="{Loc 'agent-id-card-job-icon-label'}"/>
|
||||
<GridContainer Name="IconGrid" Columns="10">
|
||||
<!-- Job icon buttons are generated in the code -->
|
||||
</GridContainer>
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
||||
@@ -8,6 +8,7 @@ using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Numerics;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Client.Access.UI
|
||||
{
|
||||
@@ -23,7 +24,7 @@ namespace Content.Client.Access.UI
|
||||
public event Action<string>? OnNameChanged;
|
||||
public event Action<string>? OnJobChanged;
|
||||
|
||||
public event Action<ProtoId<StatusIconPrototype>>? OnJobIconChanged;
|
||||
public event Action<ProtoId<JobIconPrototype>>? OnJobIconChanged;
|
||||
|
||||
public AgentIDCardWindow()
|
||||
{
|
||||
@@ -38,17 +39,16 @@ namespace Content.Client.Access.UI
|
||||
JobLineEdit.OnFocusExit += e => OnJobChanged?.Invoke(e.Text);
|
||||
}
|
||||
|
||||
public void SetAllowedIcons(HashSet<ProtoId<StatusIconPrototype>> icons, string currentJobIconId)
|
||||
public void SetAllowedIcons(string currentJobIconId)
|
||||
{
|
||||
IconGrid.DisposeAllChildren();
|
||||
|
||||
var jobIconGroup = new ButtonGroup();
|
||||
var jobIconButtonGroup = new ButtonGroup();
|
||||
var i = 0;
|
||||
foreach (var jobIconId in icons)
|
||||
var icons = _prototypeManager.EnumeratePrototypes<JobIconPrototype>().Where(icon => icon.AllowSelection).ToList();
|
||||
icons.Sort((x, y) => string.Compare(x.LocalizedJobName, y.LocalizedJobName, StringComparison.CurrentCulture));
|
||||
foreach (var jobIcon in icons)
|
||||
{
|
||||
if (!_prototypeManager.TryIndex(jobIconId, out var jobIcon))
|
||||
continue;
|
||||
|
||||
String styleBase = StyleBase.ButtonOpenBoth;
|
||||
var modulo = i % JobIconColumnCount;
|
||||
if (modulo == 0)
|
||||
@@ -62,8 +62,9 @@ namespace Content.Client.Access.UI
|
||||
Access = AccessLevel.Public,
|
||||
StyleClasses = { styleBase },
|
||||
MaxSize = new Vector2(42, 28),
|
||||
Group = jobIconGroup,
|
||||
Pressed = i == 0,
|
||||
Group = jobIconButtonGroup,
|
||||
Pressed = currentJobIconId == jobIcon.ID,
|
||||
ToolTip = jobIcon.LocalizedJobName
|
||||
};
|
||||
|
||||
// Generate buttons textures
|
||||
@@ -78,9 +79,6 @@ namespace Content.Client.Access.UI
|
||||
jobIconButton.OnPressed += _ => OnJobIconChanged?.Invoke(jobIcon.ID);
|
||||
IconGrid.AddChild(jobIconButton);
|
||||
|
||||
if (jobIconId.Equals(currentJobIconId))
|
||||
jobIconButton.Pressed = true;
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ namespace Content.Client.Actions
|
||||
SubscribeLocalEvent<InstantActionComponent, ComponentHandleState>(OnInstantHandleState);
|
||||
SubscribeLocalEvent<EntityTargetActionComponent, ComponentHandleState>(OnEntityTargetHandleState);
|
||||
SubscribeLocalEvent<WorldTargetActionComponent, ComponentHandleState>(OnWorldTargetHandleState);
|
||||
SubscribeLocalEvent<EntityWorldTargetActionComponent, ComponentHandleState>(OnEntityWorldTargetHandleState);
|
||||
}
|
||||
|
||||
private void OnInstantHandleState(EntityUid uid, InstantActionComponent component, ref ComponentHandleState args)
|
||||
@@ -76,6 +77,18 @@ namespace Content.Client.Actions
|
||||
BaseHandleState<WorldTargetActionComponent>(uid, component, state);
|
||||
}
|
||||
|
||||
private void OnEntityWorldTargetHandleState(EntityUid uid,
|
||||
EntityWorldTargetActionComponent component,
|
||||
ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is not EntityWorldTargetActionComponentState state)
|
||||
return;
|
||||
|
||||
component.Whitelist = state.Whitelist;
|
||||
component.CanTargetSelf = state.CanTargetSelf;
|
||||
BaseHandleState<EntityWorldTargetActionComponent>(uid, component, state);
|
||||
}
|
||||
|
||||
private void BaseHandleState<T>(EntityUid uid, BaseActionComponent component, BaseActionComponentState state) where T : BaseActionComponent
|
||||
{
|
||||
// TODO ACTIONS use auto comp states
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<ui:FancyWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="{Loc ban-panel-title}" MinSize="300 300">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Name="PlayerName"/>
|
||||
<Button Name="UsernameCopyButton" Text="{Loc player-panel-copy-username}"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Name="Whitelisted"/>
|
||||
<controls:ConfirmButton Name="WhitelistToggle" Text="{Loc 'player-panel-false'}" Visible="False"></controls:ConfirmButton>
|
||||
</BoxContainer>
|
||||
<Label Name="Playtime"/>
|
||||
<Label Name="Notes"/>
|
||||
<Label Name="Bans"/>
|
||||
<Label Name="RoleBans"/>
|
||||
<Label Name="SharedConnections"/>
|
||||
|
||||
<BoxContainer Align="Center">
|
||||
<GridContainer Rows="5">
|
||||
<Button Name="NotesButton" Text="{Loc player-panel-show-notes}" SetWidth="136" Disabled="True"/>
|
||||
<Button Name="AhelpButton" Text="{Loc player-panel-help}" Disabled="True"/>
|
||||
<Button Name="FreezeButton" Text = "{Loc player-panel-freeze}" Disabled="True"/>
|
||||
<controls:ConfirmButton Name="KickButton" Text="{Loc player-panel-kick}" Disabled="True"/>
|
||||
<controls:ConfirmButton Name="DeleteButton" Text="{Loc player-panel-delete}" Disabled="True"/>
|
||||
<Button Name="ShowBansButton" Text="{Loc player-panel-show-bans}" SetWidth="136" Disabled="True"/>
|
||||
<Button Name="LogsButton" Text="{Loc player-panel-logs}" Disabled="True"/>
|
||||
<Button Name="FreezeAndMuteToggleButton" Text="{Loc player-panel-freeze-and-mute}" Disabled="True"/>
|
||||
<Button Name="BanButton" Text="{Loc player-panel-ban}" Disabled="True"/>
|
||||
<controls:ConfirmButton Name="RejuvenateButton" Text="{Loc player-panel-rejuvenate}" Disabled="True"/>
|
||||
</GridContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</ui:FancyWindow>
|
||||
132
Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs
Normal file
132
Content.Client/Administration/UI/PlayerPanel/PlayerPanel.xaml.cs
Normal file
@@ -0,0 +1,132 @@
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Administration.UI.PlayerPanel;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class PlayerPanel : FancyWindow
|
||||
{
|
||||
private readonly IClientAdminManager _adminManager;
|
||||
|
||||
public event Action<string>? OnUsernameCopy;
|
||||
public event Action<NetUserId?>? OnOpenNotes;
|
||||
public event Action<NetUserId?>? OnOpenBans;
|
||||
public event Action<NetUserId?>? OnAhelp;
|
||||
public event Action<string?>? OnKick;
|
||||
public event Action<NetUserId?>? OnOpenBanPanel;
|
||||
public event Action<NetUserId?, bool>? OnWhitelistToggle;
|
||||
public event Action? OnFreezeAndMuteToggle;
|
||||
public event Action? OnFreeze;
|
||||
public event Action? OnLogs;
|
||||
public event Action? OnDelete;
|
||||
public event Action? OnRejuvenate;
|
||||
|
||||
public NetUserId? TargetPlayer;
|
||||
public string? TargetUsername;
|
||||
private bool _isWhitelisted;
|
||||
|
||||
public PlayerPanel(IClientAdminManager adminManager)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
_adminManager = adminManager;
|
||||
|
||||
UsernameCopyButton.OnPressed += _ => OnUsernameCopy?.Invoke(PlayerName.Text ?? "");
|
||||
BanButton.OnPressed += _ => OnOpenBanPanel?.Invoke(TargetPlayer);
|
||||
KickButton.OnPressed += _ => OnKick?.Invoke(TargetUsername);
|
||||
NotesButton.OnPressed += _ => OnOpenNotes?.Invoke(TargetPlayer);
|
||||
ShowBansButton.OnPressed += _ => OnOpenBans?.Invoke(TargetPlayer);
|
||||
AhelpButton.OnPressed += _ => OnAhelp?.Invoke(TargetPlayer);
|
||||
WhitelistToggle.OnPressed += _ =>
|
||||
{
|
||||
OnWhitelistToggle?.Invoke(TargetPlayer, _isWhitelisted);
|
||||
SetWhitelisted(!_isWhitelisted);
|
||||
};
|
||||
FreezeButton.OnPressed += _ => OnFreeze?.Invoke();
|
||||
FreezeAndMuteToggleButton.OnPressed += _ => OnFreezeAndMuteToggle?.Invoke();
|
||||
LogsButton.OnPressed += _ => OnLogs?.Invoke();
|
||||
DeleteButton.OnPressed += _ => OnDelete?.Invoke();
|
||||
RejuvenateButton.OnPressed += _ => OnRejuvenate?.Invoke();
|
||||
}
|
||||
|
||||
public void SetUsername(string player)
|
||||
{
|
||||
Title = Loc.GetString("player-panel-title", ("player", player));
|
||||
PlayerName.Text = Loc.GetString("player-panel-username", ("player", player));
|
||||
}
|
||||
|
||||
public void SetWhitelisted(bool? whitelisted)
|
||||
{
|
||||
if (whitelisted == null)
|
||||
{
|
||||
Whitelisted.Text = null;
|
||||
WhitelistToggle.Visible = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
Whitelisted.Text = Loc.GetString("player-panel-whitelisted");
|
||||
WhitelistToggle.Text = whitelisted.Value.ToString();
|
||||
WhitelistToggle.Visible = true;
|
||||
_isWhitelisted = whitelisted.Value;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetBans(int? totalBans, int? totalRoleBans)
|
||||
{
|
||||
// If one value exists then so should the other.
|
||||
DebugTools.Assert(totalBans.HasValue && totalRoleBans.HasValue || totalBans == null && totalRoleBans == null);
|
||||
|
||||
Bans.Text = totalBans != null ? Loc.GetString("player-panel-bans", ("totalBans", totalBans)) : null;
|
||||
|
||||
RoleBans.Text = totalRoleBans != null ? Loc.GetString("player-panel-rolebans", ("totalRoleBans", totalRoleBans)) : null;
|
||||
}
|
||||
|
||||
public void SetNotes(int? totalNotes)
|
||||
{
|
||||
Notes.Text = totalNotes != null ? Loc.GetString("player-panel-notes", ("totalNotes", totalNotes)) : null;
|
||||
}
|
||||
|
||||
public void SetSharedConnections(int sharedConnections)
|
||||
{
|
||||
SharedConnections.Text = Loc.GetString("player-panel-shared-connections", ("sharedConnections", sharedConnections));
|
||||
}
|
||||
|
||||
public void SetPlaytime(TimeSpan playtime)
|
||||
{
|
||||
Playtime.Text = Loc.GetString("player-panel-playtime",
|
||||
("days", playtime.Days),
|
||||
("hours", playtime.Hours % 24),
|
||||
("minutes", playtime.Minutes % (24 * 60)));
|
||||
}
|
||||
|
||||
public void SetFrozen(bool canFreeze, bool frozen)
|
||||
{
|
||||
FreezeAndMuteToggleButton.Disabled = !canFreeze;
|
||||
FreezeButton.Disabled = !canFreeze || frozen;
|
||||
|
||||
FreezeAndMuteToggleButton.Text = Loc.GetString(!frozen ? "player-panel-freeze-and-mute" : "player-panel-unfreeze");
|
||||
}
|
||||
|
||||
public void SetAhelp(bool canAhelp)
|
||||
{
|
||||
AhelpButton.Disabled = !canAhelp;
|
||||
}
|
||||
|
||||
public void SetButtons()
|
||||
{
|
||||
BanButton.Disabled = !_adminManager.CanCommand("banpanel");
|
||||
KickButton.Disabled = !_adminManager.CanCommand("kick");
|
||||
NotesButton.Disabled = !_adminManager.CanCommand("adminnotes");
|
||||
ShowBansButton.Disabled = !_adminManager.CanCommand("banlist");
|
||||
WhitelistToggle.Disabled =
|
||||
!(_adminManager.CanCommand("addwhitelist") && _adminManager.CanCommand("removewhitelist"));
|
||||
LogsButton.Disabled = !_adminManager.CanCommand("adminlogs");
|
||||
RejuvenateButton.Disabled = !_adminManager.HasFlag(AdminFlags.Debug);
|
||||
DeleteButton.Disabled = !_adminManager.HasFlag(AdminFlags.Debug);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.Eui;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Eui;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.Administration.UI.PlayerPanel;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class PlayerPanelEui : BaseEui
|
||||
{
|
||||
[Dependency] private readonly IClientConsoleHost _console = default!;
|
||||
[Dependency] private readonly IClientAdminManager _admin = default!;
|
||||
[Dependency] private readonly IClipboardManager _clipboard = default!;
|
||||
|
||||
private PlayerPanel PlayerPanel { get; }
|
||||
|
||||
public PlayerPanelEui()
|
||||
{
|
||||
PlayerPanel = new PlayerPanel(_admin);
|
||||
|
||||
PlayerPanel.OnUsernameCopy += username => _clipboard.SetText(username);
|
||||
PlayerPanel.OnOpenNotes += id => _console.ExecuteCommand($"adminnotes \"{id}\"");
|
||||
// Kick command does not support GUIDs
|
||||
PlayerPanel.OnKick += username => _console.ExecuteCommand($"kick \"{username}\"");
|
||||
PlayerPanel.OnOpenBanPanel += id => _console.ExecuteCommand($"banpanel \"{id}\"");
|
||||
PlayerPanel.OnOpenBans += id => _console.ExecuteCommand($"banlist \"{id}\"");
|
||||
PlayerPanel.OnAhelp += id => _console.ExecuteCommand($"openahelp \"{id}\"");
|
||||
PlayerPanel.OnWhitelistToggle += (id, whitelisted) =>
|
||||
{
|
||||
_console.ExecuteCommand(whitelisted ? $"whitelistremove \"{id}\"" : $"whitelistadd \"{id}\"");
|
||||
};
|
||||
|
||||
PlayerPanel.OnFreezeAndMuteToggle += () => SendMessage(new PlayerPanelFreezeMessage(true));
|
||||
PlayerPanel.OnFreeze += () => SendMessage(new PlayerPanelFreezeMessage());
|
||||
PlayerPanel.OnLogs += () => SendMessage(new PlayerPanelLogsMessage());
|
||||
PlayerPanel.OnRejuvenate += () => SendMessage(new PlayerPanelRejuvenationMessage());
|
||||
PlayerPanel.OnDelete+= () => SendMessage(new PlayerPanelDeleteMessage());
|
||||
|
||||
PlayerPanel.OnClose += () => SendMessage(new CloseEuiMessage());
|
||||
}
|
||||
|
||||
public override void Opened()
|
||||
{
|
||||
PlayerPanel.OpenCentered();
|
||||
}
|
||||
|
||||
public override void Closed()
|
||||
{
|
||||
PlayerPanel.Close();
|
||||
}
|
||||
|
||||
public override void HandleState(EuiStateBase state)
|
||||
{
|
||||
if (state is not PlayerPanelEuiState s)
|
||||
return;
|
||||
|
||||
PlayerPanel.TargetPlayer = s.Guid;
|
||||
PlayerPanel.TargetUsername = s.Username;
|
||||
PlayerPanel.SetUsername(s.Username);
|
||||
PlayerPanel.SetPlaytime(s.Playtime);
|
||||
PlayerPanel.SetBans(s.TotalBans, s.TotalRoleBans);
|
||||
PlayerPanel.SetNotes(s.TotalNotes);
|
||||
PlayerPanel.SetWhitelisted(s.Whitelisted);
|
||||
PlayerPanel.SetSharedConnections(s.SharedConnections);
|
||||
PlayerPanel.SetFrozen(s.CanFreeze, s.Frozen);
|
||||
PlayerPanel.SetAhelp(s.CanAhelp);
|
||||
PlayerPanel.SetButtons();
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,21 @@
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.Station;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Administration.UI.Tabs.ObjectsTab;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ObjectsTab : Control
|
||||
{
|
||||
[Dependency] private readonly IClientAdminManager _admin = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IClientConsoleHost _console = default!;
|
||||
|
||||
private readonly Color _altColor = Color.FromHex("#292B38");
|
||||
private readonly Color _defaultColor = Color.FromHex("#2F2F3B");
|
||||
@@ -49,10 +51,20 @@ public sealed partial class ObjectsTab : Control
|
||||
RefreshListButton.OnPressed += _ => RefreshObjectList();
|
||||
|
||||
var defaultSelection = ObjectsTabSelection.Grids;
|
||||
ObjectTypeOptions.SelectId((int) defaultSelection);
|
||||
ObjectTypeOptions.SelectId((int)defaultSelection);
|
||||
RefreshObjectList(defaultSelection);
|
||||
}
|
||||
|
||||
private void TeleportTo(NetEntity nent)
|
||||
{
|
||||
_console.ExecuteCommand($"tpto {nent}");
|
||||
}
|
||||
|
||||
private void Delete(NetEntity nent)
|
||||
{
|
||||
_console.ExecuteCommand($"delete {nent}");
|
||||
}
|
||||
|
||||
public void RefreshObjectList()
|
||||
{
|
||||
RefreshObjectList(_selections[ObjectTypeOptions.SelectedId]);
|
||||
@@ -116,9 +128,9 @@ public sealed partial class ObjectsTab : Control
|
||||
if (data is not ObjectsListData { Info: var info, BackgroundColor: var backgroundColor })
|
||||
return;
|
||||
|
||||
var entry = new ObjectsTabEntry(info.Name,
|
||||
info.Entity,
|
||||
new StyleBoxFlat { BackgroundColor = backgroundColor });
|
||||
var entry = new ObjectsTabEntry(_admin, info.Name, info.Entity, new StyleBoxFlat { BackgroundColor = backgroundColor });
|
||||
entry.OnTeleport += TeleportTo;
|
||||
entry.OnDelete += Delete;
|
||||
button.ToolTip = $"{info.Name}, {info.Entity}";
|
||||
|
||||
button.AddChild(entry);
|
||||
|
||||
@@ -5,13 +5,25 @@
|
||||
HorizontalExpand="True"
|
||||
SeparationOverride="4">
|
||||
<Label Name="NameLabel"
|
||||
SizeFlagsStretchRatio="3"
|
||||
SizeFlagsStretchRatio="5"
|
||||
HorizontalExpand="True"
|
||||
ClipText="True"/>
|
||||
<customControls:VSeparator/>
|
||||
<Label Name="EIDLabel"
|
||||
SizeFlagsStretchRatio="5"
|
||||
HorizontalExpand="True"
|
||||
ClipText="True"/>
|
||||
<customControls:VSeparator/>
|
||||
<Button Name="TeleportButton"
|
||||
Text="{Loc object-tab-entity-teleport}"
|
||||
SizeFlagsStretchRatio="3"
|
||||
HorizontalExpand="True"
|
||||
ClipText="True"/>
|
||||
<customControls:VSeparator/>
|
||||
<Button Name="DeleteButton"
|
||||
Text="{Loc object-tab-entity-delete}"
|
||||
SizeFlagsStretchRatio="3"
|
||||
HorizontalExpand="True"
|
||||
ClipText="True"/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Content.Client.Administration.Managers;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
@@ -10,12 +11,22 @@ public sealed partial class ObjectsTabEntry : PanelContainer
|
||||
{
|
||||
public NetEntity AssocEntity;
|
||||
|
||||
public ObjectsTabEntry(string name, NetEntity nent, StyleBox styleBox)
|
||||
public Action<NetEntity>? OnTeleport;
|
||||
public Action<NetEntity>? OnDelete;
|
||||
|
||||
public ObjectsTabEntry(IClientAdminManager manager, string name, NetEntity nent, StyleBox styleBox)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
AssocEntity = nent;
|
||||
EIDLabel.Text = nent.ToString();
|
||||
NameLabel.Text = name;
|
||||
BackgroundColorPanel.PanelOverride = styleBox;
|
||||
|
||||
TeleportButton.Disabled = !manager.CanCommand("tpto");
|
||||
DeleteButton.Disabled = !manager.CanCommand("delete");
|
||||
|
||||
TeleportButton.OnPressed += _ => OnTeleport?.Invoke(nent);
|
||||
DeleteButton.OnPressed += _ => OnDelete?.Invoke(nent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,17 +5,23 @@
|
||||
HorizontalExpand="True"
|
||||
SeparationOverride="4">
|
||||
<Label Name="ObjectNameLabel"
|
||||
SizeFlagsStretchRatio="3"
|
||||
SizeFlagsStretchRatio="5"
|
||||
HorizontalExpand="True"
|
||||
ClipText="True"
|
||||
Text="{Loc object-tab-object-name}"
|
||||
MouseFilter="Pass"/>
|
||||
<cc:VSeparator/>
|
||||
<Label Name="EntityIDLabel"
|
||||
SizeFlagsStretchRatio="3"
|
||||
SizeFlagsStretchRatio="5"
|
||||
HorizontalExpand="True"
|
||||
ClipText="True"
|
||||
Text="{Loc object-tab-entity-id}"
|
||||
MouseFilter="Pass"/>
|
||||
<Label Name="EntityTeleportLabel"
|
||||
SizeFlagsStretchRatio="3"
|
||||
HorizontalExpand="True"/>
|
||||
<Label Name="EntityDeleteLabel"
|
||||
SizeFlagsStretchRatio="3"
|
||||
HorizontalExpand="True"/>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
|
||||
@@ -31,7 +31,7 @@ public sealed partial class AnomalyScannerMenu : FancyWindow
|
||||
msg.PushNewline();
|
||||
var time = NextPulseTime.Value - _timing.CurTime;
|
||||
var timestring = $"{time.Minutes:00}:{time.Seconds:00}";
|
||||
msg.AddMarkup(Loc.GetString("anomaly-scanner-pulse-timer", ("time", timestring)));
|
||||
msg.AddMarkupOrThrow(Loc.GetString("anomaly-scanner-pulse-timer", ("time", timestring)));
|
||||
}
|
||||
|
||||
TextDisplay.SetMarkup(msg.ToMarkup());
|
||||
|
||||
10
Content.Client/Atmos/EntitySystems/GasMinerSystem.cs
Normal file
10
Content.Client/Atmos/EntitySystems/GasMinerSystem.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Client.Atmos.EntitySystems;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class GasMinerSystem : SharedGasMinerSystem
|
||||
{
|
||||
|
||||
}
|
||||
@@ -57,7 +57,7 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
|
||||
/// </summary>
|
||||
private int MaxSingleSound => (int) (_maxAmbientCount / (16.0f / 6.0f));
|
||||
|
||||
private readonly Dictionary<AmbientSoundComponent, (EntityUid? Stream, SoundSpecifier Sound, string Path)> _playingSounds = new();
|
||||
private readonly Dictionary<Entity<AmbientSoundComponent>, (EntityUid? Stream, SoundSpecifier Sound, string Path)> _playingSounds = new();
|
||||
private readonly Dictionary<string, int> _playingCount = new();
|
||||
|
||||
public bool OverlayEnabled
|
||||
@@ -107,7 +107,7 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
|
||||
|
||||
private void OnShutdown(EntityUid uid, AmbientSoundComponent component, ComponentShutdown args)
|
||||
{
|
||||
if (!_playingSounds.Remove(component, out var sound))
|
||||
if (!_playingSounds.Remove((uid, component), out var sound))
|
||||
return;
|
||||
|
||||
_audio.Stop(sound.Stream);
|
||||
@@ -120,13 +120,13 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
|
||||
{
|
||||
_ambienceVolume = SharedAudioSystem.GainToVolume(value);
|
||||
|
||||
foreach (var (comp, values) in _playingSounds)
|
||||
foreach (var (ent, values) in _playingSounds)
|
||||
{
|
||||
if (values.Stream == null)
|
||||
continue;
|
||||
|
||||
var stream = values.Stream;
|
||||
_audio.SetVolume(stream, _params.Volume + comp.Volume + _ambienceVolume);
|
||||
_audio.SetVolume(stream, _params.Volume + ent.Comp.Volume + _ambienceVolume);
|
||||
}
|
||||
}
|
||||
private void SetCooldown(float value) => _cooldown = value;
|
||||
@@ -165,7 +165,7 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
|
||||
if (_gameTiming.CurTime < _targetTime)
|
||||
return;
|
||||
|
||||
_targetTime = _gameTiming.CurTime+TimeSpan.FromSeconds(_cooldown);
|
||||
_targetTime = _gameTiming.CurTime + TimeSpan.FromSeconds(_cooldown);
|
||||
|
||||
var player = _playerManager.LocalEntity;
|
||||
if (!EntityManager.TryGetComponent(player, out TransformComponent? xform))
|
||||
@@ -190,7 +190,7 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
|
||||
|
||||
private readonly struct QueryState
|
||||
{
|
||||
public readonly Dictionary<string, List<(float Importance, AmbientSoundComponent)>> SourceDict = new();
|
||||
public readonly Dictionary<string, List<(float Importance, Entity<AmbientSoundComponent>)>> SourceDict = new();
|
||||
public readonly Vector2 MapPos;
|
||||
public readonly TransformComponent Player;
|
||||
public readonly SharedTransformSystem TransformSystem;
|
||||
@@ -224,11 +224,11 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
|
||||
if (ambientComp.Sound is SoundPathSpecifier path)
|
||||
key = path.Path.ToString();
|
||||
else
|
||||
key = ((SoundCollectionSpecifier) ambientComp.Sound).Collection ?? string.Empty;
|
||||
key = ((SoundCollectionSpecifier)ambientComp.Sound).Collection ?? string.Empty;
|
||||
|
||||
// Prioritize far away & loud sounds.
|
||||
var importance = range * (ambientComp.Volume + 32);
|
||||
state.SourceDict.GetOrNew(key).Add((importance, ambientComp));
|
||||
state.SourceDict.GetOrNew(key).Add((importance, (value.Uid, ambientComp)));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -242,16 +242,18 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
|
||||
var mapPos = _xformSystem.GetMapCoordinates(playerXform);
|
||||
|
||||
// Remove out-of-range ambiences
|
||||
foreach (var (comp, sound) in _playingSounds)
|
||||
foreach (var (ent, sound) in _playingSounds)
|
||||
{
|
||||
var entity = comp.Owner;
|
||||
//var entity = comp.Owner;
|
||||
var owner = ent.Owner;
|
||||
var comp = ent.Comp;
|
||||
|
||||
if (comp.Enabled &&
|
||||
// Don't keep playing sounds that have changed since.
|
||||
sound.Sound == comp.Sound &&
|
||||
query.TryGetComponent(entity, out var xform) &&
|
||||
query.TryGetComponent(owner, out var xform) &&
|
||||
xform.MapID == playerXform.MapID &&
|
||||
!metaQuery.GetComponent(entity).EntityPaused)
|
||||
!metaQuery.GetComponent(owner).EntityPaused)
|
||||
{
|
||||
// TODO: This is just trydistance for coordinates.
|
||||
var distance = (xform.ParentUid == playerXform.ParentUid)
|
||||
@@ -263,7 +265,7 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
|
||||
}
|
||||
|
||||
_audio.Stop(sound.Stream);
|
||||
_playingSounds.Remove(comp);
|
||||
_playingSounds.Remove(ent);
|
||||
_playingCount[sound.Path] -= 1;
|
||||
if (_playingCount[sound.Path] == 0)
|
||||
_playingCount.Remove(sound.Path);
|
||||
@@ -278,7 +280,7 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
|
||||
_treeSys.QueryAabb(ref state, Callback, mapPos.MapId, worldAabb);
|
||||
|
||||
// Add in range ambiences
|
||||
foreach (var (key, sources) in state.SourceDict)
|
||||
foreach (var (key, sourceList) in state.SourceDict)
|
||||
{
|
||||
if (_playingSounds.Count >= _maxAmbientCount)
|
||||
break;
|
||||
@@ -286,13 +288,14 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
|
||||
if (_playingCount.TryGetValue(key, out var playingCount) && playingCount >= MaxSingleSound)
|
||||
continue;
|
||||
|
||||
sources.Sort(static (a, b) => b.Importance.CompareTo(a.Importance));
|
||||
sourceList.Sort(static (a, b) => b.Importance.CompareTo(a.Importance));
|
||||
|
||||
foreach (var (_, comp) in sources)
|
||||
foreach (var (_, sourceEntity) in sourceList)
|
||||
{
|
||||
var uid = comp.Owner;
|
||||
var uid = sourceEntity.Owner;
|
||||
var comp = sourceEntity.Comp;
|
||||
|
||||
if (_playingSounds.ContainsKey(comp) ||
|
||||
if (_playingSounds.ContainsKey(sourceEntity) ||
|
||||
metaQuery.GetComponent(uid).EntityPaused)
|
||||
continue;
|
||||
|
||||
@@ -303,7 +306,7 @@ public sealed class AmbientSoundSystem : SharedAmbientSoundSystem
|
||||
.WithMaxDistance(comp.Range);
|
||||
|
||||
var stream = _audio.PlayEntity(comp.Sound, Filter.Local(), uid, false, audioParams);
|
||||
_playingSounds[comp] = (stream.Value.Entity, comp.Sound, key);
|
||||
_playingSounds[sourceEntity] = (stream.Value.Entity, comp.Sound, key);
|
||||
playingCount++;
|
||||
|
||||
if (_playingSounds.Count >= _maxAmbientCount)
|
||||
|
||||
@@ -1,148 +1,17 @@
|
||||
using System.Numerics;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Graphics;
|
||||
using static Robust.Client.GameObjects.SpriteComponent;
|
||||
using Direction = Robust.Shared.Maths.Direction;
|
||||
namespace Content.Client.Clickable;
|
||||
|
||||
namespace Content.Client.Clickable
|
||||
[RegisterComponent]
|
||||
public sealed partial class ClickableComponent : Component
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed partial class ClickableComponent : Component
|
||||
[DataField] public DirBoundData? Bounds;
|
||||
|
||||
[DataDefinition]
|
||||
public sealed partial class DirBoundData
|
||||
{
|
||||
[Dependency] private readonly IClickMapManager _clickMapManager = default!;
|
||||
|
||||
[DataField("bounds")] public DirBoundData? Bounds;
|
||||
|
||||
/// <summary>
|
||||
/// Used to check whether a click worked. Will first check if the click falls inside of some explicit bounding
|
||||
/// boxes (see <see cref="Bounds"/>). If that fails, attempts to use automatically generated click maps.
|
||||
/// </summary>
|
||||
/// <param name="worldPos">The world position that was clicked.</param>
|
||||
/// <param name="drawDepth">
|
||||
/// The draw depth for the sprite that captured the click.
|
||||
/// </param>
|
||||
/// <returns>True if the click worked, false otherwise.</returns>
|
||||
public bool CheckClick(SpriteComponent sprite, TransformComponent transform, EntityQuery<TransformComponent> xformQuery, Vector2 worldPos, IEye eye, out int drawDepth, out uint renderOrder, out float bottom)
|
||||
{
|
||||
if (!sprite.Visible)
|
||||
{
|
||||
drawDepth = default;
|
||||
renderOrder = default;
|
||||
bottom = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
drawDepth = sprite.DrawDepth;
|
||||
renderOrder = sprite.RenderOrder;
|
||||
var (spritePos, spriteRot) = transform.GetWorldPositionRotation(xformQuery);
|
||||
var spriteBB = sprite.CalculateRotatedBoundingBox(spritePos, spriteRot, eye.Rotation);
|
||||
bottom = Matrix3Helpers.CreateRotation(eye.Rotation).TransformBox(spriteBB).Bottom;
|
||||
|
||||
Matrix3x2.Invert(sprite.GetLocalMatrix(), out var invSpriteMatrix);
|
||||
|
||||
// This should have been the rotation of the sprite relative to the screen, but this is not the case with no-rot or directional sprites.
|
||||
var relativeRotation = (spriteRot + eye.Rotation).Reduced().FlipPositive();
|
||||
|
||||
Angle cardinalSnapping = sprite.SnapCardinals ? relativeRotation.GetCardinalDir().ToAngle() : Angle.Zero;
|
||||
|
||||
// First we get `localPos`, the clicked location in the sprite-coordinate frame.
|
||||
var entityXform = Matrix3Helpers.CreateInverseTransform(spritePos, sprite.NoRotation ? -eye.Rotation : spriteRot - cardinalSnapping);
|
||||
var localPos = Vector2.Transform(Vector2.Transform(worldPos, entityXform), invSpriteMatrix);
|
||||
|
||||
// Check explicitly defined click-able bounds
|
||||
if (CheckDirBound(sprite, relativeRotation, localPos))
|
||||
return true;
|
||||
|
||||
// Next check each individual sprite layer using automatically computed click maps.
|
||||
foreach (var spriteLayer in sprite.AllLayers)
|
||||
{
|
||||
// TODO: Move this to a system and also use SpriteSystem.IsVisible instead.
|
||||
if (!spriteLayer.Visible || spriteLayer is not Layer layer || layer.CopyToShaderParameters != null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check the layer's texture, if it has one
|
||||
if (layer.Texture != null)
|
||||
{
|
||||
// Convert to image coordinates
|
||||
var imagePos = (Vector2i) (localPos * EyeManager.PixelsPerMeter * new Vector2(1, -1) + layer.Texture.Size / 2f);
|
||||
|
||||
if (_clickMapManager.IsOccluding(layer.Texture, imagePos))
|
||||
return true;
|
||||
}
|
||||
|
||||
// Either we weren't clicking on the texture, or there wasn't one. In which case: check the RSI next
|
||||
if (layer.ActualRsi is not { } rsi || !rsi.TryGetState(layer.State, out var rsiState))
|
||||
continue;
|
||||
|
||||
var dir = Layer.GetDirection(rsiState.RsiDirections, relativeRotation);
|
||||
|
||||
// convert to layer-local coordinates
|
||||
layer.GetLayerDrawMatrix(dir, out var matrix);
|
||||
Matrix3x2.Invert(matrix, out var inverseMatrix);
|
||||
var layerLocal = Vector2.Transform(localPos, inverseMatrix);
|
||||
|
||||
// Convert to image coordinates
|
||||
var layerImagePos = (Vector2i) (layerLocal * EyeManager.PixelsPerMeter * new Vector2(1, -1) + rsiState.Size / 2f);
|
||||
|
||||
// Next, to get the right click map we need the "direction" of this layer that is actually being used to draw the sprite on the screen.
|
||||
// This **can** differ from the dir defined before, but can also just be the same.
|
||||
if (sprite.EnableDirectionOverride)
|
||||
dir = sprite.DirectionOverride.Convert(rsiState.RsiDirections);
|
||||
dir = dir.OffsetRsiDir(layer.DirOffset);
|
||||
|
||||
if (_clickMapManager.IsOccluding(layer.ActualRsi!, layer.State, dir, layer.AnimationFrame, layerImagePos))
|
||||
return true;
|
||||
}
|
||||
|
||||
drawDepth = default;
|
||||
renderOrder = default;
|
||||
bottom = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool CheckDirBound(SpriteComponent sprite, Angle relativeRotation, Vector2 localPos)
|
||||
{
|
||||
if (Bounds == null)
|
||||
return false;
|
||||
|
||||
// These explicit bounds only work for either 1 or 4 directional sprites.
|
||||
|
||||
// This would be the orientation of a 4-directional sprite.
|
||||
var direction = relativeRotation.GetCardinalDir();
|
||||
|
||||
var modLocalPos = sprite.NoRotation
|
||||
? localPos
|
||||
: direction.ToAngle().RotateVec(localPos);
|
||||
|
||||
// First, check the bounding box that is valid for all orientations
|
||||
if (Bounds.All.Contains(modLocalPos))
|
||||
return true;
|
||||
|
||||
// Next, get and check the appropriate bounding box for the current sprite orientation
|
||||
var boundsForDir = (sprite.EnableDirectionOverride ? sprite.DirectionOverride : direction) switch
|
||||
{
|
||||
Direction.East => Bounds.East,
|
||||
Direction.North => Bounds.North,
|
||||
Direction.South => Bounds.South,
|
||||
Direction.West => Bounds.West,
|
||||
_ => throw new InvalidOperationException()
|
||||
};
|
||||
|
||||
return boundsForDir.Contains(modLocalPos);
|
||||
}
|
||||
|
||||
[DataDefinition]
|
||||
public sealed partial class DirBoundData
|
||||
{
|
||||
[DataField("all")] public Box2 All;
|
||||
[DataField("north")] public Box2 North;
|
||||
[DataField("south")] public Box2 South;
|
||||
[DataField("east")] public Box2 East;
|
||||
[DataField("west")] public Box2 West;
|
||||
}
|
||||
[DataField] public Box2 All;
|
||||
[DataField] public Box2 North;
|
||||
[DataField] public Box2 South;
|
||||
[DataField] public Box2 East;
|
||||
[DataField] public Box2 West;
|
||||
}
|
||||
}
|
||||
|
||||
168
Content.Client/Clickable/ClickableSystem.cs
Normal file
168
Content.Client/Clickable/ClickableSystem.cs
Normal file
@@ -0,0 +1,168 @@
|
||||
using System.Numerics;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Utility;
|
||||
using Robust.Shared.Graphics;
|
||||
|
||||
namespace Content.Client.Clickable;
|
||||
|
||||
/// <summary>
|
||||
/// Handles click detection for sprites.
|
||||
/// </summary>
|
||||
public sealed class ClickableSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IClickMapManager _clickMapManager = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transforms = default!;
|
||||
[Dependency] private readonly SpriteSystem _sprites = default!;
|
||||
|
||||
private EntityQuery<ClickableComponent> _clickableQuery;
|
||||
private EntityQuery<TransformComponent> _xformQuery;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
_clickableQuery = GetEntityQuery<ClickableComponent>();
|
||||
_xformQuery = GetEntityQuery<TransformComponent>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to check whether a click worked. Will first check if the click falls inside of some explicit bounding
|
||||
/// boxes (see <see cref="Bounds"/>). If that fails, attempts to use automatically generated click maps.
|
||||
/// </summary>
|
||||
/// <param name="worldPos">The world position that was clicked.</param>
|
||||
/// <param name="drawDepth">
|
||||
/// The draw depth for the sprite that captured the click.
|
||||
/// </param>
|
||||
/// <returns>True if the click worked, false otherwise.</returns>
|
||||
public bool CheckClick(Entity<ClickableComponent?, SpriteComponent, TransformComponent?> entity, Vector2 worldPos, IEye eye, out int drawDepth, out uint renderOrder, out float bottom)
|
||||
{
|
||||
if (!_clickableQuery.Resolve(entity.Owner, ref entity.Comp1, false))
|
||||
{
|
||||
drawDepth = default;
|
||||
renderOrder = default;
|
||||
bottom = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_xformQuery.Resolve(entity.Owner, ref entity.Comp3))
|
||||
{
|
||||
drawDepth = default;
|
||||
renderOrder = default;
|
||||
bottom = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var sprite = entity.Comp2;
|
||||
var transform = entity.Comp3;
|
||||
|
||||
if (!sprite.Visible)
|
||||
{
|
||||
drawDepth = default;
|
||||
renderOrder = default;
|
||||
bottom = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
drawDepth = sprite.DrawDepth;
|
||||
renderOrder = sprite.RenderOrder;
|
||||
var (spritePos, spriteRot) = _transforms.GetWorldPositionRotation(transform);
|
||||
var spriteBB = sprite.CalculateRotatedBoundingBox(spritePos, spriteRot, eye.Rotation);
|
||||
bottom = Matrix3Helpers.CreateRotation(eye.Rotation).TransformBox(spriteBB).Bottom;
|
||||
|
||||
Matrix3x2.Invert(sprite.GetLocalMatrix(), out var invSpriteMatrix);
|
||||
|
||||
// This should have been the rotation of the sprite relative to the screen, but this is not the case with no-rot or directional sprites.
|
||||
var relativeRotation = (spriteRot + eye.Rotation).Reduced().FlipPositive();
|
||||
|
||||
var cardinalSnapping = sprite.SnapCardinals ? relativeRotation.GetCardinalDir().ToAngle() : Angle.Zero;
|
||||
|
||||
// First we get `localPos`, the clicked location in the sprite-coordinate frame.
|
||||
var entityXform = Matrix3Helpers.CreateInverseTransform(spritePos, sprite.NoRotation ? -eye.Rotation : spriteRot - cardinalSnapping);
|
||||
var localPos = Vector2.Transform(Vector2.Transform(worldPos, entityXform), invSpriteMatrix);
|
||||
|
||||
// Check explicitly defined click-able bounds
|
||||
if (CheckDirBound((entity.Owner, entity.Comp1, entity.Comp2), relativeRotation, localPos))
|
||||
return true;
|
||||
|
||||
// Next check each individual sprite layer using automatically computed click maps.
|
||||
foreach (var spriteLayer in sprite.AllLayers)
|
||||
{
|
||||
if (spriteLayer is not SpriteComponent.Layer layer || !_sprites.IsVisible(layer))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check the layer's texture, if it has one
|
||||
if (layer.Texture != null)
|
||||
{
|
||||
// Convert to image coordinates
|
||||
var imagePos = (Vector2i) (localPos * EyeManager.PixelsPerMeter * new Vector2(1, -1) + layer.Texture.Size / 2f);
|
||||
|
||||
if (_clickMapManager.IsOccluding(layer.Texture, imagePos))
|
||||
return true;
|
||||
}
|
||||
|
||||
// Either we weren't clicking on the texture, or there wasn't one. In which case: check the RSI next
|
||||
if (layer.ActualRsi is not { } rsi || !rsi.TryGetState(layer.State, out var rsiState))
|
||||
continue;
|
||||
|
||||
var dir = SpriteComponent.Layer.GetDirection(rsiState.RsiDirections, relativeRotation);
|
||||
|
||||
// convert to layer-local coordinates
|
||||
layer.GetLayerDrawMatrix(dir, out var matrix);
|
||||
Matrix3x2.Invert(matrix, out var inverseMatrix);
|
||||
var layerLocal = Vector2.Transform(localPos, inverseMatrix);
|
||||
|
||||
// Convert to image coordinates
|
||||
var layerImagePos = (Vector2i) (layerLocal * EyeManager.PixelsPerMeter * new Vector2(1, -1) + rsiState.Size / 2f);
|
||||
|
||||
// Next, to get the right click map we need the "direction" of this layer that is actually being used to draw the sprite on the screen.
|
||||
// This **can** differ from the dir defined before, but can also just be the same.
|
||||
if (sprite.EnableDirectionOverride)
|
||||
dir = sprite.DirectionOverride.Convert(rsiState.RsiDirections);
|
||||
dir = dir.OffsetRsiDir(layer.DirOffset);
|
||||
|
||||
if (_clickMapManager.IsOccluding(layer.ActualRsi!, layer.State, dir, layer.AnimationFrame, layerImagePos))
|
||||
return true;
|
||||
}
|
||||
|
||||
drawDepth = default;
|
||||
renderOrder = default;
|
||||
bottom = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool CheckDirBound(Entity<ClickableComponent, SpriteComponent> entity, Angle relativeRotation, Vector2 localPos)
|
||||
{
|
||||
var clickable = entity.Comp1;
|
||||
var sprite = entity.Comp2;
|
||||
|
||||
if (clickable.Bounds == null)
|
||||
return false;
|
||||
|
||||
// These explicit bounds only work for either 1 or 4 directional sprites.
|
||||
|
||||
// This would be the orientation of a 4-directional sprite.
|
||||
var direction = relativeRotation.GetCardinalDir();
|
||||
|
||||
var modLocalPos = sprite.NoRotation
|
||||
? localPos
|
||||
: direction.ToAngle().RotateVec(localPos);
|
||||
|
||||
// First, check the bounding box that is valid for all orientations
|
||||
if (clickable.Bounds.All.Contains(modLocalPos))
|
||||
return true;
|
||||
|
||||
// Next, get and check the appropriate bounding box for the current sprite orientation
|
||||
var boundsForDir = (sprite.EnableDirectionOverride ? sprite.DirectionOverride : direction) switch
|
||||
{
|
||||
Direction.East => clickable.Bounds.East,
|
||||
Direction.North => clickable.Bounds.North,
|
||||
Direction.South => clickable.Bounds.South,
|
||||
Direction.West => clickable.Bounds.West,
|
||||
_ => throw new InvalidOperationException()
|
||||
};
|
||||
|
||||
return boundsForDir.Contains(modLocalPos);
|
||||
}
|
||||
}
|
||||
6
Content.Client/Clothing/Systems/CursedMaskSystem.cs
Normal file
6
Content.Client/Clothing/Systems/CursedMaskSystem.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
using Content.Shared.Clothing;
|
||||
|
||||
namespace Content.Client.Clothing.Systems;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed class CursedMaskSystem : SharedCursedMaskSystem;
|
||||
@@ -51,7 +51,7 @@ public sealed class CrewManifestSection : BoxContainer
|
||||
title.SetMessage(entry.JobTitle);
|
||||
|
||||
|
||||
if (prototypeManager.TryIndex<StatusIconPrototype>(entry.JobIcon, out var jobIcon))
|
||||
if (prototypeManager.TryIndex<JobIconPrototype>(entry.JobIcon, out var jobIcon))
|
||||
{
|
||||
var icon = new TextureRect()
|
||||
{
|
||||
|
||||
@@ -7,10 +7,12 @@ using Content.Shared.Security;
|
||||
using Content.Shared.StationRecords;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Client.CriminalRecords;
|
||||
|
||||
@@ -36,7 +38,6 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
|
||||
public Action<SecurityStatus, string>? OnDialogConfirmed;
|
||||
|
||||
private uint _maxLength;
|
||||
private bool _isPopulating;
|
||||
private bool _access;
|
||||
private uint? _selectedKey;
|
||||
private CriminalRecord? _selectedRecord;
|
||||
@@ -74,7 +75,7 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
|
||||
|
||||
RecordListing.OnItemSelected += args =>
|
||||
{
|
||||
if (_isPopulating || RecordListing[args.ItemIndex].Metadata is not uint cast)
|
||||
if (RecordListing[args.ItemIndex].Metadata is not uint cast)
|
||||
return;
|
||||
|
||||
OnKeySelected?.Invoke(cast);
|
||||
@@ -82,8 +83,7 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
|
||||
|
||||
RecordListing.OnItemDeselected += _ =>
|
||||
{
|
||||
if (!_isPopulating)
|
||||
OnKeySelected?.Invoke(null);
|
||||
OnKeySelected?.Invoke(null);
|
||||
};
|
||||
|
||||
FilterType.OnItemSelected += eventArgs =>
|
||||
@@ -133,13 +133,8 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
|
||||
|
||||
FilterType.SelectId((int)_currentFilterType);
|
||||
|
||||
// set up the records listing panel
|
||||
RecordListing.Clear();
|
||||
|
||||
var hasRecords = state.RecordListing != null && state.RecordListing.Count > 0;
|
||||
NoRecords.Visible = !hasRecords;
|
||||
if (hasRecords)
|
||||
PopulateRecordListing(state.RecordListing!);
|
||||
NoRecords.Visible = state.RecordListing == null || state.RecordListing.Count == 0;
|
||||
PopulateRecordListing(state.RecordListing);
|
||||
|
||||
// set up the selected person's record
|
||||
var selected = _selectedKey != null;
|
||||
@@ -167,19 +162,59 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateRecordListing(Dictionary<uint, string> listing)
|
||||
private void PopulateRecordListing(Dictionary<uint, string>? listing)
|
||||
{
|
||||
_isPopulating = true;
|
||||
|
||||
foreach (var (key, name) in listing)
|
||||
if (listing == null)
|
||||
{
|
||||
var item = RecordListing.AddItem(name);
|
||||
item.Metadata = key;
|
||||
item.Selected = key == _selectedKey;
|
||||
RecordListing.Clear();
|
||||
return;
|
||||
}
|
||||
_isPopulating = false;
|
||||
|
||||
RecordListing.SortItemsByText();
|
||||
var entries = listing.ToList();
|
||||
entries.Sort((a, b) => string.Compare(a.Value, b.Value, StringComparison.Ordinal));
|
||||
// `entries` now contains the definitive list of items which should be in
|
||||
// our list of records and is in the order we want to present those items.
|
||||
|
||||
// Walk through the existing items in RecordListing and in the updated listing
|
||||
// in parallel to synchronize the items in RecordListing with `entries`.
|
||||
int i = RecordListing.Count - 1;
|
||||
int j = entries.Count - 1;
|
||||
while(i >= 0 && j >= 0)
|
||||
{
|
||||
var strcmp = string.Compare(RecordListing[i].Text, entries[j].Value, StringComparison.Ordinal);
|
||||
if (strcmp == 0)
|
||||
{
|
||||
// This item exists in both RecordListing and `entries`. Nothing to do.
|
||||
i--;
|
||||
j--;
|
||||
}
|
||||
else if (strcmp > 0)
|
||||
{
|
||||
// Item exists in RecordListing, but not in `entries`. Remove it.
|
||||
RecordListing.RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
else if (strcmp < 0)
|
||||
{
|
||||
// A new entry which doesn't exist in RecordListing. Create it.
|
||||
RecordListing.Insert(i + 1, new ItemList.Item(RecordListing){Text = entries[j].Value, Metadata = entries[j].Key});
|
||||
j--;
|
||||
}
|
||||
}
|
||||
|
||||
// Any remaining items in RecordListing don't exist in `entries`, so remove them
|
||||
while (i >= 0)
|
||||
{
|
||||
RecordListing.RemoveAt(i);
|
||||
i--;
|
||||
}
|
||||
|
||||
// And finally, any remaining items in `entries`, don't exist in RecordListing. Create them.
|
||||
while (j >= 0)
|
||||
{
|
||||
RecordListing.Insert(0, new ItemList.Item(RecordListing){Text = entries[j].Value, Metadata = entries[j].Key});
|
||||
j--;
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateRecordContainer(GeneralStationRecord stationRecord, CriminalRecord criminalRecord)
|
||||
@@ -211,10 +246,7 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
|
||||
|
||||
private void FilterListingOfRecords(string text = "")
|
||||
{
|
||||
if (!_isPopulating)
|
||||
{
|
||||
OnFiltersChanged?.Invoke(_currentFilterType, text);
|
||||
}
|
||||
OnFiltersChanged?.Invoke(_currentFilterType, text);
|
||||
}
|
||||
|
||||
private void SetStatus(SecurityStatus status)
|
||||
|
||||
@@ -54,10 +54,16 @@ namespace Content.Client.Forensics
|
||||
}
|
||||
text.AppendLine();
|
||||
text.AppendLine(Loc.GetString("forensic-scanner-interface-dnas"));
|
||||
foreach (var dna in msg.DNAs)
|
||||
foreach (var dna in msg.TouchDNAs)
|
||||
{
|
||||
text.AppendLine(dna);
|
||||
}
|
||||
foreach (var dna in msg.SolutionDNAs)
|
||||
{
|
||||
if (msg.TouchDNAs.Contains(dna))
|
||||
continue;
|
||||
text.AppendLine(dna);
|
||||
}
|
||||
text.AppendLine();
|
||||
text.AppendLine(Loc.GetString("forensic-scanner-interface-residues"));
|
||||
foreach (var residue in msg.Residues)
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.Clickable;
|
||||
using Content.Client.UserInterface;
|
||||
using Content.Client.Viewport;
|
||||
using Content.Shared.Input;
|
||||
using Robust.Client.ComponentTrees;
|
||||
using Robust.Client.GameObjects;
|
||||
@@ -13,11 +14,13 @@ using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Graphics;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
using YamlDotNet.Serialization.TypeInspectors;
|
||||
|
||||
namespace Content.Client.Gameplay
|
||||
{
|
||||
@@ -98,7 +101,15 @@ namespace Content.Client.Gameplay
|
||||
|
||||
public EntityUid? GetClickedEntity(MapCoordinates coordinates)
|
||||
{
|
||||
var first = GetClickableEntities(coordinates).FirstOrDefault();
|
||||
return GetClickedEntity(coordinates, _eyeManager.CurrentEye);
|
||||
}
|
||||
|
||||
public EntityUid? GetClickedEntity(MapCoordinates coordinates, IEye? eye)
|
||||
{
|
||||
if (eye == null)
|
||||
return null;
|
||||
|
||||
var first = GetClickableEntities(coordinates, eye).FirstOrDefault();
|
||||
return first.IsValid() ? first : null;
|
||||
}
|
||||
|
||||
@@ -110,6 +121,20 @@ namespace Content.Client.Gameplay
|
||||
|
||||
public IEnumerable<EntityUid> GetClickableEntities(MapCoordinates coordinates)
|
||||
{
|
||||
return GetClickableEntities(coordinates, _eyeManager.CurrentEye);
|
||||
}
|
||||
|
||||
public IEnumerable<EntityUid> GetClickableEntities(MapCoordinates coordinates, IEye? eye)
|
||||
{
|
||||
/*
|
||||
* TODO:
|
||||
* 1. Stuff like MeleeWeaponSystem need an easy way to hook into viewport specific entities / entities under mouse
|
||||
* 2. Cleanup the mess around InteractionOutlineSystem + below the keybind click detection.
|
||||
*/
|
||||
|
||||
if (eye == null)
|
||||
return Array.Empty<EntityUid>();
|
||||
|
||||
// Find all the entities intersecting our click
|
||||
var spriteTree = _entityManager.EntitySysManager.GetEntitySystem<SpriteTreeSystem>();
|
||||
var entities = spriteTree.QueryAabb(coordinates.MapId, Box2.CenteredAround(coordinates.Position, new Vector2(1, 1)));
|
||||
@@ -117,15 +142,12 @@ namespace Content.Client.Gameplay
|
||||
// Check the entities against whether or not we can click them
|
||||
var foundEntities = new List<(EntityUid, int, uint, float)>(entities.Count);
|
||||
var clickQuery = _entityManager.GetEntityQuery<ClickableComponent>();
|
||||
var xformQuery = _entityManager.GetEntityQuery<TransformComponent>();
|
||||
|
||||
// TODO: Smelly
|
||||
var eye = _eyeManager.CurrentEye;
|
||||
var clickables = _entityManager.System<ClickableSystem>();
|
||||
|
||||
foreach (var entity in entities)
|
||||
{
|
||||
if (clickQuery.TryGetComponent(entity.Uid, out var component) &&
|
||||
component.CheckClick(entity.Component, entity.Transform, xformQuery, coordinates.Position, eye, out var drawDepthClicked, out var renderOrder, out var bottom))
|
||||
clickables.CheckClick((entity.Uid, component, entity.Component, entity.Transform), coordinates.Position, eye, out var drawDepthClicked, out var renderOrder, out var bottom))
|
||||
{
|
||||
foundEntities.Add((entity.Uid, drawDepthClicked, renderOrder, bottom));
|
||||
}
|
||||
@@ -188,7 +210,15 @@ namespace Content.Client.Gameplay
|
||||
if (args.Viewport is IViewportControl vp && kArgs.PointerLocation.IsValid)
|
||||
{
|
||||
var mousePosWorld = vp.PixelToMap(kArgs.PointerLocation.Position);
|
||||
entityToClick = GetClickedEntity(mousePosWorld);
|
||||
|
||||
if (vp is ScalingViewport svp)
|
||||
{
|
||||
entityToClick = GetClickedEntity(mousePosWorld, svp.Eye);
|
||||
}
|
||||
else
|
||||
{
|
||||
entityToClick = GetClickedEntity(mousePosWorld);
|
||||
}
|
||||
|
||||
coordinates = _mapManager.TryFindGridAt(mousePosWorld, out _, out var grid) ?
|
||||
grid.MapToGrid(mousePosWorld) :
|
||||
|
||||
41
Content.Client/Guidebook/Controls/GuidebookError.xaml
Normal file
41
Content.Client/Guidebook/Controls/GuidebookError.xaml
Normal file
@@ -0,0 +1,41 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
Margin="5 5 5 5"
|
||||
MinHeight="200">
|
||||
<PanelContainer HorizontalExpand="True">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BorderThickness="2" BorderColor="White" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<BoxContainer Orientation="Vertical">
|
||||
|
||||
<PanelContainer>
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BorderThickness="0 0 0 1" BackgroundColor="DarkRed" BorderColor="Black" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<Label Margin="5" StyleClasses="bold" Text="{Loc 'guidebook-parser-error'}" />
|
||||
</PanelContainer>
|
||||
|
||||
<OutputPanel Margin="5" MinHeight="75" VerticalExpand="True" Name="Original">
|
||||
<OutputPanel.StyleBoxOverride>
|
||||
<gfx:StyleBoxFlat BorderThickness="0 0 0 1" BorderColor="Gray"
|
||||
ContentMarginLeftOverride="3" ContentMarginRightOverride="3"
|
||||
ContentMarginBottomOverride="3" ContentMarginTopOverride="3" />
|
||||
</OutputPanel.StyleBoxOverride>
|
||||
</OutputPanel>
|
||||
|
||||
<Collapsible Margin="5" MinHeight="75" VerticalExpand="True">
|
||||
<CollapsibleHeading Title="{Loc 'guidebook-error-message' }" />
|
||||
<CollapsibleBody VerticalExpand="True">
|
||||
<OutputPanel Name="Error" VerticalExpand="True" MinHeight="100">
|
||||
<OutputPanel.StyleBoxOverride>
|
||||
<gfx:StyleBoxFlat
|
||||
ContentMarginLeftOverride="3" ContentMarginRightOverride="3"
|
||||
ContentMarginBottomOverride="3" ContentMarginTopOverride="3" />
|
||||
</OutputPanel.StyleBoxOverride>
|
||||
</OutputPanel>
|
||||
</CollapsibleBody>
|
||||
</Collapsible>
|
||||
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
23
Content.Client/Guidebook/Controls/GuidebookError.xaml.cs
Normal file
23
Content.Client/Guidebook/Controls/GuidebookError.xaml.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Guidebook.Controls;
|
||||
|
||||
[UsedImplicitly] [GenerateTypedNameReferences]
|
||||
public sealed partial class GuidebookError : BoxContainer
|
||||
{
|
||||
public GuidebookError()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public GuidebookError(string original, string? error) : this()
|
||||
{
|
||||
Original.AddText(original);
|
||||
|
||||
if (error is not null)
|
||||
Error.AddText(error);
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,10 @@ using Content.Client.UserInterface.ControlExtensions;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Controls.FancyTree;
|
||||
using Content.Client.UserInterface.Systems.Info;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Guidebook;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
@@ -18,15 +16,18 @@ namespace Content.Client.Guidebook.Controls;
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
|
||||
{
|
||||
[Dependency] private readonly IResourceManager _resourceManager = default!;
|
||||
[Dependency] private readonly DocumentParsingManager _parsingMan = default!;
|
||||
[Dependency] private readonly IResourceManager _resourceManager = default!;
|
||||
|
||||
private Dictionary<ProtoId<GuideEntryPrototype>, GuideEntry> _entries = new();
|
||||
|
||||
private readonly ISawmill _sawmill;
|
||||
|
||||
public GuidebookWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
_sawmill = Logger.GetSawmill("Guidebook");
|
||||
|
||||
Tree.OnSelectedItemChanged += OnSelectionChanged;
|
||||
|
||||
@@ -36,6 +37,20 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
|
||||
};
|
||||
}
|
||||
|
||||
public void HandleClick(string link)
|
||||
{
|
||||
if (!_entries.TryGetValue(link, out var entry))
|
||||
return;
|
||||
|
||||
if (Tree.TryGetIndexFromMetadata(entry, out var index))
|
||||
{
|
||||
Tree.ExpandParentEntries(index.Value);
|
||||
Tree.SetSelectedIndex(index);
|
||||
}
|
||||
else
|
||||
ShowGuide(entry);
|
||||
}
|
||||
|
||||
private void OnSelectionChanged(TreeItem? item)
|
||||
{
|
||||
if (item != null && item.Metadata is GuideEntry entry)
|
||||
@@ -71,8 +86,9 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
|
||||
|
||||
if (!_parsingMan.TryAddMarkup(EntryContainer, file.ReadToEnd()))
|
||||
{
|
||||
EntryContainer.AddChild(new Label() { Text = "ERROR: Failed to parse document." });
|
||||
Logger.Error($"Failed to parse contents of guide document {entry.Id}.");
|
||||
// The guidebook will automatically display the in-guidebook error if it fails
|
||||
|
||||
_sawmill.Error($"Failed to parse contents of guide document {entry.Id}.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,8 +140,10 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
|
||||
|
||||
entry.Children = sortedChildren;
|
||||
}
|
||||
|
||||
entries.ExceptWith(entry.Children);
|
||||
}
|
||||
|
||||
rootEntries = entries.ToList();
|
||||
}
|
||||
|
||||
@@ -135,21 +153,25 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
|
||||
.ThenBy(rootEntry => Loc.GetString(rootEntry.Name));
|
||||
}
|
||||
|
||||
private void RepopulateTree(List<ProtoId<GuideEntryPrototype>>? roots = null, ProtoId<GuideEntryPrototype>? forcedRoot = null)
|
||||
private void RepopulateTree(List<ProtoId<GuideEntryPrototype>>? roots = null,
|
||||
ProtoId<GuideEntryPrototype>? forcedRoot = null)
|
||||
{
|
||||
Tree.Clear();
|
||||
|
||||
HashSet<ProtoId<GuideEntryPrototype>> addedEntries = new();
|
||||
|
||||
TreeItem? parent = forcedRoot == null ? null : AddEntry(forcedRoot.Value, null, addedEntries);
|
||||
var parent = forcedRoot == null ? null : AddEntry(forcedRoot.Value, null, addedEntries);
|
||||
foreach (var entry in GetSortedEntries(roots))
|
||||
{
|
||||
AddEntry(entry.Id, parent, addedEntries);
|
||||
}
|
||||
|
||||
Tree.SetAllExpanded(true);
|
||||
}
|
||||
|
||||
private TreeItem? AddEntry(ProtoId<GuideEntryPrototype> id, TreeItem? parent, HashSet<ProtoId<GuideEntryPrototype>> addedEntries)
|
||||
private TreeItem? AddEntry(ProtoId<GuideEntryPrototype> id,
|
||||
TreeItem? parent,
|
||||
HashSet<ProtoId<GuideEntryPrototype>> addedEntries)
|
||||
{
|
||||
if (!_entries.TryGetValue(id, out var entry))
|
||||
return null;
|
||||
@@ -179,22 +201,6 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
|
||||
return item;
|
||||
}
|
||||
|
||||
public void HandleClick(string link)
|
||||
{
|
||||
if (!_entries.TryGetValue(link, out var entry))
|
||||
return;
|
||||
|
||||
if (Tree.TryGetIndexFromMetadata(entry, out var index))
|
||||
{
|
||||
Tree.ExpandParentEntries(index.Value);
|
||||
Tree.SetSelectedIndex(index);
|
||||
}
|
||||
else
|
||||
{
|
||||
ShowGuide(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleFilter()
|
||||
{
|
||||
var emptySearch = SearchBar.Text.Trim().Length == 0;
|
||||
@@ -208,6 +214,5 @@ public sealed partial class GuidebookWindow : FancyWindow, ILinkClickHandler
|
||||
element.SetHiddenState(true, SearchBar.Text.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using Content.Client.Guidebook.Controls;
|
||||
using Content.Client.Guidebook.Richtext;
|
||||
using Content.Shared.Guidebook;
|
||||
using Pidgin;
|
||||
@@ -7,6 +8,7 @@ using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Reflection;
|
||||
using Robust.Shared.Sandboxing;
|
||||
using Robust.Shared.Utility;
|
||||
using static Pidgin.Parser;
|
||||
|
||||
namespace Content.Client.Guidebook;
|
||||
@@ -22,8 +24,10 @@ public sealed partial class DocumentParsingManager
|
||||
[Dependency] private readonly ISandboxHelper _sandboxHelper = default!;
|
||||
|
||||
private readonly Dictionary<string, Parser<char, Control>> _tagControlParsers = new();
|
||||
private Parser<char, Control> _tagParser = default!;
|
||||
private Parser<char, Control> _controlParser = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
private Parser<char, Control> _tagParser = default!;
|
||||
public Parser<char, IEnumerable<Control>> ControlParser = default!;
|
||||
|
||||
public void Initialize()
|
||||
@@ -32,7 +36,8 @@ public sealed partial class DocumentParsingManager
|
||||
.Assert(_tagControlParsers.ContainsKey, tag => $"unknown tag: {tag}")
|
||||
.Bind(tag => _tagControlParsers[tag]);
|
||||
|
||||
_controlParser = OneOf(_tagParser, TryHeaderControl, ListControlParser, TextControlParser).Before(SkipWhitespaces);
|
||||
_controlParser = OneOf(_tagParser, TryHeaderControl, ListControlParser, TextControlParser)
|
||||
.Before(SkipWhitespaces);
|
||||
|
||||
foreach (var typ in _reflectionManager.GetAllChildren<IDocumentTag>())
|
||||
{
|
||||
@@ -40,6 +45,8 @@ public sealed partial class DocumentParsingManager
|
||||
}
|
||||
|
||||
ControlParser = SkipWhitespaces.Then(_controlParser.Many());
|
||||
|
||||
_sawmill = Logger.GetSawmill("Guidebook");
|
||||
}
|
||||
|
||||
public bool TryAddMarkup(Control control, ProtoId<GuideEntryPrototype> entryId, bool log = true)
|
||||
@@ -68,37 +75,57 @@ public sealed partial class DocumentParsingManager
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (log)
|
||||
Logger.Error($"Encountered error while generating markup controls: {e}");
|
||||
_sawmill.Error($"Encountered error while generating markup controls: {e}");
|
||||
|
||||
control.AddChild(new GuidebookError(text, e.ToStringBetter()));
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private Parser<char, Control> CreateTagControlParser(string tagId, Type tagType, ISandboxHelper sandbox) => Map(
|
||||
(args, controls) =>
|
||||
{
|
||||
var tag = (IDocumentTag) sandbox.CreateInstance(tagType);
|
||||
if (!tag.TryParseTag(args, out var control))
|
||||
{
|
||||
Logger.Error($"Failed to parse {tagId} args");
|
||||
return new Control();
|
||||
}
|
||||
private Parser<char, Control> CreateTagControlParser(string tagId, Type tagType, ISandboxHelper sandbox)
|
||||
{
|
||||
return Map(
|
||||
(args, controls) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var tag = (IDocumentTag) sandbox.CreateInstance(tagType);
|
||||
if (!tag.TryParseTag(args, out var control))
|
||||
{
|
||||
_sawmill.Error($"Failed to parse {tagId} args");
|
||||
return new GuidebookError(args.ToString() ?? tagId, $"Failed to parse {tagId} args");
|
||||
}
|
||||
|
||||
foreach (var child in controls)
|
||||
{
|
||||
control.AddChild(child);
|
||||
}
|
||||
return control;
|
||||
},
|
||||
ParseTagArgs(tagId),
|
||||
TagContentParser(tagId)).Labelled($"{tagId} control");
|
||||
foreach (var child in controls)
|
||||
{
|
||||
control.AddChild(child);
|
||||
}
|
||||
|
||||
return control;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
var output = args.Aggregate(string.Empty,
|
||||
(current, pair) => current + $"{pair.Key}=\"{pair.Value}\" ");
|
||||
|
||||
_sawmill.Error($"Tag: {tagId} \n Arguments: {output}/>");
|
||||
return new GuidebookError($"Tag: {tagId}\nArguments: {output}", e.ToString());
|
||||
}
|
||||
},
|
||||
ParseTagArgs(tagId),
|
||||
TagContentParser(tagId))
|
||||
.Labelled($"{tagId} control");
|
||||
}
|
||||
|
||||
// Parse a bunch of controls until we encounter a matching closing tag.
|
||||
private Parser<char, IEnumerable<Control>> TagContentParser(string tag) =>
|
||||
OneOf(
|
||||
Try(ImmediateTagEnd).ThenReturn(Enumerable.Empty<Control>()),
|
||||
TagEnd.Then(_controlParser.Until(TryTagTerminator(tag)).Labelled($"{tag} children"))
|
||||
);
|
||||
private Parser<char, IEnumerable<Control>> TagContentParser(string tag)
|
||||
{
|
||||
return OneOf(
|
||||
Try(ImmediateTagEnd).ThenReturn(Enumerable.Empty<Control>()),
|
||||
TagEnd.Then(_controlParser.Until(TryTagTerminator(tag)).Labelled($"{tag} children"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using Content.Client.Guidebook.Controls;
|
||||
using Pidgin;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
@@ -14,92 +15,142 @@ public sealed partial class DocumentParsingManager
|
||||
{
|
||||
private const string ListBullet = " › ";
|
||||
|
||||
#region Text Parsing
|
||||
#region Basic Text Parsing
|
||||
// Try look for an escaped character. If found, skip the escaping slash and return the character.
|
||||
private static readonly Parser<char, char> TryEscapedChar = Try(Char('\\').Then(OneOf(
|
||||
Try(Char('<')),
|
||||
Try(Char('>')),
|
||||
Try(Char('\\')),
|
||||
Try(Char('-')),
|
||||
Try(Char('=')),
|
||||
Try(Char('"')),
|
||||
Try(Char(' ')),
|
||||
Try(Char('n')).ThenReturn('\n'),
|
||||
Try(Char('t')).ThenReturn('\t')
|
||||
)));
|
||||
// Parser that consumes a - and then just parses normal rich text with some prefix text (a bullet point).
|
||||
private static readonly Parser<char, char> TryEscapedChar = Try(Char('\\')
|
||||
.Then(OneOf(
|
||||
Try(Char('<')),
|
||||
Try(Char('>')),
|
||||
Try(Char('\\')),
|
||||
Try(Char('-')),
|
||||
Try(Char('=')),
|
||||
Try(Char('"')),
|
||||
Try(Char(' ')),
|
||||
Try(Char('n')).ThenReturn('\n'),
|
||||
Try(Char('t')).ThenReturn('\t')
|
||||
)));
|
||||
|
||||
private static readonly Parser<char, Unit> SkipNewline = Whitespace.SkipUntil(Char('\n'));
|
||||
|
||||
private static readonly Parser<char, char> TrySingleNewlineToSpace = Try(SkipNewline).Then(SkipWhitespaces).ThenReturn(' ');
|
||||
private static readonly Parser<char, char> TrySingleNewlineToSpace =
|
||||
Try(SkipNewline).Then(SkipWhitespaces).ThenReturn(' ');
|
||||
|
||||
private static readonly Parser<char, char> TextChar = OneOf(
|
||||
TryEscapedChar, // consume any backslashed being used to escape text
|
||||
TrySingleNewlineToSpace, // turn single newlines into spaces
|
||||
Any // just return the character.
|
||||
);
|
||||
);
|
||||
|
||||
// like TextChar, but not skipping whitespace around newlines
|
||||
private static readonly Parser<char, char> QuotedTextChar = OneOf(TryEscapedChar, Any);
|
||||
|
||||
private static readonly Parser<char, string> QuotedText =
|
||||
Char('"').Then(QuotedTextChar.Until(Try(Char('"'))).Select(string.Concat)).Labelled("quoted text");
|
||||
|
||||
private static readonly Parser<char, Unit> TryStartList =
|
||||
Try(SkipNewline.Then(SkipWhitespaces).Then(Char('-'))).Then(SkipWhitespaces);
|
||||
|
||||
private static readonly Parser<char, Unit> TryStartTag = Try(Char('<')).Then(SkipWhitespaces);
|
||||
|
||||
private static readonly Parser<char, Unit> TryStartParagraph =
|
||||
Try(SkipNewline.Then(SkipNewline)).Then(SkipWhitespaces);
|
||||
|
||||
private static readonly Parser<char, Unit> TryLookTextEnd =
|
||||
Lookahead(OneOf(TryStartTag, TryStartList, TryStartParagraph, Try(Whitespace.SkipUntil(End))));
|
||||
|
||||
private static readonly Parser<char, string> TextParser =
|
||||
TextChar.AtLeastOnceUntil(TryLookTextEnd).Select(string.Concat);
|
||||
|
||||
private static readonly Parser<char, Control> TextControlParser = Try(Map<char, string, Control>(text =>
|
||||
{
|
||||
var rt = new RichTextLabel
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
Margin = new Thickness(0, 0, 0, 15.0f)
|
||||
};
|
||||
|
||||
var msg = new FormattedMessage();
|
||||
// THANK YOU RICHTEXT VERY COOL
|
||||
// (text doesn't default to white).
|
||||
msg.PushColor(Color.White);
|
||||
|
||||
// If the parsing fails, don't throw an error and instead make an inline error message
|
||||
string? error;
|
||||
if (!msg.TryAddMarkup(text, out error))
|
||||
{
|
||||
Logger.GetSawmill("Guidebook").Error("Failed to parse RichText in Guidebook");
|
||||
|
||||
return new GuidebookError(text, error);
|
||||
}
|
||||
|
||||
msg.Pop();
|
||||
rt.SetMessage(msg);
|
||||
return rt;
|
||||
},
|
||||
TextParser)
|
||||
.Cast<Control>())
|
||||
.Labelled("richtext");
|
||||
|
||||
private static readonly Parser<char, Control> HeaderControlParser = Try(Char('#'))
|
||||
.Then(SkipWhitespaces.Then(Map(text => new Label
|
||||
{
|
||||
Text = text,
|
||||
StyleClasses = { "LabelHeadingBigger" }
|
||||
},
|
||||
AnyCharExcept('\n').AtLeastOnceString())
|
||||
.Cast<Control>()))
|
||||
.Labelled("header");
|
||||
|
||||
private static readonly Parser<char, Control> SubHeaderControlParser = Try(String("##"))
|
||||
.Then(SkipWhitespaces.Then(Map(text => new Label
|
||||
{
|
||||
Text = text,
|
||||
StyleClasses = { "LabelHeading" }
|
||||
},
|
||||
AnyCharExcept('\n').AtLeastOnceString())
|
||||
.Cast<Control>()))
|
||||
.Labelled("subheader");
|
||||
|
||||
private static readonly Parser<char, Control> TryHeaderControl = OneOf(SubHeaderControlParser, HeaderControlParser);
|
||||
|
||||
private static readonly Parser<char, Control> ListControlParser = Try(Char('-'))
|
||||
.Then(SkipWhitespaces)
|
||||
.Then(Map(
|
||||
control => new BoxContainer
|
||||
{
|
||||
Children = { new Label { Text = ListBullet, VerticalAlignment = VAlignment.Top }, control },
|
||||
Orientation = LayoutOrientation.Horizontal
|
||||
},
|
||||
TextControlParser)
|
||||
.Cast<Control>())
|
||||
.Labelled("list");
|
||||
|
||||
#region Text Parsing
|
||||
|
||||
#region Basic Text Parsing
|
||||
|
||||
// Try look for an escaped character. If found, skip the escaping slash and return the character.
|
||||
|
||||
|
||||
// like TextChar, but not skipping whitespace around newlines
|
||||
|
||||
|
||||
// Quoted text
|
||||
private static readonly Parser<char, string> QuotedText = Char('"').Then(QuotedTextChar.Until(Try(Char('"'))).Select(string.Concat)).Labelled("quoted text");
|
||||
|
||||
#endregion
|
||||
|
||||
#region rich text-end markers
|
||||
private static readonly Parser<char, Unit> TryStartList = Try(SkipNewline.Then(SkipWhitespaces).Then(Char('-'))).Then(SkipWhitespaces);
|
||||
private static readonly Parser<char, Unit> TryStartTag = Try(Char('<')).Then(SkipWhitespaces);
|
||||
private static readonly Parser<char, Unit> TryStartParagraph = Try(SkipNewline.Then(SkipNewline)).Then(SkipWhitespaces);
|
||||
private static readonly Parser<char, Unit> TryLookTextEnd = Lookahead(OneOf(TryStartTag, TryStartList, TryStartParagraph, Try(Whitespace.SkipUntil(End))));
|
||||
|
||||
#endregion
|
||||
|
||||
// parses text characters until it hits a text-end
|
||||
private static readonly Parser<char, string> TextParser = TextChar.AtLeastOnceUntil(TryLookTextEnd).Select(string.Concat);
|
||||
|
||||
private static readonly Parser<char, Control> TextControlParser = Try(Map(text =>
|
||||
{
|
||||
var rt = new RichTextLabel()
|
||||
{
|
||||
HorizontalExpand = true,
|
||||
Margin = new Thickness(0, 0, 0, 15.0f),
|
||||
};
|
||||
|
||||
var msg = new FormattedMessage();
|
||||
// THANK YOU RICHTEXT VERY COOL
|
||||
// (text doesn't default to white).
|
||||
msg.PushColor(Color.White);
|
||||
msg.AddMarkup(text);
|
||||
msg.Pop();
|
||||
rt.SetMessage(msg);
|
||||
return rt;
|
||||
}, TextParser).Cast<Control>()).Labelled("richtext");
|
||||
#endregion
|
||||
|
||||
#region Headers
|
||||
private static readonly Parser<char, Control> HeaderControlParser = Try(Char('#')).Then(SkipWhitespaces.Then(Map(text => new Label()
|
||||
{
|
||||
Text = text,
|
||||
StyleClasses = { "LabelHeadingBigger" }
|
||||
}, AnyCharExcept('\n').AtLeastOnceString()).Cast<Control>())).Labelled("header");
|
||||
|
||||
private static readonly Parser<char, Control> SubHeaderControlParser = Try(String("##")).Then(SkipWhitespaces.Then(Map(text => new Label()
|
||||
{
|
||||
Text = text,
|
||||
StyleClasses = { "LabelHeading" }
|
||||
}, AnyCharExcept('\n').AtLeastOnceString()).Cast<Control>())).Labelled("subheader");
|
||||
|
||||
private static readonly Parser<char, Control> TryHeaderControl = OneOf(SubHeaderControlParser, HeaderControlParser);
|
||||
#endregion
|
||||
|
||||
// Parser that consumes a - and then just parses normal rich text with some prefix text (a bullet point).
|
||||
private static readonly Parser<char, Control> ListControlParser = Try(Char('-')).Then(SkipWhitespaces).Then(Map(
|
||||
control => new BoxContainer()
|
||||
{
|
||||
Children = { new Label() { Text = ListBullet, VerticalAlignment = VAlignment.Top, }, control },
|
||||
Orientation = LayoutOrientation.Horizontal,
|
||||
}, TextControlParser).Cast<Control>()).Labelled("list");
|
||||
|
||||
#region Tag Parsing
|
||||
|
||||
// closing brackets for tags
|
||||
private static readonly Parser<char, Unit> TagEnd = Char('>').Then(SkipWhitespaces);
|
||||
private static readonly Parser<char, Unit> ImmediateTagEnd = String("/>").Then(SkipWhitespaces);
|
||||
@@ -107,20 +158,24 @@ public sealed partial class DocumentParsingManager
|
||||
private static readonly Parser<char, Unit> TryLookTagEnd = Lookahead(OneOf(Try(TagEnd), Try(ImmediateTagEnd)));
|
||||
|
||||
//parse tag argument key. any normal text character up until we hit a "="
|
||||
private static readonly Parser<char, string> TagArgKey = LetterOrDigit.Until(Char('=')).Select(string.Concat).Labelled("tag argument key");
|
||||
private static readonly Parser<char, string> TagArgKey =
|
||||
LetterOrDigit.Until(Char('=')).Select(string.Concat).Labelled("tag argument key");
|
||||
|
||||
// parser for a singular tag argument. Note that each TryQuoteOrChar will consume a whole quoted block before the Until() looks for whitespace
|
||||
private static readonly Parser<char, (string, string)> TagArgParser = Map((key, value) => (key, value), TagArgKey, QuotedText).Before(SkipWhitespaces);
|
||||
private static readonly Parser<char, (string, string)> TagArgParser =
|
||||
Map((key, value) => (key, value), TagArgKey, QuotedText).Before(SkipWhitespaces);
|
||||
|
||||
// parser for all tag arguments
|
||||
private static readonly Parser<char, IEnumerable<(string, string)>> TagArgsParser = TagArgParser.Until(TryLookTagEnd);
|
||||
private static readonly Parser<char, IEnumerable<(string, string)>> TagArgsParser =
|
||||
TagArgParser.Until(TryLookTagEnd);
|
||||
|
||||
// parser for an opening tag.
|
||||
private static readonly Parser<char, string> TryOpeningTag =
|
||||
Try(Char('<'))
|
||||
.Then(SkipWhitespaces)
|
||||
.Then(TextChar.Until(OneOf(Whitespace.SkipAtLeastOnce(), TryLookTagEnd)))
|
||||
.Select(string.Concat).Labelled($"opening tag");
|
||||
.Then(SkipWhitespaces)
|
||||
.Then(TextChar.Until(OneOf(Whitespace.SkipAtLeastOnce(), TryLookTagEnd)))
|
||||
.Select(string.Concat)
|
||||
.Labelled("opening tag");
|
||||
|
||||
private static Parser<char, Dictionary<string, string>> ParseTagArgs(string tag)
|
||||
{
|
||||
@@ -138,5 +193,6 @@ public sealed partial class DocumentParsingManager
|
||||
.Then(TagEnd)
|
||||
.Labelled($"closing {tag} tag");
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ public sealed class DragDropSystem : SharedDragDropSystem
|
||||
/// </summary>
|
||||
private bool _isReplaying;
|
||||
|
||||
private float _deadzone;
|
||||
public float Deadzone;
|
||||
|
||||
private DragState _state = DragState.NotDragging;
|
||||
|
||||
@@ -122,7 +122,7 @@ public sealed class DragDropSystem : SharedDragDropSystem
|
||||
|
||||
private void SetDeadZone(float deadZone)
|
||||
{
|
||||
_deadzone = deadZone;
|
||||
Deadzone = deadZone;
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
@@ -212,7 +212,7 @@ public sealed class DragDropSystem : SharedDragDropSystem
|
||||
|
||||
_draggedEntity = entity;
|
||||
_state = DragState.MouseDown;
|
||||
_mouseDownScreenPos = _inputManager.MouseScreenPosition;
|
||||
_mouseDownScreenPos = args.ScreenCoordinates;
|
||||
_mouseDownTime = 0;
|
||||
|
||||
// don't want anything else to process the click,
|
||||
@@ -240,8 +240,13 @@ public sealed class DragDropSystem : SharedDragDropSystem
|
||||
|
||||
if (TryComp<SpriteComponent>(_draggedEntity, out var draggedSprite))
|
||||
{
|
||||
var screenPos = _inputManager.MouseScreenPosition;
|
||||
// No _draggedEntity in null window (Happens in tests)
|
||||
if (!screenPos.IsValid)
|
||||
return;
|
||||
|
||||
// pop up drag shadow under mouse
|
||||
var mousePos = _eyeManager.PixelToMap(_inputManager.MouseScreenPosition);
|
||||
var mousePos = _eyeManager.PixelToMap(screenPos);
|
||||
_dragShadow = EntityManager.SpawnEntity("dragshadow", mousePos);
|
||||
var dragSprite = Comp<SpriteComponent>(_dragShadow.Value);
|
||||
dragSprite.CopyFrom(draggedSprite);
|
||||
@@ -534,7 +539,7 @@ public sealed class DragDropSystem : SharedDragDropSystem
|
||||
case DragState.MouseDown:
|
||||
{
|
||||
var screenPos = _inputManager.MouseScreenPosition;
|
||||
if ((_mouseDownScreenPos!.Value.Position - screenPos.Position).Length() > _deadzone)
|
||||
if ((_mouseDownScreenPos!.Value.Position - screenPos.Position).Length() > Deadzone)
|
||||
{
|
||||
StartDrag();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.Linq;
|
||||
using Content.Shared.Light.Components;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Animations;
|
||||
@@ -68,7 +68,7 @@ namespace Content.Client.Light.Components
|
||||
|
||||
if (MinDuration > 0)
|
||||
{
|
||||
MaxTime = (float) _random.NextDouble() * (MaxDuration - MinDuration) + MinDuration;
|
||||
MaxTime = (float)_random.NextDouble() * (MaxDuration - MinDuration) + MinDuration;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -192,11 +192,11 @@ namespace Content.Client.Light.Components
|
||||
{
|
||||
if (interpolateValue < 0.5f)
|
||||
{
|
||||
ApplyInterpolation(StartValue, EndValue, interpolateValue*2);
|
||||
ApplyInterpolation(StartValue, EndValue, interpolateValue * 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
ApplyInterpolation(EndValue, StartValue, (interpolateValue-0.5f)*2);
|
||||
ApplyInterpolation(EndValue, StartValue, (interpolateValue - 0.5f) * 2);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -238,9 +238,9 @@ namespace Content.Client.Light.Components
|
||||
|
||||
public override void OnInitialize()
|
||||
{
|
||||
_randomValue1 = (float) InterpolateLinear(StartValue, EndValue, (float) _random.NextDouble());
|
||||
_randomValue2 = (float) InterpolateLinear(StartValue, EndValue, (float) _random.NextDouble());
|
||||
_randomValue3 = (float) InterpolateLinear(StartValue, EndValue, (float) _random.NextDouble());
|
||||
_randomValue1 = (float)InterpolateLinear(StartValue, EndValue, (float)_random.NextDouble());
|
||||
_randomValue2 = (float)InterpolateLinear(StartValue, EndValue, (float)_random.NextDouble());
|
||||
_randomValue3 = (float)InterpolateLinear(StartValue, EndValue, (float)_random.NextDouble());
|
||||
}
|
||||
|
||||
public override void OnStart()
|
||||
@@ -258,7 +258,7 @@ namespace Content.Client.Light.Components
|
||||
}
|
||||
|
||||
_randomValue3 = _randomValue4;
|
||||
_randomValue4 = (float) InterpolateLinear(StartValue, EndValue, (float) _random.NextDouble());
|
||||
_randomValue4 = (float)InterpolateLinear(StartValue, EndValue, (float) _random.NextDouble());
|
||||
}
|
||||
|
||||
public override (int KeyFrameIndex, float FramePlayingTime) AdvancePlayback(
|
||||
@@ -362,7 +362,7 @@ namespace Content.Client.Light.Components
|
||||
[Dependency] private readonly IEntityManager _entMan = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
private const string KeyPrefix = nameof(LightBehaviourComponent);
|
||||
public const string KeyPrefix = nameof(LightBehaviourComponent);
|
||||
|
||||
public sealed class AnimationContainer
|
||||
{
|
||||
@@ -387,7 +387,7 @@ namespace Content.Client.Light.Components
|
||||
public readonly List<AnimationContainer> Animations = new();
|
||||
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
private Dictionary<string, object> _originalPropertyValues = new();
|
||||
public Dictionary<string, object> OriginalPropertyValues = new();
|
||||
|
||||
void ISerializationHooks.AfterDeserialization()
|
||||
{
|
||||
@@ -397,155 +397,12 @@ namespace Content.Client.Light.Components
|
||||
{
|
||||
var animation = new Animation()
|
||||
{
|
||||
AnimationTracks = {behaviour}
|
||||
AnimationTracks = { behaviour }
|
||||
};
|
||||
|
||||
Animations.Add(new AnimationContainer(key, animation, behaviour));
|
||||
key++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If we disable all the light behaviours we want to be able to revert the light to its original state.
|
||||
/// </summary>
|
||||
private void CopyLightSettings(EntityUid uid, string property)
|
||||
{
|
||||
if (_entMan.TryGetComponent(uid, out PointLightComponent? light))
|
||||
{
|
||||
var propertyValue = AnimationHelper.GetAnimatableProperty(light, property);
|
||||
if (propertyValue != null)
|
||||
{
|
||||
_originalPropertyValues.Add(property, propertyValue);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Warning($"{_entMan.GetComponent<MetaDataComponent>(uid).EntityName} has a {nameof(LightBehaviourComponent)} but it has no {nameof(PointLightComponent)}! Check the prototype!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start animating a light behaviour with the specified ID. If the specified ID is empty, it will start animating all light behaviour entries.
|
||||
/// If specified light behaviours are already animating, calling this does nothing.
|
||||
/// Multiple light behaviours can have the same ID.
|
||||
/// </summary>
|
||||
public void StartLightBehaviour(string id = "")
|
||||
{
|
||||
var uid = Owner;
|
||||
if (!_entMan.TryGetComponent(uid, out AnimationPlayerComponent? animation))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var animations = _entMan.System<AnimationPlayerSystem>();
|
||||
|
||||
foreach (var container in Animations)
|
||||
{
|
||||
if (container.LightBehaviour.ID == id || id == string.Empty)
|
||||
{
|
||||
if (!animations.HasRunningAnimation(uid, animation, KeyPrefix + container.Key))
|
||||
{
|
||||
CopyLightSettings(uid, container.LightBehaviour.Property);
|
||||
container.LightBehaviour.UpdatePlaybackValues(container.Animation);
|
||||
animations.Play(uid, animation, container.Animation, KeyPrefix + container.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If any light behaviour with the specified ID is animating, then stop it.
|
||||
/// If no ID is specified then all light behaviours will be stopped.
|
||||
/// Multiple light behaviours can have the same ID.
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="removeBehaviour">Should the behaviour(s) also be removed permanently?</param>
|
||||
/// <param name="resetToOriginalSettings">Should the light have its original settings applied?</param>
|
||||
public void StopLightBehaviour(string id = "", bool removeBehaviour = false, bool resetToOriginalSettings = false)
|
||||
{
|
||||
var uid = Owner;
|
||||
if (!_entMan.TryGetComponent(uid, out AnimationPlayerComponent? animation))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var toRemove = new List<AnimationContainer>();
|
||||
var animations = _entMan.System<AnimationPlayerSystem>();
|
||||
|
||||
foreach (var container in Animations)
|
||||
{
|
||||
if (container.LightBehaviour.ID == id || id == string.Empty)
|
||||
{
|
||||
if (animations.HasRunningAnimation(uid, animation, KeyPrefix + container.Key))
|
||||
{
|
||||
animations.Stop(uid, animation, KeyPrefix + container.Key);
|
||||
}
|
||||
|
||||
if (removeBehaviour)
|
||||
{
|
||||
toRemove.Add(container);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var container in toRemove)
|
||||
{
|
||||
Animations.Remove(container);
|
||||
}
|
||||
|
||||
if (resetToOriginalSettings && _entMan.TryGetComponent(uid, out PointLightComponent? light))
|
||||
{
|
||||
foreach (var (property, value) in _originalPropertyValues)
|
||||
{
|
||||
AnimationHelper.SetAnimatableProperty(light, property, value);
|
||||
}
|
||||
}
|
||||
|
||||
_originalPropertyValues.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if at least one behaviour is running.
|
||||
/// </summary>
|
||||
/// <returns>Whether at least one behaviour is running, false if none is.</returns>
|
||||
public bool HasRunningBehaviours()
|
||||
{
|
||||
var uid = Owner;
|
||||
if (!_entMan.TryGetComponent(uid, out AnimationPlayerComponent? animation))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var animations = _entMan.System<AnimationPlayerSystem>();
|
||||
return Animations.Any(container => animations.HasRunningAnimation(uid, animation, KeyPrefix + container.Key));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a new light behaviour to the component and start it immediately unless otherwise specified.
|
||||
/// </summary>
|
||||
public void AddNewLightBehaviour(LightBehaviourAnimationTrack behaviour, bool playImmediately = true)
|
||||
{
|
||||
var key = 0;
|
||||
|
||||
while (Animations.Any(x => x.Key == key))
|
||||
{
|
||||
key++;
|
||||
}
|
||||
|
||||
var animation = new Animation()
|
||||
{
|
||||
AnimationTracks = {behaviour}
|
||||
};
|
||||
|
||||
behaviour.Initialize(Owner, _random, _entMan);
|
||||
|
||||
var container = new AnimationContainer(key, animation, behaviour);
|
||||
Animations.Add(container);
|
||||
|
||||
if (playImmediately)
|
||||
{
|
||||
StartLightBehaviour(behaviour.ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ public sealed class ExpendableLightSystem : VisualizerSystem<ExpendableLightComp
|
||||
{
|
||||
[Dependency] private readonly PointLightSystem _pointLightSystem = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
|
||||
[Dependency] private readonly LightBehaviorSystem _lightBehavior = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -32,11 +33,11 @@ public sealed class ExpendableLightSystem : VisualizerSystem<ExpendableLightComp
|
||||
if (AppearanceSystem.TryGetData<string>(uid, ExpendableLightVisuals.Behavior, out var lightBehaviourID, args.Component)
|
||||
&& TryComp<LightBehaviourComponent>(uid, out var lightBehaviour))
|
||||
{
|
||||
lightBehaviour.StopLightBehaviour();
|
||||
_lightBehavior.StopLightBehaviour((uid, lightBehaviour));
|
||||
|
||||
if (!string.IsNullOrEmpty(lightBehaviourID))
|
||||
{
|
||||
lightBehaviour.StartLightBehaviour(lightBehaviourID);
|
||||
_lightBehavior.StartLightBehaviour((uid, lightBehaviour), lightBehaviourID);
|
||||
}
|
||||
else if (TryComp<PointLightComponent>(uid, out var light))
|
||||
{
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using System.Linq;
|
||||
using Content.Client.Light.Components;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Animations;
|
||||
|
||||
namespace Content.Client.Light.EntitySystems;
|
||||
|
||||
@@ -36,23 +38,163 @@ public sealed class LightBehaviorSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLightStartup(EntityUid uid, LightBehaviourComponent component, ComponentStartup args)
|
||||
private void OnLightStartup(Entity<LightBehaviourComponent> entity, ref ComponentStartup args)
|
||||
{
|
||||
// TODO: Do NOT ensure component here. And use eventbus events instead...
|
||||
EnsureComp<AnimationPlayerComponent>(uid);
|
||||
EnsureComp<AnimationPlayerComponent>(entity);
|
||||
|
||||
foreach (var container in component.Animations)
|
||||
foreach (var container in entity.Comp.Animations)
|
||||
{
|
||||
container.LightBehaviour.Initialize(uid, _random, EntityManager);
|
||||
container.LightBehaviour.Initialize(entity, _random, EntityManager);
|
||||
}
|
||||
|
||||
// we need to initialize all behaviours before starting any
|
||||
foreach (var container in component.Animations)
|
||||
foreach (var container in entity.Comp.Animations)
|
||||
{
|
||||
if (container.LightBehaviour.Enabled)
|
||||
{
|
||||
component.StartLightBehaviour(container.LightBehaviour.ID);
|
||||
StartLightBehaviour(entity, container.LightBehaviour.ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If we disable all the light behaviours we want to be able to revert the light to its original state.
|
||||
/// </summary>
|
||||
private void CopyLightSettings(Entity<LightBehaviourComponent> entity, string property)
|
||||
{
|
||||
if (EntityManager.TryGetComponent(entity, out PointLightComponent? light))
|
||||
{
|
||||
var propertyValue = AnimationHelper.GetAnimatableProperty(light, property);
|
||||
if (propertyValue != null)
|
||||
{
|
||||
entity.Comp.OriginalPropertyValues.Add(property, propertyValue);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning($"{EntityManager.GetComponent<MetaDataComponent>(entity).EntityName} has a {nameof(LightBehaviourComponent)} but it has no {nameof(PointLightComponent)}! Check the prototype!");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start animating a light behaviour with the specified ID. If the specified ID is empty, it will start animating all light behaviour entries.
|
||||
/// If specified light behaviours are already animating, calling this does nothing.
|
||||
/// Multiple light behaviours can have the same ID.
|
||||
/// </summary>
|
||||
public void StartLightBehaviour(Entity<LightBehaviourComponent> entity, string id = "")
|
||||
{
|
||||
if (!EntityManager.TryGetComponent(entity, out AnimationPlayerComponent? animation))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var container in entity.Comp.Animations)
|
||||
{
|
||||
if (container.LightBehaviour.ID == id || id == string.Empty)
|
||||
{
|
||||
if (!_player.HasRunningAnimation(entity, animation, LightBehaviourComponent.KeyPrefix + container.Key))
|
||||
{
|
||||
CopyLightSettings(entity, container.LightBehaviour.Property);
|
||||
container.LightBehaviour.UpdatePlaybackValues(container.Animation);
|
||||
_player.Play(entity, container.Animation, LightBehaviourComponent.KeyPrefix + container.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If any light behaviour with the specified ID is animating, then stop it.
|
||||
/// If no ID is specified then all light behaviours will be stopped.
|
||||
/// Multiple light behaviours can have the same ID.
|
||||
/// </summary>
|
||||
/// <param name="id"></param>
|
||||
/// <param name="removeBehaviour">Should the behaviour(s) also be removed permanently?</param>
|
||||
/// <param name="resetToOriginalSettings">Should the light have its original settings applied?</param>
|
||||
public void StopLightBehaviour(Entity<LightBehaviourComponent> entity, string id = "", bool removeBehaviour = false, bool resetToOriginalSettings = false)
|
||||
{
|
||||
if (!EntityManager.TryGetComponent(entity, out AnimationPlayerComponent? animation))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var comp = entity.Comp;
|
||||
|
||||
var toRemove = new List<LightBehaviourComponent.AnimationContainer>();
|
||||
|
||||
foreach (var container in comp.Animations)
|
||||
{
|
||||
if (container.LightBehaviour.ID == id || id == string.Empty)
|
||||
{
|
||||
if (_player.HasRunningAnimation(entity, animation, LightBehaviourComponent.KeyPrefix + container.Key))
|
||||
{
|
||||
_player.Stop(entity, animation, LightBehaviourComponent.KeyPrefix + container.Key);
|
||||
}
|
||||
|
||||
if (removeBehaviour)
|
||||
{
|
||||
toRemove.Add(container);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var container in toRemove)
|
||||
{
|
||||
comp.Animations.Remove(container);
|
||||
}
|
||||
|
||||
if (resetToOriginalSettings && EntityManager.TryGetComponent(entity, out PointLightComponent? light))
|
||||
{
|
||||
foreach (var (property, value) in comp.OriginalPropertyValues)
|
||||
{
|
||||
AnimationHelper.SetAnimatableProperty(light, property, value);
|
||||
}
|
||||
}
|
||||
|
||||
comp.OriginalPropertyValues.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if at least one behaviour is running.
|
||||
/// </summary>
|
||||
/// <returns>Whether at least one behaviour is running, false if none is.</returns>
|
||||
public bool HasRunningBehaviours(Entity<LightBehaviourComponent> entity)
|
||||
{
|
||||
//var uid = Owner;
|
||||
if (!EntityManager.TryGetComponent(entity, out AnimationPlayerComponent? animation))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return entity.Comp.Animations.Any(container => _player.HasRunningAnimation(entity, animation, LightBehaviourComponent.KeyPrefix + container.Key));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a new light behaviour to the component and start it immediately unless otherwise specified.
|
||||
/// </summary>
|
||||
public void AddNewLightBehaviour(Entity<LightBehaviourComponent> entity, LightBehaviourAnimationTrack behaviour, bool playImmediately = true)
|
||||
{
|
||||
var key = 0;
|
||||
var comp = entity.Comp;
|
||||
|
||||
while (comp.Animations.Any(x => x.Key == key))
|
||||
{
|
||||
key++;
|
||||
}
|
||||
|
||||
var animation = new Animation()
|
||||
{
|
||||
AnimationTracks = { behaviour }
|
||||
};
|
||||
|
||||
behaviour.Initialize(entity.Owner, _random, EntityManager);
|
||||
|
||||
var container = new LightBehaviourComponent.AnimationContainer(key, animation, behaviour);
|
||||
comp.Animations.Add(container);
|
||||
|
||||
if (playImmediately)
|
||||
{
|
||||
StartLightBehaviour(entity, behaviour.ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,15 @@ using Content.Client.Light.Components;
|
||||
using Content.Shared.Light;
|
||||
using Content.Shared.Light.Components;
|
||||
using Content.Shared.Toggleable;
|
||||
using Robust.Client.Animations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Shared.Animations;
|
||||
using Content.Client.Light.EntitySystems;
|
||||
|
||||
namespace Content.Client.Light;
|
||||
|
||||
public sealed class HandheldLightSystem : SharedHandheldLightSystem
|
||||
{
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly LightBehaviorSystem _lightBehavior = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -41,9 +41,9 @@ public sealed class HandheldLightSystem : SharedHandheldLightSystem
|
||||
if (TryComp<LightBehaviourComponent>(uid, out var lightBehaviour))
|
||||
{
|
||||
// Reset any running behaviour to reset the animated properties back to the original value, to avoid conflicts between resets
|
||||
if (lightBehaviour.HasRunningBehaviours())
|
||||
if (_lightBehavior.HasRunningBehaviours((uid, lightBehaviour)))
|
||||
{
|
||||
lightBehaviour.StopLightBehaviour(resetToOriginalSettings: true);
|
||||
_lightBehavior.StopLightBehaviour((uid, lightBehaviour), resetToOriginalSettings: true);
|
||||
}
|
||||
|
||||
if (!enabled)
|
||||
@@ -56,10 +56,10 @@ public sealed class HandheldLightSystem : SharedHandheldLightSystem
|
||||
case HandheldLightPowerStates.FullPower:
|
||||
break; // We just needed to reset all behaviours
|
||||
case HandheldLightPowerStates.LowPower:
|
||||
lightBehaviour.StartLightBehaviour(component.RadiatingBehaviourId);
|
||||
_lightBehavior.StartLightBehaviour((uid, lightBehaviour), component.RadiatingBehaviourId);
|
||||
break;
|
||||
case HandheldLightPowerStates.Dying:
|
||||
lightBehaviour.StartLightBehaviour(component.BlinkingBehaviourId);
|
||||
_lightBehavior.StartLightBehaviour((uid, lightBehaviour), component.BlinkingBehaviourId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,7 +257,7 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
|
||||
mainContainer.AddChild(jobContainer);
|
||||
|
||||
// Job icon
|
||||
if (_prototypeManager.TryIndex<StatusIconPrototype>(sensor.JobIcon, out var proto))
|
||||
if (_prototypeManager.TryIndex<JobIconPrototype>(sensor.JobIcon, out var proto))
|
||||
{
|
||||
var jobIcon = new TextureRect()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
using Content.Shared.Nutrition.Components;
|
||||
using Content.Shared.Nutrition.EntitySystems;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.Nutrition.EntitySystems;
|
||||
|
||||
public sealed class ClientFoodSequenceSystem : SharedFoodSequenceSystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<FoodSequenceStartPointComponent, AfterAutoHandleStateEvent>(OnHandleState);
|
||||
}
|
||||
|
||||
private void OnHandleState(Entity<FoodSequenceStartPointComponent> start, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
if (!TryComp<SpriteComponent>(start, out var sprite))
|
||||
return;
|
||||
|
||||
UpdateFoodVisuals(start, sprite);
|
||||
}
|
||||
|
||||
private void UpdateFoodVisuals(Entity<FoodSequenceStartPointComponent> start, SpriteComponent? sprite = null)
|
||||
{
|
||||
if (!Resolve(start, ref sprite, false))
|
||||
return;
|
||||
|
||||
//Remove old layers
|
||||
foreach (var key in start.Comp.RevealedLayers)
|
||||
{
|
||||
sprite.RemoveLayer(key);
|
||||
}
|
||||
start.Comp.RevealedLayers.Clear();
|
||||
|
||||
//Add new layers
|
||||
var counter = 0;
|
||||
foreach (var state in start.Comp.FoodLayers)
|
||||
{
|
||||
if (state.Sprite is null)
|
||||
continue;
|
||||
|
||||
counter++;
|
||||
|
||||
var keyCode = $"food-layer-{counter}";
|
||||
start.Comp.RevealedLayers.Add(keyCode);
|
||||
|
||||
var index = sprite.LayerMapReserveBlank(keyCode);
|
||||
|
||||
//Set image
|
||||
sprite.LayerSetSprite(index, state.Sprite);
|
||||
|
||||
//Offset the layer
|
||||
var layerPos = start.Comp.StartPosition;
|
||||
layerPos += start.Comp.Offset * counter;
|
||||
sprite.LayerSetOffset(index, layerPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,11 +110,15 @@ public sealed class InteractionOutlineSystem : EntitySystem
|
||||
&& _inputManager.MouseScreenPosition.IsValid)
|
||||
{
|
||||
var mousePosWorld = vp.PixelToMap(_inputManager.MouseScreenPosition.Position);
|
||||
entityToClick = screen.GetClickedEntity(mousePosWorld);
|
||||
|
||||
if (vp is ScalingViewport svp)
|
||||
{
|
||||
renderScale = svp.CurrentRenderScale;
|
||||
entityToClick = screen.GetClickedEntity(mousePosWorld, svp.Eye);
|
||||
}
|
||||
else
|
||||
{
|
||||
entityToClick = screen.GetClickedEntity(mousePosWorld);
|
||||
}
|
||||
}
|
||||
else if (_uiManager.CurrentlyHovered is EntityMenuElement element)
|
||||
|
||||
@@ -33,7 +33,7 @@ public sealed class EntityHealthBarOverlay : Overlay
|
||||
|
||||
public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
|
||||
public HashSet<string> DamageContainers = new();
|
||||
public ProtoId<StatusIconPrototype>? StatusIcon;
|
||||
public ProtoId<HealthIconPrototype>? StatusIcon;
|
||||
|
||||
public EntityHealthBarOverlay(IEntityManager entManager, IPrototypeManager prototype)
|
||||
{
|
||||
|
||||
@@ -53,17 +53,17 @@ public sealed class ShowHealthIconsSystem : EquipmentHudSystem<ShowHealthIconsCo
|
||||
args.StatusIcons.AddRange(healthIcons);
|
||||
}
|
||||
|
||||
private IReadOnlyList<StatusIconPrototype> DecideHealthIcons(Entity<DamageableComponent> entity)
|
||||
private IReadOnlyList<HealthIconPrototype> DecideHealthIcons(Entity<DamageableComponent> entity)
|
||||
{
|
||||
var damageableComponent = entity.Comp;
|
||||
|
||||
if (damageableComponent.DamageContainerID == null ||
|
||||
!DamageContainers.Contains(damageableComponent.DamageContainerID))
|
||||
{
|
||||
return Array.Empty<StatusIconPrototype>();
|
||||
return Array.Empty<HealthIconPrototype>();
|
||||
}
|
||||
|
||||
var result = new List<StatusIconPrototype>();
|
||||
var result = new List<HealthIconPrototype>();
|
||||
|
||||
// Here you could check health status, diseases, mind status, etc. and pick a good icon, or multiple depending on whatever.
|
||||
if (damageableComponent?.DamageContainerID == "Biological")
|
||||
|
||||
@@ -13,7 +13,7 @@ public sealed class ShowJobIconsSystem : EquipmentHudSystem<ShowJobIconsComponen
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
|
||||
|
||||
[ValidatePrototypeId<StatusIconPrototype>]
|
||||
[ValidatePrototypeId<JobIconPrototype>]
|
||||
private const string JobIconForNoId = "JobIconNoId";
|
||||
|
||||
public override void Initialize()
|
||||
@@ -52,7 +52,7 @@ public sealed class ShowJobIconsSystem : EquipmentHudSystem<ShowJobIconsComponen
|
||||
}
|
||||
}
|
||||
|
||||
if (_prototype.TryIndex<StatusIconPrototype>(iconId, out var iconPrototype))
|
||||
if (_prototype.TryIndex<JobIconPrototype>(iconId, out var iconPrototype))
|
||||
ev.StatusIcons.Add(iconPrototype);
|
||||
else
|
||||
Log.Error($"Invalid job icon prototype: {iconPrototype}");
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed class ShowSyndicateIconsSystem : EquipmentHudSystem<ShowSyndicateI
|
||||
if (!IsActive)
|
||||
return;
|
||||
|
||||
if (_prototype.TryIndex<StatusIconPrototype>(component.SyndStatusIcon, out var iconPrototype))
|
||||
if (_prototype.TryIndex<FactionIconPrototype>(component.SyndStatusIcon, out var iconPrototype))
|
||||
ev.StatusIcons.Add(iconPrototype);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Shared.Pinpointer;
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.Pinpointer.UI;
|
||||
@@ -23,6 +24,9 @@ public sealed class StationMapBoundUserInterface : BoundUserInterface
|
||||
|
||||
_window = this.CreateWindow<StationMapWindow>();
|
||||
_window.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName;
|
||||
_window.Set(gridUid, Owner);
|
||||
if (EntMan.TryGetComponent<StationMapComponent>(Owner, out var comp) && comp.ShowLocation)
|
||||
_window.Set(gridUid, Owner);
|
||||
else
|
||||
_window.Set(gridUid, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
using Robust.Client.UserInterface;
|
||||
|
||||
namespace Content.Client.Pinpointer.UI;
|
||||
|
||||
public sealed class UntrackedStationMapBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
[ViewVariables]
|
||||
private StationMapWindow? _window;
|
||||
|
||||
public UntrackedStationMapBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
EntityUid? gridUid = null;
|
||||
|
||||
// TODO: What this just looks like it's been copy-pasted wholesale from StationMapBoundUserInterface?
|
||||
if (EntMan.TryGetComponent<TransformComponent>(Owner, out var xform))
|
||||
{
|
||||
gridUid = xform.GridUid;
|
||||
}
|
||||
|
||||
_window = this.CreateWindow<StationMapWindow>();
|
||||
_window.Set(gridUid, Owner);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<BoxContainer
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
HorizontalExpand="True"
|
||||
Orientation="Vertical"
|
||||
>
|
||||
<customControls:HSeparator></customControls:HSeparator>
|
||||
<BoxContainer>
|
||||
<Label StyleClasses="SiliconLawPositionLabel" Name="PositionText" Margin="5 0 0 0"></Label>
|
||||
<PanelContainer
|
||||
Margin="20 10 0 0"
|
||||
MinHeight="128"
|
||||
>
|
||||
<PanelContainer.PanelOverride>
|
||||
<graphics:StyleBoxFlat BackgroundColor="#1B1B1B"></graphics:StyleBoxFlat>
|
||||
</PanelContainer.PanelOverride>
|
||||
<BoxContainer Orientation="Horizontal" SeparationOverride="5">
|
||||
<TextEdit Name="LawContent" HorizontalExpand="True" Editable="True" MinWidth="500" MinHeight="80"></TextEdit>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal" Margin="0 5 0 0" MaxHeight="64" Align="Begin">
|
||||
<Button Name="MoveUp" Text="{Loc silicon-law-ui-minus-one}" StyleClasses="OpenRight"></Button>
|
||||
<Button Name="MoveDown" Text="{Loc silicon-law-ui-plus-one}" StyleClasses="OpenLeft"></Button>
|
||||
<CheckBox Name="Corrupted" Text="{Loc silicon-law-ui-check-corrupted}" ToolTip="{Loc silicon-law-ui-check-corrupted-tooltip}"></CheckBox>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal" Align="End" Margin="0 10 5 10">
|
||||
<Button Name="Delete" Text="{Loc silicon-law-ui-delete}" ModulateSelfOverride="Red"></Button>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
@@ -0,0 +1,61 @@
|
||||
using Content.Shared.Silicons.Laws;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Silicons.Laws.SiliconLawEditUi;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class SiliconLawContainer : BoxContainer
|
||||
{
|
||||
public const string StyleClassSiliconLawPositionLabel = "SiliconLawPositionLabel";
|
||||
|
||||
public static readonly string CorruptedString =
|
||||
Loc.GetString("ion-storm-law-scrambled-number", ("length", 5));
|
||||
|
||||
private SiliconLaw? _law;
|
||||
|
||||
public event Action<SiliconLaw>? MoveLawUp;
|
||||
public event Action<SiliconLaw>? MoveLawDown;
|
||||
public event Action<SiliconLaw>? DeleteAction;
|
||||
|
||||
|
||||
public SiliconLawContainer()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
MoveUp.OnPressed += _ => MoveLawUp?.Invoke(_law!);
|
||||
MoveDown.OnPressed += _ => MoveLawDown?.Invoke(_law!);
|
||||
Corrupted.OnPressed += _ =>
|
||||
{
|
||||
if (Corrupted.Pressed)
|
||||
{
|
||||
_law!.LawIdentifierOverride = CorruptedString;
|
||||
}
|
||||
else
|
||||
{
|
||||
_law!.LawIdentifierOverride = null;
|
||||
}
|
||||
};
|
||||
|
||||
LawContent.OnTextChanged += _ => _law!.LawString = Rope.Collapse(LawContent.TextRope).Trim();
|
||||
LawContent.Placeholder = new Rope.Leaf(Loc.GetString("silicon-law-ui-placeholder"));
|
||||
Delete.OnPressed += _ => DeleteAction?.Invoke(_law!);
|
||||
}
|
||||
|
||||
public void SetLaw(SiliconLaw law)
|
||||
{
|
||||
_law = law;
|
||||
LawContent.TextRope = new Rope.Leaf(Loc.GetString(law.LawString));
|
||||
PositionText.Text = law.Order.ToString();
|
||||
if (!string.IsNullOrEmpty(law.LawIdentifierOverride))
|
||||
{
|
||||
Corrupted.Pressed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Corrupted.Pressed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using Content.Client.Eui;
|
||||
using Content.Shared.Eui;
|
||||
using Content.Shared.Silicons.Laws;
|
||||
|
||||
namespace Content.Client.Silicons.Laws.SiliconLawEditUi;
|
||||
|
||||
public sealed class SiliconLawEui : BaseEui
|
||||
{
|
||||
public readonly EntityManager _entityManager = default!;
|
||||
|
||||
private SiliconLawUi _siliconLawUi;
|
||||
private EntityUid _target;
|
||||
|
||||
public SiliconLawEui()
|
||||
{
|
||||
_entityManager = IoCManager.Resolve<EntityManager>();
|
||||
|
||||
_siliconLawUi = new SiliconLawUi();
|
||||
_siliconLawUi.OnClose += () => SendMessage(new CloseEuiMessage());
|
||||
_siliconLawUi.Save.OnPressed += _ => SendMessage(new SiliconLawsSaveMessage(_siliconLawUi.GetLaws(), _entityManager.GetNetEntity(_target)));
|
||||
}
|
||||
|
||||
public override void HandleState(EuiStateBase state)
|
||||
{
|
||||
if (state is not SiliconLawsEuiState s)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_target = _entityManager.GetEntity(s.Target);
|
||||
_siliconLawUi.SetLaws(s.Laws);
|
||||
}
|
||||
|
||||
public override void Opened()
|
||||
{
|
||||
_siliconLawUi.OpenCentered();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<controls:FancyWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="{Loc silicon-law-ui-title}"
|
||||
MinSize="560 400"
|
||||
>
|
||||
<!-->
|
||||
this shit does not layout properly unless I put the horizontal boxcontainer inside of a vertical one
|
||||
????
|
||||
<!-->
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<BoxContainer Orientation="Horizontal" Align="End">
|
||||
<Button Name="NewLawButton" Text="{Loc silicon-law-ui-new-law}" MaxSize="256 64" StyleClasses="OpenRight"></Button>
|
||||
<Button Name="Save" Text="{Loc silicon-law-ui-save}" MaxSize="256 64" Access="Public" StyleClasses="OpenLeft"></Button>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Vertical" Margin="4 60 0 0">
|
||||
<ScrollContainer VerticalExpand="True" HorizontalExpand="True" HScrollEnabled="False">
|
||||
<BoxContainer Orientation="Vertical" Name="LawContainer" Access="Public" VerticalExpand="True" />
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
@@ -0,0 +1,89 @@
|
||||
using System.Linq;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Silicons.Laws;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Silicons.Laws.SiliconLawEditUi;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class SiliconLawUi : FancyWindow
|
||||
{
|
||||
private List<SiliconLaw> _laws = new();
|
||||
|
||||
public SiliconLawUi()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
NewLawButton.OnPressed += _ => AddNewLaw();
|
||||
}
|
||||
|
||||
private void AddNewLaw()
|
||||
{
|
||||
var newLaw = new SiliconLaw();
|
||||
newLaw.Order = FixedPoint2.New(_laws.Count + 1);
|
||||
_laws.Add(newLaw);
|
||||
SetLaws(_laws);
|
||||
}
|
||||
|
||||
public void SetLaws(List<SiliconLaw> sLaws)
|
||||
{
|
||||
_laws = sLaws;
|
||||
LawContainer.RemoveAllChildren();
|
||||
foreach (var law in sLaws.OrderBy(l => l.Order))
|
||||
{
|
||||
var lawControl = new SiliconLawContainer();
|
||||
lawControl.SetLaw(law);
|
||||
lawControl.MoveLawDown += MoveLawDown;
|
||||
lawControl.MoveLawUp += MoveLawUp;
|
||||
lawControl.DeleteAction += DeleteLaw;
|
||||
|
||||
LawContainer.AddChild(lawControl);
|
||||
}
|
||||
}
|
||||
|
||||
public void DeleteLaw(SiliconLaw law)
|
||||
{
|
||||
_laws.Remove(law);
|
||||
SetLaws(_laws);
|
||||
}
|
||||
|
||||
public void MoveLawDown(SiliconLaw law)
|
||||
{
|
||||
if (_laws.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var index = _laws.IndexOf(law);
|
||||
if (index == -1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_laws[index].Order += FixedPoint2.New(1);
|
||||
SetLaws(_laws);
|
||||
}
|
||||
|
||||
public void MoveLawUp(SiliconLaw law)
|
||||
{
|
||||
if (_laws.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var index = _laws.IndexOf(law);
|
||||
if (index == -1)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_laws[index].Order += FixedPoint2.New(-1);
|
||||
SetLaws(_laws);
|
||||
}
|
||||
|
||||
public List<SiliconLaw> GetLaws()
|
||||
{
|
||||
return _laws;
|
||||
}
|
||||
}
|
||||
@@ -5,21 +5,26 @@ namespace Content.Client.Sticky.Visualizers;
|
||||
|
||||
public sealed class StickyVisualizerSystem : VisualizerSystem<StickyVisualizerComponent>
|
||||
{
|
||||
private EntityQuery<SpriteComponent> _spriteQuery;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_spriteQuery = GetEntityQuery<SpriteComponent>();
|
||||
|
||||
SubscribeLocalEvent<StickyVisualizerComponent, ComponentInit>(OnInit);
|
||||
}
|
||||
|
||||
private void OnInit(EntityUid uid, StickyVisualizerComponent component, ComponentInit args)
|
||||
private void OnInit(Entity<StickyVisualizerComponent> ent, ref ComponentInit args)
|
||||
{
|
||||
if (!TryComp(uid, out SpriteComponent? sprite))
|
||||
if (!_spriteQuery.TryComp(ent, out var sprite))
|
||||
return;
|
||||
|
||||
component.DefaultDrawDepth = sprite.DrawDepth;
|
||||
ent.Comp.OriginalDrawDepth = sprite.DrawDepth;
|
||||
}
|
||||
|
||||
protected override void OnAppearanceChange(EntityUid uid, StickyVisualizerComponent component, ref AppearanceChangeEvent args)
|
||||
protected override void OnAppearanceChange(EntityUid uid, StickyVisualizerComponent comp, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite == null)
|
||||
return;
|
||||
@@ -27,8 +32,7 @@ public sealed class StickyVisualizerSystem : VisualizerSystem<StickyVisualizerCo
|
||||
if (!AppearanceSystem.TryGetData<bool>(uid, StickyVisuals.IsStuck, out var isStuck, args.Component))
|
||||
return;
|
||||
|
||||
var drawDepth = isStuck ? component.StuckDrawDepth : component.DefaultDrawDepth;
|
||||
var drawDepth = isStuck ? comp.StuckDrawDepth : comp.OriginalDrawDepth;
|
||||
args.Sprite.DrawDepth = drawDepth;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using Content.Client.ContextMenu.UI;
|
||||
using Content.Client.Examine;
|
||||
using Content.Client.PDA;
|
||||
using Content.Client.Resources;
|
||||
using Content.Client.Silicons.Laws.SiliconLawEditUi;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Controls.FancyTree;
|
||||
using Content.Client.Verbs.UI;
|
||||
@@ -1613,6 +1614,10 @@ namespace Content.Client.Stylesheets
|
||||
{
|
||||
BackgroundColor = FancyTreeSelectedRowColor,
|
||||
}),
|
||||
|
||||
// Silicon law edit ui
|
||||
Element<Label>().Class(SiliconLawContainer.StyleClassSiliconLawPositionLabel)
|
||||
.Prop(Label.StylePropertyFontColor, NanoGold),
|
||||
// Pinned button style
|
||||
new StyleRule(
|
||||
new SelectorElement(typeof(TextureButton), new[] { StyleClassPinButtonPinned }, null, null),
|
||||
|
||||
@@ -184,10 +184,13 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
|
||||
switch (action)
|
||||
{
|
||||
case WorldTargetActionComponent mapTarget:
|
||||
return TryTargetWorld(args, actionId, mapTarget, user, comp) || !mapTarget.InteractOnMiss;
|
||||
return TryTargetWorld(args, actionId, mapTarget, user, comp) || !mapTarget.InteractOnMiss;
|
||||
|
||||
case EntityTargetActionComponent entTarget:
|
||||
return TryTargetEntity(args, actionId, entTarget, user, comp) || !entTarget.InteractOnMiss;
|
||||
return TryTargetEntity(args, actionId, entTarget, user, comp) || !entTarget.InteractOnMiss;
|
||||
|
||||
case EntityWorldTargetActionComponent entMapTarget:
|
||||
return TryTargetEntityWorld(args, actionId, entMapTarget, user, comp) || !entMapTarget.InteractOnMiss;
|
||||
|
||||
default:
|
||||
Logger.Error($"Unknown targeting action: {actionId.GetType()}");
|
||||
@@ -266,6 +269,47 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryTargetEntityWorld(in PointerInputCmdArgs args,
|
||||
EntityUid actionId,
|
||||
EntityWorldTargetActionComponent action,
|
||||
EntityUid user,
|
||||
ActionsComponent actionComp)
|
||||
{
|
||||
if (_actionsSystem == null)
|
||||
return false;
|
||||
|
||||
var entity = args.EntityUid;
|
||||
var coords = args.Coordinates;
|
||||
|
||||
if (!_actionsSystem.ValidateEntityWorldTarget(user, entity, coords, (actionId, action)))
|
||||
{
|
||||
if (action.DeselectOnMiss)
|
||||
StopTargeting();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (action.ClientExclusive)
|
||||
{
|
||||
if (action.Event != null)
|
||||
{
|
||||
action.Event.Entity = entity;
|
||||
action.Event.Coords = coords;
|
||||
action.Event.Performer = user;
|
||||
action.Event.Action = actionId;
|
||||
}
|
||||
|
||||
_actionsSystem.PerformAction(user, actionComp, actionId, action, action.Event, _timing.CurTime);
|
||||
}
|
||||
else
|
||||
EntityManager.RaisePredictiveEvent(new RequestPerformActionEvent(EntityManager.GetNetEntity(actionId), EntityManager.GetNetEntity(args.EntityUid), EntityManager.GetNetCoordinates(coords)));
|
||||
|
||||
if (!action.Repeat)
|
||||
StopTargeting();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void UnloadButton()
|
||||
{
|
||||
if (ActionButton == null)
|
||||
|
||||
@@ -9,6 +9,7 @@ using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.UnitTesting;
|
||||
|
||||
@@ -28,6 +29,12 @@ public sealed partial class TestPair
|
||||
public TestMapData? TestMap;
|
||||
private List<NetUserId> _modifiedProfiles = new();
|
||||
|
||||
private int _nextServerSeed;
|
||||
private int _nextClientSeed;
|
||||
|
||||
public int ServerSeed;
|
||||
public int ClientSeed;
|
||||
|
||||
public RobustIntegrationTest.ServerIntegrationInstance Server { get; private set; } = default!;
|
||||
public RobustIntegrationTest.ClientIntegrationInstance Client { get; private set; } = default!;
|
||||
|
||||
@@ -74,22 +81,27 @@ public sealed partial class TestPair
|
||||
await Server.WaitPost(() => gameTicker.RestartRound());
|
||||
}
|
||||
|
||||
if (settings.ShouldBeConnected)
|
||||
{
|
||||
Client.SetConnectTarget(Server);
|
||||
await Client.WaitIdleAsync();
|
||||
var netMgr = Client.ResolveDependency<IClientNetManager>();
|
||||
// Always initially connect clients to generate an initial random set of preferences/profiles.
|
||||
// This is to try and prevent issues where if the first test that connects the client is consistently some test
|
||||
// that uses a fixed seed, it would effectively prevent it from beingrandomized.
|
||||
|
||||
await Client.WaitPost(() =>
|
||||
{
|
||||
if (!netMgr.IsConnected)
|
||||
{
|
||||
netMgr.ClientConnect(null!, 0, null!);
|
||||
}
|
||||
});
|
||||
Client.SetConnectTarget(Server);
|
||||
await Client.WaitIdleAsync();
|
||||
var netMgr = Client.ResolveDependency<IClientNetManager>();
|
||||
await Client.WaitPost(() => netMgr.ClientConnect(null!, 0, null!));
|
||||
await ReallyBeIdle(10);
|
||||
await Client.WaitRunTicks(1);
|
||||
|
||||
if (!settings.ShouldBeConnected)
|
||||
{
|
||||
await Client.WaitPost(() => netMgr.ClientDisconnect("Initial disconnect"));
|
||||
await ReallyBeIdle(10);
|
||||
await Client.WaitRunTicks(1);
|
||||
}
|
||||
|
||||
var cRand = Client.ResolveDependency<IRobustRandom>();
|
||||
var sRand = Server.ResolveDependency<IRobustRandom>();
|
||||
_nextClientSeed = cRand.Next();
|
||||
_nextServerSeed = sRand.Next();
|
||||
}
|
||||
|
||||
public void Kill()
|
||||
@@ -129,4 +141,33 @@ public sealed partial class TestPair
|
||||
CleanDisposed = 2,
|
||||
Dead = 3,
|
||||
}
|
||||
|
||||
public void SetupSeed()
|
||||
{
|
||||
var sRand = Server.ResolveDependency<IRobustRandom>();
|
||||
if (Settings.ServerSeed is { } severSeed)
|
||||
{
|
||||
ServerSeed = severSeed;
|
||||
sRand.SetSeed(ServerSeed);
|
||||
}
|
||||
else
|
||||
{
|
||||
ServerSeed = _nextServerSeed;
|
||||
sRand.SetSeed(ServerSeed);
|
||||
_nextServerSeed = sRand.Next();
|
||||
}
|
||||
|
||||
var cRand = Client.ResolveDependency<IRobustRandom>();
|
||||
if (Settings.ClientSeed is { } clientSeed)
|
||||
{
|
||||
ClientSeed = clientSeed;
|
||||
cRand.SetSeed(ClientSeed);
|
||||
}
|
||||
else
|
||||
{
|
||||
ClientSeed = _nextClientSeed;
|
||||
cRand.SetSeed(ClientSeed);
|
||||
_nextClientSeed = cRand.Next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ public static partial class PoolManager
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (pair != null && pair.TestHistory.Count > 1)
|
||||
if (pair != null && pair.TestHistory.Count > 0)
|
||||
{
|
||||
await testOut.WriteLineAsync($"{nameof(GetServerClientPair)}: Pair {pair.Id} Test History Start");
|
||||
for (var i = 0; i < pair.TestHistory.Count; i++)
|
||||
@@ -268,12 +268,14 @@ public static partial class PoolManager
|
||||
var poolRetrieveTime = poolRetrieveTimeWatch.Elapsed;
|
||||
await testOut.WriteLineAsync(
|
||||
$"{nameof(GetServerClientPair)}: Retrieving pair {pair.Id} from pool took {poolRetrieveTime.TotalMilliseconds} ms");
|
||||
await testOut.WriteLineAsync(
|
||||
$"{nameof(GetServerClientPair)}: Returning pair {pair.Id}");
|
||||
|
||||
pair.ClearModifiedCvars();
|
||||
pair.Settings = poolSettings;
|
||||
pair.TestHistory.Add(currentTestName);
|
||||
pair.SetupSeed();
|
||||
await testOut.WriteLineAsync(
|
||||
$"{nameof(GetServerClientPair)}: Returning pair {pair.Id} with client/server seeds: {pair.ClientSeed}/{pair.ServerSeed}");
|
||||
|
||||
pair.Watch.Restart();
|
||||
return pair;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#nullable enable
|
||||
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
@@ -9,16 +11,6 @@ namespace Content.IntegrationTests;
|
||||
/// </summary>
|
||||
public sealed class PoolSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// If the returned pair must not be reused
|
||||
/// </summary>
|
||||
public bool MustNotBeReused => Destructive || NoLoadContent || NoLoadTestPrototypes;
|
||||
|
||||
/// <summary>
|
||||
/// If the given pair must be brand new
|
||||
/// </summary>
|
||||
public bool MustBeNew => Fresh || NoLoadContent || NoLoadTestPrototypes;
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if the test will ruin the server/client pair.
|
||||
/// </summary>
|
||||
@@ -34,8 +26,6 @@ public sealed class PoolSettings
|
||||
/// </summary>
|
||||
public bool DummyTicker { get; init; } = true;
|
||||
|
||||
public bool UseDummyTicker => !InLobby && DummyTicker;
|
||||
|
||||
/// <summary>
|
||||
/// If true, this enables the creation of admin logs during the test.
|
||||
/// </summary>
|
||||
@@ -48,8 +38,6 @@ public sealed class PoolSettings
|
||||
/// </summary>
|
||||
public bool Connected { get; init; }
|
||||
|
||||
public bool ShouldBeConnected => InLobby || Connected;
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if the given server/client pair should be in the lobby.
|
||||
/// If the pair is not in the lobby at the end of the test, this test must be marked as dirty.
|
||||
@@ -92,6 +80,34 @@ public sealed class PoolSettings
|
||||
/// </summary>
|
||||
public string? TestName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If set, this will be used to call <see cref="IRobustRandom.SetSeed"/>
|
||||
/// </summary>
|
||||
public int? ServerSeed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If set, this will be used to call <see cref="IRobustRandom.SetSeed"/>
|
||||
/// </summary>
|
||||
public int? ClientSeed { get; set; }
|
||||
|
||||
#region Inferred Properties
|
||||
|
||||
/// <summary>
|
||||
/// If the returned pair must not be reused
|
||||
/// </summary>
|
||||
public bool MustNotBeReused => Destructive || NoLoadContent || NoLoadTestPrototypes;
|
||||
|
||||
/// <summary>
|
||||
/// If the given pair must be brand new
|
||||
/// </summary>
|
||||
public bool MustBeNew => Fresh || NoLoadContent || NoLoadTestPrototypes;
|
||||
|
||||
public bool UseDummyTicker => !InLobby && DummyTicker;
|
||||
|
||||
public bool ShouldBeConnected => InLobby || Connected;
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Tries to guess if we can skip recycling the server/client pair.
|
||||
/// </summary>
|
||||
@@ -114,4 +130,4 @@ public sealed class PoolSettings
|
||||
&& Map == nextSettings.Map
|
||||
&& InLobby == nextSettings.InLobby;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ namespace Content.IntegrationTests.Tests
|
||||
var serverEntManager = server.ResolveDependency<IEntityManager>();
|
||||
var eyeManager = client.ResolveDependency<IEyeManager>();
|
||||
var spriteQuery = clientEntManager.GetEntityQuery<SpriteComponent>();
|
||||
var xformQuery = clientEntManager.GetEntityQuery<TransformComponent>();
|
||||
var eye = client.ResolveDependency<IEyeManager>().CurrentEye;
|
||||
|
||||
var testMap = await pair.CreateTestMap();
|
||||
@@ -80,9 +79,8 @@ namespace Content.IntegrationTests.Tests
|
||||
eyeManager.CurrentEye.Rotation = 0;
|
||||
|
||||
var pos = clientEntManager.System<SharedTransformSystem>().GetWorldPosition(clientEnt);
|
||||
var clickable = clientEntManager.GetComponent<ClickableComponent>(clientEnt);
|
||||
|
||||
hit = clickable.CheckClick(sprite, xformQuery.GetComponent(clientEnt), xformQuery, new Vector2(clickPosX, clickPosY) + pos, eye, out _, out _, out _);
|
||||
hit = clientEntManager.System<ClickableSystem>().CheckClick((clientEnt, null, sprite, null), new Vector2(clickPosX, clickPosY) + pos, eye, out _, out _, out _);
|
||||
});
|
||||
|
||||
await server.WaitPost(() =>
|
||||
|
||||
365
Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs
Normal file
365
Content.IntegrationTests/Tests/Commands/SuicideCommandTests.cs
Normal file
@@ -0,0 +1,365 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Execution;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Ghost;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Commands;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class SuicideCommandTests
|
||||
{
|
||||
|
||||
[TestPrototypes]
|
||||
private const string Prototypes = @"
|
||||
- type: entity
|
||||
id: SharpTestObject
|
||||
name: very sharp test object
|
||||
components:
|
||||
- type: Item
|
||||
- type: MeleeWeapon
|
||||
damage:
|
||||
types:
|
||||
Slash: 5
|
||||
- type: Execution
|
||||
|
||||
- type: entity
|
||||
id: MixedDamageTestObject
|
||||
name: mixed damage test object
|
||||
components:
|
||||
- type: Item
|
||||
- type: MeleeWeapon
|
||||
damage:
|
||||
types:
|
||||
Slash: 5
|
||||
Blunt: 5
|
||||
- type: Execution
|
||||
|
||||
- type: entity
|
||||
id: TestMaterialReclaimer
|
||||
name: test version of the material reclaimer
|
||||
components:
|
||||
- type: MaterialReclaimer";
|
||||
|
||||
/// <summary>
|
||||
/// Run the suicide command in the console
|
||||
/// Should successfully kill the player and ghost them
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task TestSuicide()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings
|
||||
{
|
||||
Connected = true,
|
||||
Dirty = true,
|
||||
DummyTicker = false
|
||||
});
|
||||
var server = pair.Server;
|
||||
var consoleHost = server.ResolveDependency<IConsoleHost>();
|
||||
var entManager = server.ResolveDependency<IEntityManager>();
|
||||
var playerMan = server.ResolveDependency<IPlayerManager>();
|
||||
var mindSystem = entManager.System<SharedMindSystem>();
|
||||
var mobStateSystem = entManager.System<MobStateSystem>();
|
||||
|
||||
// We need to know the player and whether they can be hurt, killed, and whether they have a mind
|
||||
var player = playerMan.Sessions.First().AttachedEntity!.Value;
|
||||
var mind = mindSystem.GetMind(player);
|
||||
|
||||
MindComponent mindComponent = default;
|
||||
MobStateComponent mobStateComp = default;
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
if (mind != null)
|
||||
mindComponent = entManager.GetComponent<MindComponent>(mind.Value);
|
||||
|
||||
mobStateComp = entManager.GetComponent<MobStateComponent>(player);
|
||||
});
|
||||
|
||||
|
||||
// Check that running the suicide command kills the player
|
||||
// and properly ghosts them without them being able to return to their body
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide");
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(mobStateSystem.IsDead(player, mobStateComp));
|
||||
Assert.That(entManager.TryGetComponent<GhostComponent>(mindComponent.CurrentEntity, out var ghostComp) &&
|
||||
!ghostComp.CanReturnToBody);
|
||||
});
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run the suicide command while the player is already injured
|
||||
/// This should only deal as much damage as necessary to get to the dead threshold
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task TestSuicideWhileDamaged()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings
|
||||
{
|
||||
Connected = true,
|
||||
Dirty = true,
|
||||
DummyTicker = false
|
||||
});
|
||||
var server = pair.Server;
|
||||
var consoleHost = server.ResolveDependency<IConsoleHost>();
|
||||
var entManager = server.ResolveDependency<IEntityManager>();
|
||||
var playerMan = server.ResolveDependency<IPlayerManager>();
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
var damageableSystem = entManager.System<DamageableSystem>();
|
||||
var mindSystem = entManager.System<SharedMindSystem>();
|
||||
var mobStateSystem = entManager.System<MobStateSystem>();
|
||||
|
||||
// We need to know the player and whether they can be hurt, killed, and whether they have a mind
|
||||
var player = playerMan.Sessions.First().AttachedEntity!.Value;
|
||||
var mind = mindSystem.GetMind(player);
|
||||
|
||||
MindComponent mindComponent = default;
|
||||
MobStateComponent mobStateComp = default;
|
||||
MobThresholdsComponent mobThresholdsComp = default;
|
||||
DamageableComponent damageableComp = default;
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
if (mind != null)
|
||||
mindComponent = entManager.GetComponent<MindComponent>(mind.Value);
|
||||
|
||||
mobStateComp = entManager.GetComponent<MobStateComponent>(player);
|
||||
mobThresholdsComp = entManager.GetComponent<MobThresholdsComponent>(player);
|
||||
damageableComp = entManager.GetComponent<DamageableComponent>(player);
|
||||
});
|
||||
|
||||
if (protoMan.TryIndex<DamageTypePrototype>("Slash", out var slashProto))
|
||||
damageableSystem.TryChangeDamage(player, new DamageSpecifier(slashProto, FixedPoint2.New(46.5)));
|
||||
|
||||
// Check that running the suicide command kills the player
|
||||
// and properly ghosts them without them being able to return to their body
|
||||
// and that all the damage is concentrated in the Slash category
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide");
|
||||
var lethalDamageThreshold = mobThresholdsComp.Thresholds.Keys.Last();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(mobStateSystem.IsDead(player, mobStateComp));
|
||||
Assert.That(entManager.TryGetComponent<GhostComponent>(mindComponent.CurrentEntity, out var ghostComp) &&
|
||||
!ghostComp.CanReturnToBody);
|
||||
Assert.That(damageableComp.Damage.GetTotal(), Is.EqualTo(lethalDamageThreshold));
|
||||
});
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run the suicide command in the console
|
||||
/// Should only ghost the player but not kill them
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task TestSuicideWhenCannotSuicide()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings
|
||||
{
|
||||
Connected = true,
|
||||
Dirty = true,
|
||||
DummyTicker = false
|
||||
});
|
||||
var server = pair.Server;
|
||||
var consoleHost = server.ResolveDependency<IConsoleHost>();
|
||||
var entManager = server.ResolveDependency<IEntityManager>();
|
||||
var playerMan = server.ResolveDependency<IPlayerManager>();
|
||||
var mindSystem = entManager.System<SharedMindSystem>();
|
||||
var mobStateSystem = entManager.System<MobStateSystem>();
|
||||
var tagSystem = entManager.System<TagSystem>();
|
||||
|
||||
// We need to know the player and whether they can be hurt, killed, and whether they have a mind
|
||||
var player = playerMan.Sessions.First().AttachedEntity!.Value;
|
||||
var mind = mindSystem.GetMind(player);
|
||||
MindComponent mindComponent = default;
|
||||
MobStateComponent mobStateComp = default;
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
if (mind != null)
|
||||
mindComponent = entManager.GetComponent<MindComponent>(mind.Value);
|
||||
mobStateComp = entManager.GetComponent<MobStateComponent>(player);
|
||||
});
|
||||
|
||||
tagSystem.AddTag(player, "CannotSuicide");
|
||||
|
||||
// Check that running the suicide command kills the player
|
||||
// and properly ghosts them without them being able to return to their body
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide");
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(mobStateSystem.IsAlive(player, mobStateComp));
|
||||
Assert.That(entManager.TryGetComponent<GhostComponent>(mindComponent.CurrentEntity, out var ghostComp) &&
|
||||
!ghostComp.CanReturnToBody);
|
||||
});
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Run the suicide command while the player is holding an execution-capable weapon
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task TestSuicideByHeldItem()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings
|
||||
{
|
||||
Connected = true,
|
||||
Dirty = true,
|
||||
DummyTicker = false
|
||||
});
|
||||
var server = pair.Server;
|
||||
var consoleHost = server.ResolveDependency<IConsoleHost>();
|
||||
var entManager = server.ResolveDependency<IEntityManager>();
|
||||
var playerMan = server.ResolveDependency<IPlayerManager>();
|
||||
|
||||
var handsSystem = entManager.System<SharedHandsSystem>();
|
||||
var mindSystem = entManager.System<SharedMindSystem>();
|
||||
var mobStateSystem = entManager.System<MobStateSystem>();
|
||||
var transformSystem = entManager.System<TransformSystem>();
|
||||
|
||||
// We need to know the player and whether they can be hurt, killed, and whether they have a mind
|
||||
var player = playerMan.Sessions.First().AttachedEntity!.Value;
|
||||
var mind = mindSystem.GetMind(player);
|
||||
|
||||
MindComponent mindComponent = default;
|
||||
MobStateComponent mobStateComp = default;
|
||||
MobThresholdsComponent mobThresholdsComp = default;
|
||||
DamageableComponent damageableComp = default;
|
||||
HandsComponent handsComponent = default;
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
if (mind != null)
|
||||
mindComponent = entManager.GetComponent<MindComponent>(mind.Value);
|
||||
|
||||
mobStateComp = entManager.GetComponent<MobStateComponent>(player);
|
||||
mobThresholdsComp = entManager.GetComponent<MobThresholdsComponent>(player);
|
||||
damageableComp = entManager.GetComponent<DamageableComponent>(player);
|
||||
handsComponent = entManager.GetComponent<HandsComponent>(player);
|
||||
});
|
||||
|
||||
// Spawn the weapon of choice and put it in the player's hands
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var item = entManager.SpawnEntity("SharpTestObject", transformSystem.GetMapCoordinates(player));
|
||||
Assert.That(handsSystem.TryPickup(player, item, handsComponent.ActiveHand!));
|
||||
entManager.TryGetComponent<ExecutionComponent>(item, out var executionComponent);
|
||||
Assert.That(executionComponent, Is.Not.EqualTo(null));
|
||||
});
|
||||
|
||||
// Check that running the suicide command kills the player
|
||||
// and properly ghosts them without them being able to return to their body
|
||||
// and that all the damage is concentrated in the Slash category
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide");
|
||||
var lethalDamageThreshold = mobThresholdsComp.Thresholds.Keys.Last();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(mobStateSystem.IsDead(player, mobStateComp));
|
||||
Assert.That(entManager.TryGetComponent<GhostComponent>(mindComponent.CurrentEntity, out var ghostComp) &&
|
||||
!ghostComp.CanReturnToBody);
|
||||
Assert.That(damageableComp.Damage.DamageDict["Slash"], Is.EqualTo(lethalDamageThreshold));
|
||||
});
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run the suicide command while the player is holding an execution-capable weapon
|
||||
/// with damage spread between slash and blunt
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task TestSuicideByHeldItemSpreadDamage()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient(new PoolSettings
|
||||
{
|
||||
Connected = true,
|
||||
Dirty = true,
|
||||
DummyTicker = false
|
||||
});
|
||||
var server = pair.Server;
|
||||
var consoleHost = server.ResolveDependency<IConsoleHost>();
|
||||
var entManager = server.ResolveDependency<IEntityManager>();
|
||||
var playerMan = server.ResolveDependency<IPlayerManager>();
|
||||
|
||||
var handsSystem = entManager.System<SharedHandsSystem>();
|
||||
var mindSystem = entManager.System<SharedMindSystem>();
|
||||
var mobStateSystem = entManager.System<MobStateSystem>();
|
||||
var transformSystem = entManager.System<TransformSystem>();
|
||||
|
||||
// We need to know the player and whether they can be hurt, killed, and whether they have a mind
|
||||
var player = playerMan.Sessions.First().AttachedEntity!.Value;
|
||||
var mind = mindSystem.GetMind(player);
|
||||
|
||||
MindComponent mindComponent = default;
|
||||
MobStateComponent mobStateComp = default;
|
||||
MobThresholdsComponent mobThresholdsComp = default;
|
||||
DamageableComponent damageableComp = default;
|
||||
HandsComponent handsComponent = default;
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
if (mind != null)
|
||||
mindComponent = entManager.GetComponent<MindComponent>(mind.Value);
|
||||
|
||||
mobStateComp = entManager.GetComponent<MobStateComponent>(player);
|
||||
mobThresholdsComp = entManager.GetComponent<MobThresholdsComponent>(player);
|
||||
damageableComp = entManager.GetComponent<DamageableComponent>(player);
|
||||
handsComponent = entManager.GetComponent<HandsComponent>(player);
|
||||
});
|
||||
|
||||
// Spawn the weapon of choice and put it in the player's hands
|
||||
await server.WaitPost(() =>
|
||||
{
|
||||
var item = entManager.SpawnEntity("MixedDamageTestObject", transformSystem.GetMapCoordinates(player));
|
||||
Assert.That(handsSystem.TryPickup(player, item, handsComponent.ActiveHand!));
|
||||
entManager.TryGetComponent<ExecutionComponent>(item, out var executionComponent);
|
||||
Assert.That(executionComponent, Is.Not.EqualTo(null));
|
||||
});
|
||||
|
||||
// Check that running the suicide command kills the player
|
||||
// and properly ghosts them without them being able to return to their body
|
||||
// and that slash damage is split in half
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
consoleHost.GetSessionShell(playerMan.Sessions.First()).ExecuteCommand("suicide");
|
||||
var lethalDamageThreshold = mobThresholdsComp.Thresholds.Keys.Last();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(mobStateSystem.IsDead(player, mobStateComp));
|
||||
Assert.That(entManager.TryGetComponent<GhostComponent>(mindComponent.CurrentEntity, out var ghostComp) &&
|
||||
!ghostComp.CanReturnToBody);
|
||||
Assert.That(damageableComp.Damage.DamageDict["Slash"], Is.EqualTo(lethalDamageThreshold / 2));
|
||||
});
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ using Content.Server.GameTicking;
|
||||
using Content.Server.GameTicking.Presets;
|
||||
using Content.Server.GameTicking.Rules.Components;
|
||||
using Content.Server.Mind;
|
||||
using Content.Server.Pinpointer;
|
||||
using Content.Server.Roles;
|
||||
using Content.Server.RoundEnd;
|
||||
using Content.Server.Shuttles.Components;
|
||||
@@ -18,6 +17,7 @@ using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.NPC.Systems;
|
||||
using Content.Shared.NukeOps;
|
||||
using Content.Shared.Pinpointer;
|
||||
using Content.Shared.Station.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.GameObjects;
|
||||
|
||||
@@ -1207,11 +1207,12 @@ public abstract partial class InteractionTest
|
||||
BoundKeyFunction key,
|
||||
BoundKeyState state,
|
||||
NetCoordinates? coordinates = null,
|
||||
NetEntity? cursorEntity = null)
|
||||
NetEntity? cursorEntity = null,
|
||||
ScreenCoordinates? screenCoordinates = null)
|
||||
{
|
||||
var coords = coordinates ?? TargetCoords;
|
||||
var target = cursorEntity ?? Target ?? default;
|
||||
ScreenCoordinates screen = default;
|
||||
var screen = screenCoordinates ?? default;
|
||||
|
||||
var funcId = InputManager.NetworkBindMap.KeyFunctionID(key);
|
||||
var message = new ClientFullInputCmdMessage(CTiming.CurTick, CTiming.TickFraction, funcId)
|
||||
|
||||
46
Content.IntegrationTests/Tests/Strip/StrippableTest.cs
Normal file
46
Content.IntegrationTests/Tests/Strip/StrippableTest.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using Content.Client.Interaction;
|
||||
using Content.IntegrationTests.Tests.Interaction;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Strip;
|
||||
|
||||
public sealed class StrippableTest : InteractionTest
|
||||
{
|
||||
protected override string PlayerPrototype => "MobHuman";
|
||||
|
||||
[Test]
|
||||
public async Task DragDropOpensStrip()
|
||||
{
|
||||
// Spawn one tile away
|
||||
TargetCoords = SEntMan.GetNetCoordinates(new EntityCoordinates(MapData.MapUid, 1, 0));
|
||||
await SpawnTarget("MobHuman");
|
||||
|
||||
var userInterface = Comp<UserInterfaceComponent>(Target);
|
||||
Assert.That(userInterface.Actors.Count == 0);
|
||||
|
||||
// screenCoordinates diff needs to be larger than DragDropSystem._deadzone
|
||||
var screenX = CEntMan.System<DragDropSystem>().Deadzone + 1f;
|
||||
|
||||
// Start drag
|
||||
await SetKey(EngineKeyFunctions.Use,
|
||||
BoundKeyState.Down,
|
||||
TargetCoords,
|
||||
Target,
|
||||
screenCoordinates: new ScreenCoordinates(screenX, 0f, WindowId.Main));
|
||||
|
||||
await RunTicks(5);
|
||||
|
||||
// End drag
|
||||
await SetKey(EngineKeyFunctions.Use,
|
||||
BoundKeyState.Up,
|
||||
PlayerCoords,
|
||||
Player,
|
||||
screenCoordinates: new ScreenCoordinates(0f, 0f, WindowId.Main));
|
||||
|
||||
await RunTicks(5);
|
||||
|
||||
Assert.That(userInterface.Actors.Count > 0);
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,11 @@ public static class ServerPackaging
|
||||
private static readonly List<PlatformReg> Platforms = new()
|
||||
{
|
||||
new PlatformReg("win-x64", "Windows", true),
|
||||
new PlatformReg("win-arm64", "Windows", true),
|
||||
new PlatformReg("linux-x64", "Linux", true),
|
||||
new PlatformReg("linux-arm64", "Linux", true),
|
||||
new PlatformReg("osx-x64", "MacOS", true),
|
||||
new PlatformReg("osx-arm64", "MacOS", true),
|
||||
// Non-default platforms (i.e. for Watchdog Git)
|
||||
new PlatformReg("win-x86", "Windows", false),
|
||||
new PlatformReg("linux-x86", "Linux", false),
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
using Content.Shared.StatusIcon;
|
||||
using Robust.Shared.Prototypes;
|
||||
namespace Content.Server.Access.Components;
|
||||
|
||||
namespace Content.Server.Access.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed partial class AgentIDCardComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Set of job icons that the agent ID card can show.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public HashSet<ProtoId<StatusIconPrototype>> Icons;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Allows an ID card to copy accesses from other IDs and to change the name, job title and job icon via an interface.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class AgentIDCardComponent : Component { }
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace Content.Server.Access.Systems
|
||||
if (!TryComp<IdCardComponent>(uid, out var idCard))
|
||||
return;
|
||||
|
||||
var state = new AgentIDCardBoundUserInterfaceState(idCard.FullName ?? "", idCard.JobTitle ?? "", idCard.JobIcon ?? "", component.Icons);
|
||||
var state = new AgentIDCardBoundUserInterfaceState(idCard.FullName ?? "", idCard.JobTitle ?? "", idCard.JobIcon);
|
||||
_uiSystem.SetUiState(uid, AgentIDCardUiKey.Key, state);
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ namespace Content.Server.Access.Systems
|
||||
_cardSystem.TryChangeJobDepartment(uid, job, idCard);
|
||||
}
|
||||
|
||||
private bool TryFindJobProtoFromIcon(StatusIconPrototype jobIcon, [NotNullWhen(true)] out JobPrototype? job)
|
||||
private bool TryFindJobProtoFromIcon(JobIconPrototype jobIcon, [NotNullWhen(true)] out JobPrototype? job)
|
||||
{
|
||||
foreach (var jobPrototype in _prototypeManager.EnumeratePrototypes<JobPrototype>())
|
||||
{
|
||||
|
||||
@@ -105,6 +105,31 @@ public sealed class ActionOnInteractSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
// Then EntityWorld target actions
|
||||
var entWorldOptions = GetValidActions<EntityWorldTargetActionComponent>(actionEnts, args.CanReach);
|
||||
for (var i = entWorldOptions.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var action = entWorldOptions[i];
|
||||
if (!_actions.ValidateEntityWorldTarget(args.User, args.Target, args.ClickLocation, action))
|
||||
entWorldOptions.RemoveAt(i);
|
||||
}
|
||||
|
||||
if (entWorldOptions.Count > 0)
|
||||
{
|
||||
var (entActId, entAct) = _random.Pick(entWorldOptions);
|
||||
if (entAct.Event != null)
|
||||
{
|
||||
entAct.Event.Performer = args.User;
|
||||
entAct.Event.Action = entActId;
|
||||
entAct.Event.Entity = args.Target;
|
||||
entAct.Event.Coords = args.ClickLocation;
|
||||
}
|
||||
|
||||
_actions.PerformAction(args.User, null, entActId, entAct, entAct.Event, _timing.CurTime, false);
|
||||
args.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// else: try world target actions
|
||||
var options = GetValidActions<WorldTargetActionComponent>(component.ActionEntities, args.CanReach);
|
||||
for (var i = options.Count - 1; i >= 0; i--)
|
||||
|
||||
@@ -17,7 +17,7 @@ public sealed class BanPanelCommand : LocalizedCommands
|
||||
{
|
||||
if (shell.Player is not { } player)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-banpanel-server"));
|
||||
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Content.Server.Administration.Commands
|
||||
{
|
||||
if (shell.Player is not { } player)
|
||||
{
|
||||
shell.WriteLine("shell-server-cannot");
|
||||
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,10 +17,9 @@ namespace Content.Server.Administration.Commands
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var player = shell.Player;
|
||||
if (player == null)
|
||||
if (shell.Player is not { } player)
|
||||
{
|
||||
shell.WriteLine("shell-only-players-can-run-this-command");
|
||||
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -132,6 +132,6 @@ public sealed class ExplosionCommand : IConsoleCommand
|
||||
}
|
||||
|
||||
var sysMan = IoCManager.Resolve<IEntitySystemManager>();
|
||||
sysMan.GetEntitySystem<ExplosionSystem>().QueueExplosion(coords, type.ID, intensity, slope, maxIntensity);
|
||||
sysMan.GetEntitySystem<ExplosionSystem>().QueueExplosion(coords, type.ID, intensity, slope, maxIntensity, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,9 @@ public sealed class FaxUiCommand : IConsoleCommand
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var player = shell.Player;
|
||||
if (player == null)
|
||||
if (shell.Player is not { } player)
|
||||
{
|
||||
shell.WriteLine("shell-only-players-can-run-this-command");
|
||||
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,9 @@ public sealed class FollowCommand : IConsoleCommand
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var player = shell.Player;
|
||||
if (player == null)
|
||||
if (shell.Player is not { } player)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-only-players-can-run-this-command"));
|
||||
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ public sealed class OpenAdminLogsCommand : IConsoleCommand
|
||||
{
|
||||
if (shell.Player is not { } player)
|
||||
{
|
||||
shell.WriteLine("This does not work from the server console.");
|
||||
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ public sealed class OpenAdminNotesCommand : IConsoleCommand
|
||||
{
|
||||
if (shell.Player is not { } player)
|
||||
{
|
||||
shell.WriteError("This does not work from the server console.");
|
||||
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed class OpenUserVisibleNotesCommand : IConsoleCommand
|
||||
|
||||
if (shell.Player is not { } player)
|
||||
{
|
||||
shell.WriteError("This does not work from the server console.");
|
||||
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
56
Content.Server/Administration/Commands/PlayerPanelCommand.cs
Normal file
56
Content.Server/Administration/Commands/PlayerPanelCommand.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System.Linq;
|
||||
using Content.Server.EUI;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server.Administration.Commands;
|
||||
|
||||
[AdminCommand(AdminFlags.Admin)]
|
||||
public sealed class PlayerPanelCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IPlayerLocator _locator = default!;
|
||||
[Dependency] private readonly EuiManager _euis = default!;
|
||||
[Dependency] private readonly IPlayerManager _players = default!;
|
||||
|
||||
public override string Command => "playerpanel";
|
||||
|
||||
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (shell.Player is not { } admin)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-playerpanel-server"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length != 1)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-playerpanel-invalid-arguments"));
|
||||
return;
|
||||
}
|
||||
|
||||
var queriedPlayer = await _locator.LookupIdByNameOrIdAsync(args[0]);
|
||||
|
||||
if (queriedPlayer == null)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-playerpanel-invalid-player"));
|
||||
return;
|
||||
}
|
||||
|
||||
var ui = new PlayerPanelEui(queriedPlayer);
|
||||
_euis.OpenEui(ui, admin);
|
||||
ui.SetPlayerState();
|
||||
}
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
var options = _players.Sessions.OrderBy(c => c.Name).Select(c => c.Name).ToArray();
|
||||
|
||||
return CompletionResult.FromHintOptions(options, LocalizationManager.GetString("cmd-playerpanel-completion"));
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ namespace Content.Server.Administration.Commands
|
||||
{
|
||||
if (shell.Player == null)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-only-players-can-run-this-command"));
|
||||
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
210
Content.Server/Administration/PlayerPanelEui.cs
Normal file
210
Content.Server/Administration/PlayerPanelEui.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Administration.Notes;
|
||||
using Content.Server.Administration.Systems;
|
||||
using Content.Server.Database;
|
||||
using Content.Server.EUI;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Eui;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Administration;
|
||||
|
||||
public sealed class PlayerPanelEui : BaseEui
|
||||
{
|
||||
[Dependency] private readonly IAdminManager _admins = default!;
|
||||
[Dependency] private readonly IServerDbManager _db = default!;
|
||||
[Dependency] private readonly IAdminNotesManager _notesMan = default!;
|
||||
[Dependency] private readonly IEntityManager _entity = default!;
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
[Dependency] private readonly EuiManager _eui = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLog = default!;
|
||||
|
||||
private readonly LocatedPlayerData _targetPlayer;
|
||||
private int? _notes;
|
||||
private int? _bans;
|
||||
private int? _roleBans;
|
||||
private int _sharedConnections;
|
||||
private bool? _whitelisted;
|
||||
private TimeSpan _playtime;
|
||||
private bool _frozen;
|
||||
private bool _canFreeze;
|
||||
private bool _canAhelp;
|
||||
|
||||
public PlayerPanelEui(LocatedPlayerData player)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
_targetPlayer = player;
|
||||
}
|
||||
|
||||
public override void Opened()
|
||||
{
|
||||
base.Opened();
|
||||
_admins.OnPermsChanged += OnPermsChanged;
|
||||
}
|
||||
|
||||
public override void Closed()
|
||||
{
|
||||
base.Closed();
|
||||
_admins.OnPermsChanged -= OnPermsChanged;
|
||||
}
|
||||
|
||||
public override EuiStateBase GetNewState()
|
||||
{
|
||||
return new PlayerPanelEuiState(_targetPlayer.UserId,
|
||||
_targetPlayer.Username,
|
||||
_playtime,
|
||||
_notes,
|
||||
_bans,
|
||||
_roleBans,
|
||||
_sharedConnections,
|
||||
_whitelisted,
|
||||
_canFreeze,
|
||||
_frozen,
|
||||
_canAhelp);
|
||||
}
|
||||
|
||||
private void OnPermsChanged(AdminPermsChangedEventArgs args)
|
||||
{
|
||||
if (args.Player != Player)
|
||||
return;
|
||||
|
||||
SetPlayerState();
|
||||
}
|
||||
|
||||
public override void HandleMessage(EuiMessageBase msg)
|
||||
{
|
||||
base.HandleMessage(msg);
|
||||
|
||||
ICommonSession? session;
|
||||
|
||||
switch (msg)
|
||||
{
|
||||
case PlayerPanelFreezeMessage freezeMsg:
|
||||
if (!_admins.IsAdmin(Player) ||
|
||||
!_entity.TrySystem<AdminFrozenSystem>(out var frozenSystem) ||
|
||||
!_player.TryGetSessionById(_targetPlayer.UserId, out session) ||
|
||||
session.AttachedEntity == null)
|
||||
return;
|
||||
|
||||
if (_entity.HasComponent<AdminFrozenComponent>(session.AttachedEntity))
|
||||
{
|
||||
_adminLog.Add(LogType.Action,$"{Player:actor} unfroze {_entity.ToPrettyString(session.AttachedEntity):subject}");
|
||||
_entity.RemoveComponent<AdminFrozenComponent>(session.AttachedEntity.Value);
|
||||
SetPlayerState();
|
||||
return;
|
||||
}
|
||||
|
||||
if (freezeMsg.Mute)
|
||||
{
|
||||
_adminLog.Add(LogType.Action,$"{Player:actor} froze and muted {_entity.ToPrettyString(session.AttachedEntity):subject}");
|
||||
frozenSystem.FreezeAndMute(session.AttachedEntity.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_adminLog.Add(LogType.Action,$"{Player:actor} froze {_entity.ToPrettyString(session.AttachedEntity):subject}");
|
||||
_entity.EnsureComponent<AdminFrozenComponent>(session.AttachedEntity.Value);
|
||||
}
|
||||
SetPlayerState();
|
||||
break;
|
||||
|
||||
case PlayerPanelLogsMessage:
|
||||
if (!_admins.HasAdminFlag(Player, AdminFlags.Logs))
|
||||
return;
|
||||
|
||||
_adminLog.Add(LogType.Action, $"{Player:actor} opened logs on {_targetPlayer.Username:subject}");
|
||||
var ui = new AdminLogsEui();
|
||||
_eui.OpenEui(ui, Player);
|
||||
ui.SetLogFilter(search: _targetPlayer.Username);
|
||||
break;
|
||||
case PlayerPanelDeleteMessage:
|
||||
case PlayerPanelRejuvenationMessage:
|
||||
if (!_admins.HasAdminFlag(Player, AdminFlags.Debug) ||
|
||||
!_player.TryGetSessionById(_targetPlayer.UserId, out session) ||
|
||||
session.AttachedEntity == null)
|
||||
return;
|
||||
|
||||
if (msg is PlayerPanelRejuvenationMessage)
|
||||
{
|
||||
_adminLog.Add(LogType.Action,$"{Player:actor} rejuvenated {_entity.ToPrettyString(session.AttachedEntity):subject}");
|
||||
if (!_entity.TrySystem<RejuvenateSystem>(out var rejuvenate))
|
||||
return;
|
||||
|
||||
rejuvenate.PerformRejuvenate(session.AttachedEntity.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
_adminLog.Add(LogType.Action,$"{Player:actor} deleted {_entity.ToPrettyString(session.AttachedEntity):subject}");
|
||||
_entity.DeleteEntity(session.AttachedEntity);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public async void SetPlayerState()
|
||||
{
|
||||
if (!_admins.IsAdmin(Player))
|
||||
{
|
||||
Close();
|
||||
return;
|
||||
}
|
||||
|
||||
_playtime = (await _db.GetPlayTimes(_targetPlayer.UserId))
|
||||
.Where(p => p.Tracker == "Overall")
|
||||
.Select(p => p.TimeSpent)
|
||||
.FirstOrDefault();
|
||||
|
||||
if (_notesMan.CanView(Player))
|
||||
{
|
||||
_notes = (await _notesMan.GetAllAdminRemarks(_targetPlayer.UserId)).Count;
|
||||
}
|
||||
else
|
||||
{
|
||||
_notes = null;
|
||||
}
|
||||
|
||||
_sharedConnections = _player.Sessions.Count(s => s.Channel.RemoteEndPoint.Address.Equals(_targetPlayer.LastAddress) && s.UserId != _targetPlayer.UserId);
|
||||
|
||||
// Apparently the Bans flag is also used for whitelists
|
||||
if (_admins.HasAdminFlag(Player, AdminFlags.Ban))
|
||||
{
|
||||
_whitelisted = await _db.GetWhitelistStatusAsync(_targetPlayer.UserId);
|
||||
// This won't get associated ip or hwid bans but they were not placed on this account anyways
|
||||
_bans = (await _db.GetServerBansAsync(null, _targetPlayer.UserId, null)).Count;
|
||||
// Unfortunately role bans for departments and stuff are issued individually. This means that a single role ban can have many individual role bans internally
|
||||
// The only way to distinguish whether a role ban is the same is to compare the ban time.
|
||||
// This is horrible and I would love to just erase the database and start from scratch instead but that's what I can do for now.
|
||||
_roleBans = (await _db.GetServerRoleBansAsync(null, _targetPlayer.UserId, null)).DistinctBy(rb => rb.BanTime).Count();
|
||||
}
|
||||
else
|
||||
{
|
||||
_whitelisted = null;
|
||||
_bans = null;
|
||||
_roleBans = null;
|
||||
}
|
||||
|
||||
if (_player.TryGetSessionById(_targetPlayer.UserId, out var session))
|
||||
{
|
||||
_canFreeze = session.AttachedEntity != null;
|
||||
_frozen = _entity.HasComponent<AdminFrozenComponent>(session.AttachedEntity);
|
||||
}
|
||||
else
|
||||
{
|
||||
_canFreeze = false;
|
||||
}
|
||||
|
||||
if (_admins.HasAdminFlag(Player, AdminFlags.Adminhelp))
|
||||
{
|
||||
_canAhelp = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_canAhelp = false;
|
||||
}
|
||||
|
||||
StateDirty();
|
||||
}
|
||||
}
|
||||
@@ -105,7 +105,7 @@ public sealed partial class AdminVerbSystem
|
||||
var coords = _transformSystem.GetMapCoordinates(args.Target);
|
||||
Timer.Spawn(_gameTiming.TickPeriod,
|
||||
() => _explosionSystem.QueueExplosion(coords, ExplosionSystem.DefaultExplosionPrototypeId,
|
||||
4, 1, 2, maxTileBreak: 0), // it gibs, damage doesn't need to be high.
|
||||
4, 1, 2, args.Target, maxTileBreak: 0), // it gibs, damage doesn't need to be high.
|
||||
CancellationToken.None);
|
||||
|
||||
_bodySystem.GibBody(args.Target);
|
||||
|
||||
@@ -724,15 +724,7 @@ public sealed partial class AdminVerbSystem
|
||||
if (!int.TryParse(amount, out var result))
|
||||
return;
|
||||
|
||||
if (result > 0)
|
||||
{
|
||||
ballisticAmmo.UnspawnedCount = result;
|
||||
}
|
||||
else
|
||||
{
|
||||
ballisticAmmo.UnspawnedCount = 0;
|
||||
}
|
||||
|
||||
_gun.SetBallisticUnspawned((args.Target, ballisticAmmo), result);
|
||||
_gun.UpdateBallisticAppearance(args.Target, ballisticAmmo);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -35,6 +35,9 @@ using Robust.Shared.Toolshed;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Server.Silicons.Laws;
|
||||
using Content.Shared.Silicons.Laws.Components;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using static Content.Shared.Configurable.ConfigurationComponent;
|
||||
|
||||
@@ -68,6 +71,8 @@ namespace Content.Server.Administration.Systems
|
||||
[Dependency] private readonly StationSpawningSystem _spawning = default!;
|
||||
[Dependency] private readonly ExamineSystemShared _examine = default!;
|
||||
[Dependency] private readonly AdminFrozenSystem _freeze = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly SiliconLawSystem _siliconLawSystem = default!;
|
||||
|
||||
private readonly Dictionary<ICommonSession, List<EditSolutionsEui>> _openSolutionUis = new();
|
||||
|
||||
@@ -208,6 +213,15 @@ namespace Content.Server.Administration.Systems
|
||||
ConfirmationPopup = true,
|
||||
Impact = LogImpact.High,
|
||||
});
|
||||
|
||||
// PlayerPanel
|
||||
args.Verbs.Add(new Verb
|
||||
{
|
||||
Text = Loc.GetString("admin-player-actions-player-panel"),
|
||||
Category = VerbCategory.Admin,
|
||||
Act = () => _console.ExecuteCommand(player, $"playerpanel \"{targetActor.PlayerSession.UserId}\""),
|
||||
Impact = LogImpact.Low
|
||||
});
|
||||
}
|
||||
|
||||
// Freeze
|
||||
@@ -329,6 +343,25 @@ namespace Content.Server.Administration.Systems
|
||||
Impact = LogImpact.Low
|
||||
});
|
||||
|
||||
if (TryComp<SiliconLawBoundComponent>(args.Target, out var lawBoundComponent))
|
||||
{
|
||||
args.Verbs.Add(new Verb()
|
||||
{
|
||||
Text = Loc.GetString("silicon-law-ui-verb"),
|
||||
Category = VerbCategory.Admin,
|
||||
Act = () =>
|
||||
{
|
||||
var ui = new SiliconLawEui(_siliconLawSystem, EntityManager, _adminManager);
|
||||
if (!_playerManager.TryGetSessionByEntity(args.User, out var session))
|
||||
{
|
||||
return;
|
||||
}
|
||||
_euiManager.OpenEui(ui, session);
|
||||
ui.UpdateLaws(lawBoundComponent, args.Target);
|
||||
},
|
||||
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Actions/actions_borg.rsi"), "state-laws"),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ public sealed partial class AnomalySystem
|
||||
msg.PushNewline();
|
||||
|
||||
if (secret != null && secret.Secret.Contains(AnomalySecretData.Behavior))
|
||||
msg.AddMarkup(Loc.GetString("anomaly-behavior-unknown"));
|
||||
msg.AddMarkupOrThrow(Loc.GetString("anomaly-behavior-unknown"));
|
||||
else
|
||||
{
|
||||
if (anomalyComp.CurrentBehavior != null)
|
||||
|
||||
90
Content.Server/Atmos/EntitySystems/GasMinerSystem.cs
Normal file
90
Content.Server/Atmos/EntitySystems/GasMinerSystem.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Server.Atmos.Piping.Components;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Components;
|
||||
using Content.Shared.Atmos.EntitySystems;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
|
||||
namespace Content.Server.Atmos.EntitySystems;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class GasMinerSystem : SharedGasMinerSystem
|
||||
{
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||
[Dependency] private readonly TransformSystem _transformSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<GasMinerComponent, AtmosDeviceUpdateEvent>(OnMinerUpdated);
|
||||
}
|
||||
|
||||
private void OnMinerUpdated(Entity<GasMinerComponent> ent, ref AtmosDeviceUpdateEvent args)
|
||||
{
|
||||
var miner = ent.Comp;
|
||||
var oldState = miner.MinerState;
|
||||
float toSpawn;
|
||||
|
||||
if (!GetValidEnvironment(ent, out var environment) || !Transform(ent).Anchored)
|
||||
{
|
||||
miner.MinerState = GasMinerState.Disabled;
|
||||
}
|
||||
// SpawnAmount is declared in mol/s so to get the amount of gas we hope to mine, we have to multiply this by
|
||||
// how long we have been waiting to spawn it and further cap the number according to the miner's state.
|
||||
else if ((toSpawn = CapSpawnAmount(ent, miner.SpawnAmount * args.dt, environment)) == 0)
|
||||
{
|
||||
miner.MinerState = GasMinerState.Idle;
|
||||
}
|
||||
else
|
||||
{
|
||||
miner.MinerState = GasMinerState.Working;
|
||||
|
||||
// Time to mine some gas.
|
||||
var merger = new GasMixture(1) { Temperature = miner.SpawnTemperature };
|
||||
merger.SetMoles(miner.SpawnGas, toSpawn);
|
||||
_atmosphereSystem.Merge(environment, merger);
|
||||
}
|
||||
|
||||
if (miner.MinerState != oldState)
|
||||
{
|
||||
Dirty(ent);
|
||||
}
|
||||
}
|
||||
|
||||
private bool GetValidEnvironment(Entity<GasMinerComponent> ent, [NotNullWhen(true)] out GasMixture? environment)
|
||||
{
|
||||
var (uid, miner) = ent;
|
||||
var transform = Transform(uid);
|
||||
var position = _transformSystem.GetGridOrMapTilePosition(uid, transform);
|
||||
|
||||
// Treat space as an invalid environment
|
||||
if (_atmosphereSystem.IsTileSpace(transform.GridUid, transform.MapUid, position))
|
||||
{
|
||||
environment = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
environment = _atmosphereSystem.GetContainingMixture((uid, transform), true, true);
|
||||
return environment != null;
|
||||
}
|
||||
|
||||
private float CapSpawnAmount(Entity<GasMinerComponent> ent, float toSpawnTarget, GasMixture environment)
|
||||
{
|
||||
var (uid, miner) = ent;
|
||||
|
||||
// How many moles could we theoretically spawn. Cap by pressure and amount.
|
||||
var allowableMoles = Math.Min(
|
||||
(miner.MaxExternalPressure - environment.Pressure) * environment.Volume / (miner.SpawnTemperature * Atmospherics.R),
|
||||
miner.MaxExternalAmount - environment.TotalMoles);
|
||||
|
||||
var toSpawnReal = Math.Clamp(allowableMoles, 0f, toSpawnTarget);
|
||||
|
||||
if (toSpawnReal < Atmospherics.GasMinMoles) {
|
||||
return 0f;
|
||||
}
|
||||
|
||||
return toSpawnReal;
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
using Content.Shared.Atmos;
|
||||
|
||||
namespace Content.Server.Atmos.Piping.Other.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed partial class GasMinerComponent : Component
|
||||
{
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public bool Enabled { get; set; } = true;
|
||||
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public bool Idle { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// If the number of moles in the external environment exceeds this number, no gas will be mined.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("maxExternalAmount")]
|
||||
public float MaxExternalAmount { get; set; } = float.PositiveInfinity;
|
||||
|
||||
/// <summary>
|
||||
/// If the pressure (in kPA) of the external environment exceeds this number, no gas will be mined.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("maxExternalPressure")]
|
||||
public float MaxExternalPressure { get; set; } = Atmospherics.GasMinerDefaultMaxExternalPressure;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("spawnGas")]
|
||||
public Gas? SpawnGas { get; set; } = null;
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("spawnTemperature")]
|
||||
public float SpawnTemperature { get; set; } = Atmospherics.T20C;
|
||||
|
||||
/// <summary>
|
||||
/// Number of moles created per second when the miner is working.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("spawnAmount")]
|
||||
public float SpawnAmount { get; set; } = Atmospherics.MolesCellStandard * 20f;
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Atmos.Piping.Components;
|
||||
using Content.Server.Atmos.Piping.Other.Components;
|
||||
using Content.Shared.Atmos;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
|
||||
namespace Content.Server.Atmos.Piping.Other.EntitySystems
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class GasMinerSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||
[Dependency] private readonly TransformSystem _transformSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<GasMinerComponent, AtmosDeviceUpdateEvent>(OnMinerUpdated);
|
||||
}
|
||||
|
||||
private void OnMinerUpdated(Entity<GasMinerComponent> ent, ref AtmosDeviceUpdateEvent args)
|
||||
{
|
||||
var miner = ent.Comp;
|
||||
|
||||
if (!GetValidEnvironment(ent, out var environment))
|
||||
{
|
||||
miner.Idle = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// SpawnAmount is declared in mol/s so to get the amount of gas we hope to mine, we have to multiply this by
|
||||
// how long we have been waiting to spawn it and further cap the number according to the miner's state.
|
||||
var toSpawn = CapSpawnAmount(ent, miner.SpawnAmount * args.dt, environment);
|
||||
miner.Idle = toSpawn == 0;
|
||||
if (miner.Idle || !miner.Enabled || !miner.SpawnGas.HasValue)
|
||||
return;
|
||||
|
||||
// Time to mine some gas.
|
||||
|
||||
var merger = new GasMixture(1) { Temperature = miner.SpawnTemperature };
|
||||
merger.SetMoles(miner.SpawnGas.Value, toSpawn);
|
||||
|
||||
_atmosphereSystem.Merge(environment, merger);
|
||||
}
|
||||
|
||||
private bool GetValidEnvironment(Entity<GasMinerComponent> ent, [NotNullWhen(true)] out GasMixture? environment)
|
||||
{
|
||||
var (uid, miner) = ent;
|
||||
var transform = Transform(uid);
|
||||
var position = _transformSystem.GetGridOrMapTilePosition(uid, transform);
|
||||
|
||||
// Treat space as an invalid environment
|
||||
if (_atmosphereSystem.IsTileSpace(transform.GridUid, transform.MapUid, position))
|
||||
{
|
||||
environment = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
environment = _atmosphereSystem.GetContainingMixture((uid, transform), true, true);
|
||||
return environment != null;
|
||||
}
|
||||
|
||||
private float CapSpawnAmount(Entity<GasMinerComponent> ent, float toSpawnTarget, GasMixture environment)
|
||||
{
|
||||
var (uid, miner) = ent;
|
||||
|
||||
// How many moles could we theoretically spawn. Cap by pressure and amount.
|
||||
var allowableMoles = Math.Min(
|
||||
(miner.MaxExternalPressure - environment.Pressure) * environment.Volume / (miner.SpawnTemperature * Atmospherics.R),
|
||||
miner.MaxExternalAmount - environment.TotalMoles);
|
||||
|
||||
var toSpawnReal = Math.Clamp(allowableMoles, 0f, toSpawnTarget);
|
||||
|
||||
if (toSpawnReal < Atmospherics.GasMinMoles) {
|
||||
return 0f;
|
||||
}
|
||||
|
||||
return toSpawnReal;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,12 @@ using Content.Shared.Alert;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Chemistry.Reaction;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Drunk;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Forensics;
|
||||
using Content.Shared.HealthExaminable;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Popups;
|
||||
@@ -54,6 +56,7 @@ public sealed class BloodstreamSystem : EntitySystem
|
||||
SubscribeLocalEvent<BloodstreamComponent, ReactionAttemptEvent>(OnReactionAttempt);
|
||||
SubscribeLocalEvent<BloodstreamComponent, SolutionRelayEvent<ReactionAttemptEvent>>(OnReactionAttempt);
|
||||
SubscribeLocalEvent<BloodstreamComponent, RejuvenateEvent>(OnRejuvenate);
|
||||
SubscribeLocalEvent<BloodstreamComponent, GenerateDnaEvent>(OnDnaGenerated);
|
||||
}
|
||||
|
||||
private void OnMapInit(Entity<BloodstreamComponent> ent, ref MapInitEvent args)
|
||||
@@ -183,8 +186,18 @@ public sealed class BloodstreamSystem : EntitySystem
|
||||
bloodSolution.MaxVolume = entity.Comp.BloodMaxVolume;
|
||||
tempSolution.MaxVolume = entity.Comp.BleedPuddleThreshold * 4; // give some leeway, for chemstream as well
|
||||
|
||||
// Ensure blood that should have DNA has it; must be run here, in case DnaComponent has not yet been initialized
|
||||
|
||||
if (TryComp<DnaComponent>(entity.Owner, out var donorComp) && donorComp.DNA == String.Empty)
|
||||
{
|
||||
donorComp.DNA = _forensicsSystem.GenerateDNA();
|
||||
|
||||
var ev = new GenerateDnaEvent { Owner = entity.Owner, DNA = donorComp.DNA };
|
||||
RaiseLocalEvent(entity.Owner, ref ev);
|
||||
}
|
||||
|
||||
// Fill blood solution with BLOOD
|
||||
bloodSolution.AddReagent(entity.Comp.BloodReagent, entity.Comp.BloodMaxVolume - bloodSolution.Volume);
|
||||
bloodSolution.AddReagent(new ReagentId(entity.Comp.BloodReagent, GetEntityBloodData(entity.Owner)), entity.Comp.BloodMaxVolume - bloodSolution.Volume);
|
||||
}
|
||||
|
||||
private void OnDamageChanged(Entity<BloodstreamComponent> ent, ref DamageChangedEvent args)
|
||||
@@ -242,20 +255,20 @@ public sealed class BloodstreamSystem : EntitySystem
|
||||
if (ent.Comp.BleedAmount > ent.Comp.MaxBleedAmount / 2)
|
||||
{
|
||||
args.Message.PushNewline();
|
||||
args.Message.AddMarkup(Loc.GetString("bloodstream-component-profusely-bleeding", ("target", ent.Owner)));
|
||||
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-profusely-bleeding", ("target", ent.Owner)));
|
||||
}
|
||||
// Shows bleeding message when bleeding, but less than profusely.
|
||||
else if (ent.Comp.BleedAmount > 0)
|
||||
{
|
||||
args.Message.PushNewline();
|
||||
args.Message.AddMarkup(Loc.GetString("bloodstream-component-bleeding", ("target", ent.Owner)));
|
||||
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-bleeding", ("target", ent.Owner)));
|
||||
}
|
||||
|
||||
// If the mob's blood level is below the damage threshhold, the pale message is added.
|
||||
if (GetBloodLevelPercentage(ent, ent) < ent.Comp.BloodlossThreshold)
|
||||
{
|
||||
args.Message.PushNewline();
|
||||
args.Message.AddMarkup(Loc.GetString("bloodstream-component-looks-pale", ("target", ent.Owner)));
|
||||
args.Message.AddMarkupOrThrow(Loc.GetString("bloodstream-component-looks-pale", ("target", ent.Owner)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,7 +362,7 @@ public sealed class BloodstreamSystem : EntitySystem
|
||||
}
|
||||
|
||||
if (amount >= 0)
|
||||
return _solutionContainerSystem.TryAddReagent(component.BloodSolution.Value, component.BloodReagent, amount, out _);
|
||||
return _solutionContainerSystem.TryAddReagent(component.BloodSolution.Value, component.BloodReagent, amount, null, GetEntityBloodData(uid));
|
||||
|
||||
// Removal is more involved,
|
||||
// since we also wanna handle moving it to the temporary solution
|
||||
@@ -370,10 +383,7 @@ public sealed class BloodstreamSystem : EntitySystem
|
||||
tempSolution.AddSolution(temp, _prototypeManager);
|
||||
}
|
||||
|
||||
if (_puddleSystem.TrySpillAt(uid, tempSolution, out var puddleUid, sound: false))
|
||||
{
|
||||
_forensicsSystem.TransferDna(puddleUid, uid, canDnaBeCleaned: false);
|
||||
}
|
||||
_puddleSystem.TrySpillAt(uid, tempSolution, out var puddleUid, sound: false);
|
||||
|
||||
tempSolution.RemoveAllSolution();
|
||||
}
|
||||
@@ -436,10 +446,7 @@ public sealed class BloodstreamSystem : EntitySystem
|
||||
_solutionContainerSystem.RemoveAllSolution(component.TemporarySolution.Value);
|
||||
}
|
||||
|
||||
if (_puddleSystem.TrySpillAt(uid, tempSol, out var puddleUid))
|
||||
{
|
||||
_forensicsSystem.TransferDna(puddleUid, uid, canDnaBeCleaned: false);
|
||||
}
|
||||
_puddleSystem.TrySpillAt(uid, tempSol, out var puddleUid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -464,6 +471,40 @@ public sealed class BloodstreamSystem : EntitySystem
|
||||
component.BloodReagent = reagent;
|
||||
|
||||
if (currentVolume > 0)
|
||||
_solutionContainerSystem.TryAddReagent(component.BloodSolution.Value, component.BloodReagent, currentVolume, out _);
|
||||
_solutionContainerSystem.TryAddReagent(component.BloodSolution.Value, component.BloodReagent, currentVolume, null, GetEntityBloodData(uid));
|
||||
}
|
||||
|
||||
private void OnDnaGenerated(Entity<BloodstreamComponent> entity, ref GenerateDnaEvent args)
|
||||
{
|
||||
if (_solutionContainerSystem.ResolveSolution(entity.Owner, entity.Comp.BloodSolutionName, ref entity.Comp.BloodSolution, out var bloodSolution))
|
||||
{
|
||||
foreach (var reagent in bloodSolution.Contents)
|
||||
{
|
||||
List<ReagentData> reagentData = reagent.Reagent.EnsureReagentData();
|
||||
reagentData.RemoveAll(x => x is DnaData);
|
||||
reagentData.AddRange(GetEntityBloodData(entity.Owner));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the reagent data for blood that a specific entity should have.
|
||||
/// </summary>
|
||||
public List<ReagentData> GetEntityBloodData(EntityUid uid)
|
||||
{
|
||||
var bloodData = new List<ReagentData>();
|
||||
var dnaData = new DnaData();
|
||||
|
||||
if (TryComp<DnaComponent>(uid, out var donorComp))
|
||||
{
|
||||
dnaData.DNA = donorComp.DNA;
|
||||
} else
|
||||
{
|
||||
dnaData.DNA = Loc.GetString("forensics-dna-unknown");
|
||||
}
|
||||
|
||||
bloodData.Add(dnaData);
|
||||
|
||||
return bloodData;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ public sealed class BotanySwabSystem : EntitySystem
|
||||
{
|
||||
Broadcast = true,
|
||||
BreakOnMove = true,
|
||||
NeedHand = true
|
||||
NeedHand = true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ public sealed class MutationSystem : EntitySystem
|
||||
}
|
||||
|
||||
// Add up everything in the bits column and put the number here.
|
||||
const int totalbits = 275;
|
||||
const int totalbits = 262;
|
||||
|
||||
#pragma warning disable IDE0055 // disable formatting warnings because this looks more readable
|
||||
// Tolerances (55)
|
||||
@@ -65,10 +65,10 @@ public sealed class MutationSystem : EntitySystem
|
||||
// Kill the plant (30)
|
||||
MutateBool(ref seed.Viable , false, 30, totalbits, severity);
|
||||
|
||||
// Fun (90)
|
||||
// Fun (72)
|
||||
MutateBool(ref seed.Seedless , true , 10, totalbits, severity);
|
||||
MutateBool(ref seed.Slip , true , 10, totalbits, severity);
|
||||
MutateBool(ref seed.Sentient , true , 10, totalbits, severity);
|
||||
MutateBool(ref seed.Sentient , true , 2 , totalbits, severity);
|
||||
MutateBool(ref seed.Ligneous , true , 10, totalbits, severity);
|
||||
MutateBool(ref seed.Bioluminescent, true , 10, totalbits, severity);
|
||||
MutateBool(ref seed.TurnIntoKudzu , true , 10, totalbits, severity);
|
||||
@@ -115,10 +115,10 @@ public sealed class MutationSystem : EntitySystem
|
||||
CrossFloat(ref result.Production, a.Production);
|
||||
CrossFloat(ref result.Potency, a.Potency);
|
||||
|
||||
// we do not transfer Sentient to another plant to avoid ghost role spam
|
||||
CrossBool(ref result.Seedless, a.Seedless);
|
||||
CrossBool(ref result.Viable, a.Viable);
|
||||
CrossBool(ref result.Slip, a.Slip);
|
||||
CrossBool(ref result.Sentient, a.Sentient);
|
||||
CrossBool(ref result.Ligneous, a.Ligneous);
|
||||
CrossBool(ref result.Bioluminescent, a.Bioluminescent);
|
||||
CrossBool(ref result.TurnIntoKudzu, a.TurnIntoKudzu);
|
||||
|
||||
@@ -298,8 +298,17 @@ public sealed class PlantHolderSystem : EntitySystem
|
||||
{
|
||||
healthOverride = component.Health;
|
||||
}
|
||||
component.Seed.Unique = false;
|
||||
var seed = _botany.SpawnSeedPacket(component.Seed, Transform(args.User).Coordinates, args.User, healthOverride);
|
||||
var packetSeed = component.Seed;
|
||||
if (packetSeed.Sentient)
|
||||
{
|
||||
packetSeed = packetSeed.Clone(); // clone before modifying the seed
|
||||
packetSeed.Sentient = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
packetSeed.Unique = false;
|
||||
}
|
||||
var seed = _botany.SpawnSeedPacket(packetSeed, Transform(args.User).Coordinates, args.User, healthOverride);
|
||||
_randomHelper.RandomOffset(seed, 0.25f);
|
||||
var displayName = Loc.GetString(component.Seed.DisplayName);
|
||||
_popup.PopupCursor(Loc.GetString("plant-holder-component-take-sample-message",
|
||||
@@ -626,8 +635,15 @@ public sealed class PlantHolderSystem : EntitySystem
|
||||
}
|
||||
else if (component.Age < 0) // Revert back to seed packet!
|
||||
{
|
||||
var packetSeed = component.Seed;
|
||||
if (packetSeed.Sentient)
|
||||
{
|
||||
if (!packetSeed.Unique) // clone if necessary before modifying the seed
|
||||
packetSeed = packetSeed.Clone();
|
||||
packetSeed.Sentient = false; // remove Sentient to avoid ghost role spam
|
||||
}
|
||||
// will put it in the trays hands if it has any, please do not try doing this
|
||||
_botany.SpawnSeedPacket(component.Seed, Transform(uid).Coordinates, uid);
|
||||
_botany.SpawnSeedPacket(packetSeed, Transform(uid).Coordinates, uid);
|
||||
RemovePlant(uid, component);
|
||||
component.ForceUpdate = true;
|
||||
Update(uid, component);
|
||||
|
||||
@@ -42,12 +42,19 @@ public sealed class SeedExtractorSystem : EntitySystem
|
||||
var amount = _random.Next(seedExtractor.BaseMinSeeds, seedExtractor.BaseMaxSeeds + 1);
|
||||
var coords = Transform(uid).Coordinates;
|
||||
|
||||
var packetSeed = seed;
|
||||
if (packetSeed.Sentient)
|
||||
{
|
||||
if (!packetSeed.Unique) // clone if necessary before modifying the seed
|
||||
packetSeed = packetSeed.Clone();
|
||||
packetSeed.Sentient = false; // remove Sentient to avoid ghost role spam
|
||||
}
|
||||
if (amount > 1)
|
||||
seed.Unique = false;
|
||||
packetSeed.Unique = false;
|
||||
|
||||
for (var i = 0; i < amount; i++)
|
||||
{
|
||||
_botanySystem.SpawnSeedPacket(seed, coords, args.User);
|
||||
_botanySystem.SpawnSeedPacket(packetSeed, coords, args.User);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace Content.Server.Chat.Commands
|
||||
{
|
||||
if (shell.Player is not { } player)
|
||||
{
|
||||
shell.WriteError("This command cannot be run from the server.");
|
||||
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Content.Server.Chat.Commands
|
||||
{
|
||||
if (shell.Player is not { } player)
|
||||
{
|
||||
shell.WriteError("This command cannot be run from the server.");
|
||||
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Mind;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Enums;
|
||||
@@ -22,7 +23,7 @@ namespace Content.Server.Chat.Commands
|
||||
{
|
||||
if (shell.Player is not { } player)
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("shell-cannot-run-command-from-server"));
|
||||
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -32,15 +33,13 @@ namespace Content.Server.Chat.Commands
|
||||
var minds = _e.System<SharedMindSystem>();
|
||||
|
||||
// This check also proves mind not-null for at the end when the mob is ghosted.
|
||||
if (!minds.TryGetMind(player, out var mindId, out var mind) ||
|
||||
mind.OwnedEntity is not { Valid: true } victim)
|
||||
if (!minds.TryGetMind(player, out var mindId, out var mindComp) ||
|
||||
mindComp.OwnedEntity is not { Valid: true } victim)
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("suicide-command-no-mind"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var gameTicker = _e.System<GameTicker>();
|
||||
var suicideSystem = _e.System<SuicideSystem>();
|
||||
|
||||
if (_e.HasComponent<AdminFrozenComponent>(victim))
|
||||
@@ -53,14 +52,6 @@ namespace Content.Server.Chat.Commands
|
||||
}
|
||||
|
||||
if (suicideSystem.Suicide(victim))
|
||||
{
|
||||
// Prevent the player from returning to the body.
|
||||
// Note that mind cannot be null because otherwise victim would be null.
|
||||
gameTicker.OnGhostAttempt(mindId, false, mind: mind);
|
||||
return;
|
||||
}
|
||||
|
||||
if (gameTicker.OnGhostAttempt(mindId, true, mind: mind))
|
||||
return;
|
||||
|
||||
shell.WriteLine(Loc.GetString("ghost-command-denied"));
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Content.Server.Chat.Commands
|
||||
{
|
||||
if (shell.Player is not { } player)
|
||||
{
|
||||
shell.WriteError("This command cannot be run from the server.");
|
||||
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -267,7 +267,7 @@ namespace Content.Server.Chat.Managers
|
||||
|
||||
//TODO: player.Name color, this will need to change the structure of the MsgChatMessage
|
||||
ChatMessageToAll(ChatChannel.OOC, message, wrappedMessage, EntityUid.Invalid, hideChat: false, recordReplay: true, colorOverride: colorOverride, author: player.UserId);
|
||||
_mommiLink.SendOOCMessage(player.Name, message);
|
||||
_mommiLink.SendOOCMessage(player.Name, message.Replace("@", "\\@").Replace("<", "\\<").Replace("/", "\\/")); // @ and < are both problematic for discord due to pinging. / is sanitized solely to kneecap links to murder embeds via blunt force
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"OOC from {player:Player}: {message}");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,141 +1,154 @@
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Item;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.Mind.Components;
|
||||
|
||||
namespace Content.Server.Chat
|
||||
namespace Content.Server.Chat;
|
||||
|
||||
public sealed class SuicideSystem : EntitySystem
|
||||
{
|
||||
public sealed class SuicideSystem : EntitySystem
|
||||
[Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!;
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly TagSystem _tagSystem = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly GameTicker _gameTicker = default!;
|
||||
[Dependency] private readonly SharedSuicideSystem _suicide = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
|
||||
[Dependency] private readonly EntityLookupSystem _entityLookupSystem = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly TagSystem _tagSystem = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobState = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
base.Initialize();
|
||||
|
||||
public bool Suicide(EntityUid victim)
|
||||
{
|
||||
// Checks to see if the CannotSuicide tag exits, ghosts instead.
|
||||
if (_tagSystem.HasTag(victim, "CannotSuicide"))
|
||||
return false;
|
||||
|
||||
// Checks to see if the player is dead.
|
||||
if (!TryComp<MobStateComponent>(victim, out var mobState) || _mobState.IsDead(victim, mobState))
|
||||
return false;
|
||||
|
||||
_adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(victim):player} is attempting to suicide");
|
||||
|
||||
var suicideEvent = new SuicideEvent(victim);
|
||||
|
||||
//Check to see if there were any systems blocking this suicide
|
||||
if (SuicideAttemptBlocked(victim, suicideEvent))
|
||||
return false;
|
||||
|
||||
bool environmentSuicide = false;
|
||||
// If you are critical, you wouldn't be able to use your surroundings to suicide, so you do the default suicide
|
||||
if (!_mobState.IsCritical(victim, mobState))
|
||||
{
|
||||
environmentSuicide = EnvironmentSuicideHandler(victim, suicideEvent);
|
||||
}
|
||||
|
||||
if (suicideEvent.AttemptBlocked)
|
||||
return false;
|
||||
|
||||
DefaultSuicideHandler(victim, suicideEvent);
|
||||
|
||||
ApplyDeath(victim, suicideEvent.Kind!.Value);
|
||||
_adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(victim):player} suicided{(environmentSuicide ? " (environment)" : "")}");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If not handled, does the default suicide, which is biting your own tongue
|
||||
/// </summary>
|
||||
private void DefaultSuicideHandler(EntityUid victim, SuicideEvent suicideEvent)
|
||||
{
|
||||
if (suicideEvent.Handled)
|
||||
return;
|
||||
|
||||
var othersMessage = Loc.GetString("suicide-command-default-text-others", ("name", victim));
|
||||
_popup.PopupEntity(othersMessage, victim, Filter.PvsExcept(victim), true);
|
||||
|
||||
var selfMessage = Loc.GetString("suicide-command-default-text-self");
|
||||
_popup.PopupEntity(selfMessage, victim, victim);
|
||||
suicideEvent.SetHandled(SuicideKind.Bloodloss);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks to see if there are any other systems that prevent suicide
|
||||
/// </summary>
|
||||
/// <returns>Returns true if there was a blocked attempt</returns>
|
||||
private bool SuicideAttemptBlocked(EntityUid victim, SuicideEvent suicideEvent)
|
||||
{
|
||||
RaiseLocalEvent(victim, suicideEvent, true);
|
||||
|
||||
if (suicideEvent.AttemptBlocked)
|
||||
return true;
|
||||
SubscribeLocalEvent<DamageableComponent, SuicideEvent>(OnDamageableSuicide);
|
||||
SubscribeLocalEvent<MobStateComponent, SuicideEvent>(OnEnvironmentalSuicide);
|
||||
SubscribeLocalEvent<MindContainerComponent, SuicideGhostEvent>(OnSuicideGhost);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calling this function will attempt to kill the user by suiciding on objects in the surrounding area
|
||||
/// or by applying a lethal amount of damage to the user with the default method.
|
||||
/// Used when writing /suicide
|
||||
/// </summary>
|
||||
public bool Suicide(EntityUid victim)
|
||||
{
|
||||
// Can't suicide if we're already dead
|
||||
if (!TryComp<MobStateComponent>(victim, out var mobState) || _mobState.IsDead(victim, mobState))
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raise event to attempt to use held item, or surrounding entities to attempt to commit suicide
|
||||
/// </summary>
|
||||
private bool EnvironmentSuicideHandler(EntityUid victim, SuicideEvent suicideEvent)
|
||||
{
|
||||
var itemQuery = GetEntityQuery<ItemComponent>();
|
||||
|
||||
// Suicide by held item
|
||||
if (EntityManager.TryGetComponent(victim, out HandsComponent? handsComponent)
|
||||
&& handsComponent.ActiveHandEntity is { } item)
|
||||
{
|
||||
RaiseLocalEvent(item, suicideEvent, false);
|
||||
|
||||
if (suicideEvent.Handled)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Suicide by nearby entity (ex: Microwave)
|
||||
foreach (var entity in _entityLookupSystem.GetEntitiesInRange(victim, 1, LookupFlags.Approximate | LookupFlags.Static))
|
||||
{
|
||||
// Skip any nearby items that can be picked up, we already checked the active held item above
|
||||
if (itemQuery.HasComponent(entity))
|
||||
continue;
|
||||
|
||||
RaiseLocalEvent(entity, suicideEvent);
|
||||
|
||||
if (suicideEvent.Handled)
|
||||
return true;
|
||||
}
|
||||
var suicideGhostEvent = new SuicideGhostEvent(victim);
|
||||
RaiseLocalEvent(victim, suicideGhostEvent);
|
||||
|
||||
// Suicide is considered a fail if the user wasn't able to ghost
|
||||
// Suiciding with the CannotSuicide tag will ghost the player but not kill the body
|
||||
if (!suicideGhostEvent.Handled || _tagSystem.HasTag(victim, "CannotSuicide"))
|
||||
return false;
|
||||
|
||||
_adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(victim):player} is attempting to suicide");
|
||||
var suicideEvent = new SuicideEvent(victim);
|
||||
RaiseLocalEvent(victim, suicideEvent);
|
||||
|
||||
_adminLogger.Add(LogType.Mind, $"{EntityManager.ToPrettyString(victim):player} suicided.");
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event subscription created to handle the ghosting aspect relating to suicides
|
||||
/// Mainly useful when you can raise an event in Shared and can't call Suicide() directly
|
||||
/// </summary>
|
||||
private void OnSuicideGhost(Entity<MindContainerComponent> victim, ref SuicideGhostEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
if (victim.Comp.Mind == null)
|
||||
return;
|
||||
|
||||
if (!TryComp<MindComponent>(victim.Comp.Mind, out var mindComponent))
|
||||
return;
|
||||
|
||||
// CannotSuicide tag will allow the user to ghost, but also return to their mind
|
||||
// This is kind of weird, not sure what it applies to?
|
||||
if (_tagSystem.HasTag(victim, "CannotSuicide"))
|
||||
args.CanReturnToBody = true;
|
||||
|
||||
if (_gameTicker.OnGhostAttempt(victim.Comp.Mind.Value, args.CanReturnToBody, mind: mindComponent))
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raise event to attempt to use held item, or surrounding entities to attempt to commit suicide
|
||||
/// </summary>
|
||||
private void OnEnvironmentalSuicide(Entity<MobStateComponent> victim, ref SuicideEvent args)
|
||||
{
|
||||
if (args.Handled || _mobState.IsCritical(victim))
|
||||
return;
|
||||
|
||||
var suicideByEnvironmentEvent = new SuicideByEnvironmentEvent(victim);
|
||||
|
||||
// Try to suicide by raising an event on the held item
|
||||
if (EntityManager.TryGetComponent(victim, out HandsComponent? handsComponent)
|
||||
&& handsComponent.ActiveHandEntity is { } item)
|
||||
{
|
||||
RaiseLocalEvent(item, suicideByEnvironmentEvent);
|
||||
if (suicideByEnvironmentEvent.Handled)
|
||||
{
|
||||
args.Handled = suicideByEnvironmentEvent.Handled;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyDeath(EntityUid target, SuicideKind kind)
|
||||
// Try to suicide by nearby entities, like Microwaves or Crematoriums, by raising an event on it
|
||||
// Returns upon being handled by any entity
|
||||
var itemQuery = GetEntityQuery<ItemComponent>();
|
||||
foreach (var entity in _entityLookupSystem.GetEntitiesInRange(victim, 1, LookupFlags.Approximate | LookupFlags.Static))
|
||||
{
|
||||
if (kind == SuicideKind.Special)
|
||||
return;
|
||||
// Skip any nearby items that can be picked up, we already checked the active held item above
|
||||
if (itemQuery.HasComponent(entity))
|
||||
continue;
|
||||
|
||||
if (!_prototypeManager.TryIndex<DamageTypePrototype>(kind.ToString(), out var damagePrototype))
|
||||
{
|
||||
const SuicideKind fallback = SuicideKind.Blunt;
|
||||
Log.Error($"{nameof(SuicideSystem)} could not find the damage type prototype associated with {kind}. Falling back to {fallback}");
|
||||
damagePrototype = _prototypeManager.Index<DamageTypePrototype>(fallback.ToString());
|
||||
}
|
||||
const int lethalAmountOfDamage = 200; // TODO: Would be nice to get this number from somewhere else
|
||||
_damageableSystem.TryChangeDamage(target, new(damagePrototype, lethalAmountOfDamage), true, origin: target);
|
||||
RaiseLocalEvent(entity, suicideByEnvironmentEvent);
|
||||
if (!suicideByEnvironmentEvent.Handled)
|
||||
continue;
|
||||
|
||||
args.Handled = suicideByEnvironmentEvent.Handled;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Default suicide behavior for any kind of entity that can take damage
|
||||
/// </summary>
|
||||
private void OnDamageableSuicide(Entity<DamageableComponent> victim, ref SuicideEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
var othersMessage = Loc.GetString("suicide-command-default-text-others", ("name", victim));
|
||||
_popup.PopupEntity(othersMessage, victim, Filter.PvsExcept(victim), true);
|
||||
|
||||
var selfMessage = Loc.GetString("suicide-command-default-text-self");
|
||||
_popup.PopupEntity(selfMessage, victim, victim);
|
||||
|
||||
if (args.DamageSpecifier != null)
|
||||
{
|
||||
_suicide.ApplyLethalDamage(victim, args.DamageSpecifier);
|
||||
args.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
args.DamageType ??= "Bloodloss";
|
||||
_suicide.ApplyLethalDamage(victim, args.DamageType);
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,11 +334,41 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
if (playSound)
|
||||
{
|
||||
if (sender == Loc.GetString("admin-announce-announcer-default")) announcementSound = new SoundPathSpecifier(CentComAnnouncementSound); // Corvax-Announcements: Support custom alert sound from admin panel
|
||||
_audio.PlayGlobal(announcementSound?.GetSound() ?? DefaultAnnouncementSound, Filter.Broadcast(), true, announcementSound?.Params ?? AudioParams.Default.WithVolume(-2f));
|
||||
_audio.PlayGlobal(announcementSound == null ? DefaultAnnouncementSound : _audio.GetSound(announcementSound), Filter.Broadcast(), true, AudioParams.Default.WithVolume(-2f));
|
||||
}
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Global station announcement from {sender}: {message}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches an announcement to players selected by filter.
|
||||
/// </summary>
|
||||
/// <param name="filter">Filter to select players who will recieve the announcement</param>
|
||||
/// <param name="message">The contents of the message</param>
|
||||
/// <param name="source">The entity making the announcement (used to determine the station)</param>
|
||||
/// <param name="sender">The sender (Communications Console in Communications Console Announcement)</param>
|
||||
/// <param name="playDefaultSound">Play the announcement sound</param>
|
||||
/// <param name="announcementSound">Sound to play</param>
|
||||
/// <param name="colorOverride">Optional color for the announcement message</param>
|
||||
public void DispatchFilteredAnnouncement(
|
||||
Filter filter,
|
||||
string message,
|
||||
EntityUid? source = null,
|
||||
string? sender = null,
|
||||
bool playSound = true,
|
||||
SoundSpecifier? announcementSound = null,
|
||||
Color? colorOverride = null)
|
||||
{
|
||||
sender ??= Loc.GetString("chat-manager-sender-announcement");
|
||||
|
||||
var wrappedMessage = Loc.GetString("chat-manager-sender-announcement-wrap-message", ("sender", sender), ("message", FormattedMessage.EscapeText(message)));
|
||||
_chatManager.ChatMessageToManyFiltered(filter, ChatChannel.Radio, message, wrappedMessage, source ?? default, false, true, colorOverride);
|
||||
if (playSound)
|
||||
{
|
||||
_audio.PlayGlobal(announcementSound?.ToString() ?? DefaultAnnouncementSound, filter, true, AudioParams.Default.WithVolume(-2f));
|
||||
}
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Station Announcement from {sender}: {message}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches an announcement on a specific station
|
||||
/// </summary>
|
||||
@@ -374,7 +404,7 @@ public sealed partial class ChatSystem : SharedChatSystem
|
||||
|
||||
if (playDefaultSound)
|
||||
{
|
||||
_audio.PlayGlobal(announcementSound?.GetSound() ?? DefaultAnnouncementSound, filter, true, AudioParams.Default.WithVolume(-2f));
|
||||
_audio.PlayGlobal(announcementSound?.ToString() ?? DefaultAnnouncementSound, filter, true, AudioParams.Default.WithVolume(-2f));
|
||||
}
|
||||
|
||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Station Announcement on {station} from {sender}: {message}");
|
||||
|
||||
@@ -123,7 +123,7 @@ namespace Content.Server.Chemistry.EntitySystems
|
||||
var reagent = _protoManager.Index<ReagentPrototype>(reagentQuantity.Reagent.Prototype);
|
||||
|
||||
var reaction =
|
||||
reagent.ReactionTile(tile, (reagentQuantity.Quantity / vapor.TransferAmount) * 0.25f, EntityManager);
|
||||
reagent.ReactionTile(tile, (reagentQuantity.Quantity / vapor.TransferAmount) * 0.25f, EntityManager, reagentQuantity.Reagent.Data);
|
||||
|
||||
if (reaction > reagentQuantity.Quantity)
|
||||
{
|
||||
|
||||
@@ -21,10 +21,12 @@ public sealed partial class CleanDecalsReaction : ITileReaction
|
||||
[DataField]
|
||||
public FixedPoint2 CleanCost { get; private set; } = FixedPoint2.New(0.25f);
|
||||
|
||||
|
||||
public FixedPoint2 TileReact(TileRef tile,
|
||||
ReagentPrototype reagent,
|
||||
FixedPoint2 reactVolume,
|
||||
IEntityManager entityManager)
|
||||
IEntityManager entityManager,
|
||||
List<ReagentData>? data)
|
||||
{
|
||||
if (reactVolume <= CleanCost ||
|
||||
!entityManager.TryGetComponent<MapGridComponent>(tile.GridUid, out var grid) ||
|
||||
|
||||
@@ -34,7 +34,8 @@ public sealed partial class CleanTileReaction : ITileReaction
|
||||
FixedPoint2 ITileReaction.TileReact(TileRef tile,
|
||||
ReagentPrototype reagent,
|
||||
FixedPoint2 reactVolume,
|
||||
IEntityManager entityManager)
|
||||
IEntityManager entityManager
|
||||
, List<ReagentData>? data)
|
||||
{
|
||||
var entities = entityManager.System<EntityLookupSystem>().GetLocalEntitiesIntersecting(tile, 0f).ToArray();
|
||||
var puddleQuery = entityManager.GetEntityQuery<PuddleComponent>();
|
||||
|
||||
@@ -38,7 +38,8 @@ public sealed partial class CreateEntityTileReaction : ITileReaction
|
||||
public FixedPoint2 TileReact(TileRef tile,
|
||||
ReagentPrototype reagent,
|
||||
FixedPoint2 reactVolume,
|
||||
IEntityManager entityManager)
|
||||
IEntityManager entityManager,
|
||||
List<ReagentData>? data)
|
||||
{
|
||||
if (reactVolume >= Usage)
|
||||
{
|
||||
|
||||
@@ -17,7 +17,8 @@ namespace Content.Server.Chemistry.TileReactions
|
||||
public FixedPoint2 TileReact(TileRef tile,
|
||||
ReagentPrototype reagent,
|
||||
FixedPoint2 reactVolume,
|
||||
IEntityManager entityManager)
|
||||
IEntityManager entityManager,
|
||||
List<ReagentData>? data)
|
||||
{
|
||||
if (reactVolume <= FixedPoint2.Zero || tile.Tile.IsEmpty)
|
||||
return FixedPoint2.Zero;
|
||||
|
||||
@@ -16,7 +16,8 @@ namespace Content.Server.Chemistry.TileReactions
|
||||
public FixedPoint2 TileReact(TileRef tile,
|
||||
ReagentPrototype reagent,
|
||||
FixedPoint2 reactVolume,
|
||||
IEntityManager entityManager)
|
||||
IEntityManager entityManager,
|
||||
List<ReagentData>? data)
|
||||
{
|
||||
if (reactVolume <= FixedPoint2.Zero || tile.Tile.IsEmpty)
|
||||
return FixedPoint2.Zero;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Server.Maps;
|
||||
using Content.Server.Maps;
|
||||
using Content.Shared.Chemistry.Reaction;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.FixedPoint;
|
||||
@@ -15,7 +15,8 @@ public sealed partial class PryTileReaction : ITileReaction
|
||||
public FixedPoint2 TileReact(TileRef tile,
|
||||
ReagentPrototype reagent,
|
||||
FixedPoint2 reactVolume,
|
||||
IEntityManager entityManager)
|
||||
IEntityManager entityManager,
|
||||
List<ReagentData>? data)
|
||||
{
|
||||
var sys = entityManager.System<TileSystem>();
|
||||
sys.PryTile(tile);
|
||||
|
||||
@@ -15,13 +15,14 @@ namespace Content.Server.Chemistry.TileReactions
|
||||
public FixedPoint2 TileReact(TileRef tile,
|
||||
ReagentPrototype reagent,
|
||||
FixedPoint2 reactVolume,
|
||||
IEntityManager entityManager)
|
||||
IEntityManager entityManager,
|
||||
List<ReagentData>? data)
|
||||
{
|
||||
var spillSystem = entityManager.System<PuddleSystem>();
|
||||
if (reactVolume < 5 || !spillSystem.TryGetPuddle(tile, out _))
|
||||
return FixedPoint2.Zero;
|
||||
|
||||
return spillSystem.TrySpillAt(tile, new Solution(reagent.ID, reactVolume), out _, sound: false, tileReact: false)
|
||||
return spillSystem.TrySpillAt(tile, new Solution(reagent.ID, reactVolume, data), out _, sound: false, tileReact: false)
|
||||
? reactVolume
|
||||
: FixedPoint2.Zero;
|
||||
}
|
||||
|
||||
@@ -29,13 +29,14 @@ namespace Content.Server.Chemistry.TileReactions
|
||||
public FixedPoint2 TileReact(TileRef tile,
|
||||
ReagentPrototype reagent,
|
||||
FixedPoint2 reactVolume,
|
||||
IEntityManager entityManager)
|
||||
IEntityManager entityManager,
|
||||
List<ReagentData>? data)
|
||||
{
|
||||
if (reactVolume < 5)
|
||||
return FixedPoint2.Zero;
|
||||
|
||||
if (entityManager.EntitySysManager.GetEntitySystem<PuddleSystem>()
|
||||
.TrySpillAt(tile, new Solution(reagent.ID, reactVolume), out var puddleUid, false, false))
|
||||
.TrySpillAt(tile, new Solution(reagent.ID, reactVolume, data), out var puddleUid, false, false))
|
||||
{
|
||||
var slippery = entityManager.EnsureComponent<SlipperyComponent>(puddleUid);
|
||||
slippery.LaunchForwardsMultiplier = _launchForwardsMultiplier;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user