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:
Morb0
2024-08-12 13:18:43 +03:00
835 changed files with 27961 additions and 19871 deletions

View File

@@ -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);
}
}
}

View File

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

View File

@@ -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++;
}
}

View File

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

View File

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

View 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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);

View File

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

View File

@@ -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);
}
}

View File

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

View File

@@ -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());

View File

@@ -0,0 +1,10 @@
using Content.Shared.Atmos.EntitySystems;
using JetBrains.Annotations;
namespace Content.Client.Atmos.EntitySystems;
[UsedImplicitly]
public sealed class GasMinerSystem : SharedGasMinerSystem
{
}

View File

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

View File

@@ -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;
}
}

View 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);
}
}

View File

@@ -0,0 +1,6 @@
using Content.Shared.Clothing;
namespace Content.Client.Clothing.Systems;
/// <inheritdoc/>
public sealed class CursedMaskSystem : SharedCursedMaskSystem;

View File

@@ -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()
{

View File

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

View File

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

View File

@@ -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) :

View 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>

View 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);
}
}

View File

@@ -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());
}
}
}
}

View File

@@ -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"))
);
}
}

View File

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

View File

@@ -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();
}

View File

@@ -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);
}
}
}
}

View File

@@ -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))
{

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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()
{

View File

@@ -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);
}
}
}

View File

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

View File

@@ -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)
{

View File

@@ -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")

View File

@@ -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}");

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

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

View File

@@ -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;
}
}
}

View File

@@ -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();
}
}

View File

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

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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),

View File

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

View File

@@ -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();
}
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}
}

View File

@@ -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(() =>

View 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();
}
}

View File

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

View File

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

View 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);
}
}

View File

@@ -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),

View File

@@ -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 { }

View File

@@ -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>())
{

View File

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

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View 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;
}
}

View File

@@ -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;
}

View 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();
}
}

View File

@@ -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);

View File

@@ -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);
});
},

View File

@@ -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"),
});
}
}
}

View File

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

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -48,7 +48,7 @@ public sealed class BotanySwabSystem : EntitySystem
{
Broadcast = true,
BreakOnMove = true,
NeedHand = true
NeedHand = true,
});
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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"));

View File

@@ -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;
}

View File

@@ -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}");
}

View File

@@ -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;
}
}

View File

@@ -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}");

View File

@@ -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)
{

View File

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

View File

@@ -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>();

View File

@@ -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)
{

View File

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

View File

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

View File

@@ -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);

View File

@@ -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;
}

View File

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