Namespace cleanup around Mind Roles (#30965)
* namespaces * Comment does not need a semicolon --------- Co-authored-by: Vasilis <vascreeper@yahoo.com>
This commit is contained in:
@@ -7,67 +7,66 @@ using Robust.Shared.GameObjects;
|
|||||||
using Robust.Shared.IoC;
|
using Robust.Shared.IoC;
|
||||||
using Robust.Shared.Maths;
|
using Robust.Shared.Maths;
|
||||||
|
|
||||||
namespace Content.Client.Administration
|
namespace Content.Client.Administration;
|
||||||
|
|
||||||
|
internal sealed class AdminNameOverlay : Overlay
|
||||||
{
|
{
|
||||||
internal sealed class AdminNameOverlay : Overlay
|
private readonly AdminSystem _system;
|
||||||
|
private readonly IEntityManager _entityManager;
|
||||||
|
private readonly IEyeManager _eyeManager;
|
||||||
|
private readonly EntityLookupSystem _entityLookup;
|
||||||
|
private readonly Font _font;
|
||||||
|
|
||||||
|
public AdminNameOverlay(AdminSystem system, IEntityManager entityManager, IEyeManager eyeManager, IResourceCache resourceCache, EntityLookupSystem entityLookup)
|
||||||
{
|
{
|
||||||
private readonly AdminSystem _system;
|
_system = system;
|
||||||
private readonly IEntityManager _entityManager;
|
_entityManager = entityManager;
|
||||||
private readonly IEyeManager _eyeManager;
|
_eyeManager = eyeManager;
|
||||||
private readonly EntityLookupSystem _entityLookup;
|
_entityLookup = entityLookup;
|
||||||
private readonly Font _font;
|
ZIndex = 200;
|
||||||
|
_font = new VectorFont(resourceCache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
|
||||||
|
}
|
||||||
|
|
||||||
public AdminNameOverlay(AdminSystem system, IEntityManager entityManager, IEyeManager eyeManager, IResourceCache resourceCache, EntityLookupSystem entityLookup)
|
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
|
||||||
|
|
||||||
|
protected override void Draw(in OverlayDrawArgs args)
|
||||||
|
{
|
||||||
|
var viewport = args.WorldAABB;
|
||||||
|
|
||||||
|
foreach (var playerInfo in _system.PlayerList)
|
||||||
{
|
{
|
||||||
_system = system;
|
var entity = _entityManager.GetEntity(playerInfo.NetEntity);
|
||||||
_entityManager = entityManager;
|
|
||||||
_eyeManager = eyeManager;
|
|
||||||
_entityLookup = entityLookup;
|
|
||||||
ZIndex = 200;
|
|
||||||
_font = new VectorFont(resourceCache.GetResource<FontResource>("/Fonts/NotoSans/NotoSans-Regular.ttf"), 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
|
// Otherwise the entity can not exist yet
|
||||||
|
if (entity == null || !_entityManager.EntityExists(entity))
|
||||||
protected override void Draw(in OverlayDrawArgs args)
|
|
||||||
{
|
|
||||||
var viewport = args.WorldAABB;
|
|
||||||
|
|
||||||
foreach (var playerInfo in _system.PlayerList)
|
|
||||||
{
|
{
|
||||||
var entity = _entityManager.GetEntity(playerInfo.NetEntity);
|
continue;
|
||||||
|
|
||||||
// Otherwise the entity can not exist yet
|
|
||||||
if (entity == null || !_entityManager.EntityExists(entity))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if not on the same map, continue
|
|
||||||
if (_entityManager.GetComponent<TransformComponent>(entity.Value).MapID != args.MapId)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var aabb = _entityLookup.GetWorldAABB(entity.Value);
|
|
||||||
|
|
||||||
// if not on screen, continue
|
|
||||||
if (!aabb.Intersects(in viewport))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var lineoffset = new Vector2(0f, 11f);
|
|
||||||
var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center +
|
|
||||||
new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec(
|
|
||||||
aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f);
|
|
||||||
if (playerInfo.Antag)
|
|
||||||
{
|
|
||||||
args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), "ANTAG", Color.OrangeRed);
|
|
||||||
}
|
|
||||||
args.ScreenHandle.DrawString(_font, screenCoordinates+lineoffset, playerInfo.Username, playerInfo.Connected ? Color.Yellow : Color.White);
|
|
||||||
args.ScreenHandle.DrawString(_font, screenCoordinates, playerInfo.CharacterName, playerInfo.Connected ? Color.Aquamarine : Color.White);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if not on the same map, continue
|
||||||
|
if (_entityManager.GetComponent<TransformComponent>(entity.Value).MapID != args.MapId)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var aabb = _entityLookup.GetWorldAABB(entity.Value);
|
||||||
|
|
||||||
|
// if not on screen, continue
|
||||||
|
if (!aabb.Intersects(in viewport))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var lineoffset = new Vector2(0f, 11f);
|
||||||
|
var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center +
|
||||||
|
new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec(
|
||||||
|
aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f);
|
||||||
|
if (playerInfo.Antag)
|
||||||
|
{
|
||||||
|
args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), "ANTAG", Color.OrangeRed);
|
||||||
|
}
|
||||||
|
args.ScreenHandle.DrawString(_font, screenCoordinates+lineoffset, playerInfo.Username, playerInfo.Connected ? Color.Yellow : Color.White);
|
||||||
|
args.ScreenHandle.DrawString(_font, screenCoordinates, playerInfo.CharacterName, playerInfo.Connected ? Color.Aquamarine : Color.White);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,220 +10,219 @@ using Robust.Client.UserInterface.XAML;
|
|||||||
using static Content.Client.Administration.UI.Tabs.PlayerTab.PlayerTabHeader;
|
using static Content.Client.Administration.UI.Tabs.PlayerTab.PlayerTabHeader;
|
||||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||||
|
|
||||||
namespace Content.Client.Administration.UI.Tabs.PlayerTab
|
namespace Content.Client.Administration.UI.Tabs.PlayerTab;
|
||||||
|
|
||||||
|
[GenerateTypedNameReferences]
|
||||||
|
public sealed partial class PlayerTab : Control
|
||||||
{
|
{
|
||||||
[GenerateTypedNameReferences]
|
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||||
public sealed partial class PlayerTab : Control
|
[Dependency] private readonly IPlayerManager _playerMan = default!;
|
||||||
|
|
||||||
|
private const string ArrowUp = "↑";
|
||||||
|
private const string ArrowDown = "↓";
|
||||||
|
private readonly Color _altColor = Color.FromHex("#292B38");
|
||||||
|
private readonly Color _defaultColor = Color.FromHex("#2F2F3B");
|
||||||
|
private readonly AdminSystem _adminSystem;
|
||||||
|
private IReadOnlyList<PlayerInfo> _players = new List<PlayerInfo>();
|
||||||
|
|
||||||
|
private Header _headerClicked = Header.Username;
|
||||||
|
private bool _ascending = true;
|
||||||
|
private bool _showDisconnected;
|
||||||
|
|
||||||
|
public event Action<GUIBoundKeyEventArgs, ListData>? OnEntryKeyBindDown;
|
||||||
|
|
||||||
|
public PlayerTab()
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
IoCManager.InjectDependencies(this);
|
||||||
[Dependency] private readonly IPlayerManager _playerMan = default!;
|
RobustXamlLoader.Load(this);
|
||||||
|
|
||||||
private const string ArrowUp = "↑";
|
_adminSystem = _entManager.System<AdminSystem>();
|
||||||
private const string ArrowDown = "↓";
|
_adminSystem.PlayerListChanged += RefreshPlayerList;
|
||||||
private readonly Color _altColor = Color.FromHex("#292B38");
|
_adminSystem.OverlayEnabled += OverlayEnabled;
|
||||||
private readonly Color _defaultColor = Color.FromHex("#2F2F3B");
|
_adminSystem.OverlayDisabled += OverlayDisabled;
|
||||||
private readonly AdminSystem _adminSystem;
|
|
||||||
private IReadOnlyList<PlayerInfo> _players = new List<PlayerInfo>();
|
|
||||||
|
|
||||||
private Header _headerClicked = Header.Username;
|
OverlayButton.OnPressed += OverlayButtonPressed;
|
||||||
private bool _ascending = true;
|
ShowDisconnectedButton.OnPressed += ShowDisconnectedPressed;
|
||||||
private bool _showDisconnected;
|
|
||||||
|
|
||||||
public event Action<GUIBoundKeyEventArgs, ListData>? OnEntryKeyBindDown;
|
ListHeader.BackgroundColorPanel.PanelOverride = new StyleBoxFlat(_altColor);
|
||||||
|
ListHeader.OnHeaderClicked += HeaderClicked;
|
||||||
|
|
||||||
public PlayerTab()
|
SearchList.SearchBar = SearchLineEdit;
|
||||||
{
|
SearchList.GenerateItem += GenerateButton;
|
||||||
IoCManager.InjectDependencies(this);
|
SearchList.DataFilterCondition += DataFilterCondition;
|
||||||
RobustXamlLoader.Load(this);
|
SearchList.ItemKeyBindDown += (args, data) => OnEntryKeyBindDown?.Invoke(args, data);
|
||||||
|
|
||||||
_adminSystem = _entManager.System<AdminSystem>();
|
RefreshPlayerList(_adminSystem.PlayerList);
|
||||||
_adminSystem.PlayerListChanged += RefreshPlayerList;
|
|
||||||
_adminSystem.OverlayEnabled += OverlayEnabled;
|
|
||||||
_adminSystem.OverlayDisabled += OverlayDisabled;
|
|
||||||
|
|
||||||
OverlayButton.OnPressed += OverlayButtonPressed;
|
|
||||||
ShowDisconnectedButton.OnPressed += ShowDisconnectedPressed;
|
|
||||||
|
|
||||||
ListHeader.BackgroundColorPanel.PanelOverride = new StyleBoxFlat(_altColor);
|
|
||||||
ListHeader.OnHeaderClicked += HeaderClicked;
|
|
||||||
|
|
||||||
SearchList.SearchBar = SearchLineEdit;
|
|
||||||
SearchList.GenerateItem += GenerateButton;
|
|
||||||
SearchList.DataFilterCondition += DataFilterCondition;
|
|
||||||
SearchList.ItemKeyBindDown += (args, data) => OnEntryKeyBindDown?.Invoke(args, data);
|
|
||||||
|
|
||||||
RefreshPlayerList(_adminSystem.PlayerList);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Antag Overlay
|
|
||||||
|
|
||||||
private void OverlayEnabled()
|
|
||||||
{
|
|
||||||
OverlayButton.Pressed = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OverlayDisabled()
|
|
||||||
{
|
|
||||||
OverlayButton.Pressed = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OverlayButtonPressed(ButtonEventArgs args)
|
|
||||||
{
|
|
||||||
if (args.Button.Pressed)
|
|
||||||
{
|
|
||||||
_adminSystem.AdminOverlayOn();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_adminSystem.AdminOverlayOff();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
private void ShowDisconnectedPressed(ButtonEventArgs args)
|
|
||||||
{
|
|
||||||
_showDisconnected = args.Button.Pressed;
|
|
||||||
RefreshPlayerList(_players);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
base.Dispose(disposing);
|
|
||||||
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
_adminSystem.PlayerListChanged -= RefreshPlayerList;
|
|
||||||
_adminSystem.OverlayEnabled -= OverlayEnabled;
|
|
||||||
_adminSystem.OverlayDisabled -= OverlayDisabled;
|
|
||||||
|
|
||||||
OverlayButton.OnPressed -= OverlayButtonPressed;
|
|
||||||
|
|
||||||
ListHeader.OnHeaderClicked -= HeaderClicked;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#region ListContainer
|
|
||||||
|
|
||||||
private void RefreshPlayerList(IReadOnlyList<PlayerInfo> players)
|
|
||||||
{
|
|
||||||
_players = players;
|
|
||||||
PlayerCount.Text = Loc.GetString("player-tab-player-count", ("count", _playerMan.PlayerCount));
|
|
||||||
|
|
||||||
var filteredPlayers = players.Where(info => _showDisconnected || info.Connected).ToList();
|
|
||||||
|
|
||||||
var sortedPlayers = new List<PlayerInfo>(filteredPlayers);
|
|
||||||
sortedPlayers.Sort(Compare);
|
|
||||||
|
|
||||||
UpdateHeaderSymbols();
|
|
||||||
|
|
||||||
SearchList.PopulateList(sortedPlayers.Select(info => new PlayerListData(info,
|
|
||||||
$"{info.Username} {info.CharacterName} {info.IdentityName} {info.StartingJob}"))
|
|
||||||
.ToList());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void GenerateButton(ListData data, ListContainerButton button)
|
|
||||||
{
|
|
||||||
if (data is not PlayerListData { Info: var player})
|
|
||||||
return;
|
|
||||||
|
|
||||||
var entry = new PlayerTabEntry(player, new StyleBoxFlat(button.Index % 2 == 0 ? _altColor : _defaultColor));
|
|
||||||
button.AddChild(entry);
|
|
||||||
button.ToolTip = $"{player.Username}, {player.CharacterName}, {player.IdentityName}, {player.StartingJob}";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines whether <paramref name="filter"/> is contained in <paramref name="listData"/>.FilteringString.
|
|
||||||
/// If all characters are lowercase, the comparison ignores case.
|
|
||||||
/// If there is an uppercase character, the comparison is case sensitive.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="filter"></param>
|
|
||||||
/// <param name="listData"></param>
|
|
||||||
/// <returns>Whether <paramref name="filter"/> is contained in <paramref name="listData"/>.FilteringString.</returns>
|
|
||||||
private bool DataFilterCondition(string filter, ListData listData)
|
|
||||||
{
|
|
||||||
if (listData is not PlayerListData {Info: var info, FilteringString: var playerString})
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (!_showDisconnected && !info.Connected)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (IsAllLower(filter))
|
|
||||||
{
|
|
||||||
if (!playerString.Contains(filter, StringComparison.CurrentCultureIgnoreCase))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!playerString.Contains(filter))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsAllLower(string input)
|
|
||||||
{
|
|
||||||
foreach (var c in input)
|
|
||||||
{
|
|
||||||
if (char.IsLetter(c) && !char.IsLower(c))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Header
|
|
||||||
|
|
||||||
private void UpdateHeaderSymbols()
|
|
||||||
{
|
|
||||||
ListHeader.ResetHeaderText();
|
|
||||||
ListHeader.GetHeader(_headerClicked).Text += $" {(_ascending ? ArrowUp : ArrowDown)}";
|
|
||||||
}
|
|
||||||
|
|
||||||
private int Compare(PlayerInfo x, PlayerInfo y)
|
|
||||||
{
|
|
||||||
if (!_ascending)
|
|
||||||
{
|
|
||||||
(x, y) = (y, x);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _headerClicked switch
|
|
||||||
{
|
|
||||||
Header.Username => Compare(x.Username, y.Username),
|
|
||||||
Header.Character => Compare(x.CharacterName, y.CharacterName),
|
|
||||||
Header.Job => Compare(x.StartingJob, y.StartingJob),
|
|
||||||
Header.Antagonist => x.Antag.CompareTo(y.Antag),
|
|
||||||
Header.Playtime => TimeSpan.Compare(x.OverallPlaytime ?? default, y.OverallPlaytime ?? default),
|
|
||||||
_ => 1
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private int Compare(string x, string y)
|
|
||||||
{
|
|
||||||
return string.Compare(x, y, StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HeaderClicked(Header header)
|
|
||||||
{
|
|
||||||
if (_headerClicked == header)
|
|
||||||
{
|
|
||||||
_ascending = !_ascending;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_headerClicked = header;
|
|
||||||
_ascending = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
RefreshPlayerList(_adminSystem.PlayerList);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public record PlayerListData(PlayerInfo Info, string FilteringString) : ListData;
|
#region Antag Overlay
|
||||||
|
|
||||||
|
private void OverlayEnabled()
|
||||||
|
{
|
||||||
|
OverlayButton.Pressed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OverlayDisabled()
|
||||||
|
{
|
||||||
|
OverlayButton.Pressed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OverlayButtonPressed(ButtonEventArgs args)
|
||||||
|
{
|
||||||
|
if (args.Button.Pressed)
|
||||||
|
{
|
||||||
|
_adminSystem.AdminOverlayOn();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_adminSystem.AdminOverlayOff();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private void ShowDisconnectedPressed(ButtonEventArgs args)
|
||||||
|
{
|
||||||
|
_showDisconnected = args.Button.Pressed;
|
||||||
|
RefreshPlayerList(_players);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
base.Dispose(disposing);
|
||||||
|
|
||||||
|
if (disposing)
|
||||||
|
{
|
||||||
|
_adminSystem.PlayerListChanged -= RefreshPlayerList;
|
||||||
|
_adminSystem.OverlayEnabled -= OverlayEnabled;
|
||||||
|
_adminSystem.OverlayDisabled -= OverlayDisabled;
|
||||||
|
|
||||||
|
OverlayButton.OnPressed -= OverlayButtonPressed;
|
||||||
|
|
||||||
|
ListHeader.OnHeaderClicked -= HeaderClicked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region ListContainer
|
||||||
|
|
||||||
|
private void RefreshPlayerList(IReadOnlyList<PlayerInfo> players)
|
||||||
|
{
|
||||||
|
_players = players;
|
||||||
|
PlayerCount.Text = Loc.GetString("player-tab-player-count", ("count", _playerMan.PlayerCount));
|
||||||
|
|
||||||
|
var filteredPlayers = players.Where(info => _showDisconnected || info.Connected).ToList();
|
||||||
|
|
||||||
|
var sortedPlayers = new List<PlayerInfo>(filteredPlayers);
|
||||||
|
sortedPlayers.Sort(Compare);
|
||||||
|
|
||||||
|
UpdateHeaderSymbols();
|
||||||
|
|
||||||
|
SearchList.PopulateList(sortedPlayers.Select(info => new PlayerListData(info,
|
||||||
|
$"{info.Username} {info.CharacterName} {info.IdentityName} {info.StartingJob}"))
|
||||||
|
.ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GenerateButton(ListData data, ListContainerButton button)
|
||||||
|
{
|
||||||
|
if (data is not PlayerListData { Info: var player})
|
||||||
|
return;
|
||||||
|
|
||||||
|
var entry = new PlayerTabEntry(player, new StyleBoxFlat(button.Index % 2 == 0 ? _altColor : _defaultColor));
|
||||||
|
button.AddChild(entry);
|
||||||
|
button.ToolTip = $"{player.Username}, {player.CharacterName}, {player.IdentityName}, {player.StartingJob}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether <paramref name="filter"/> is contained in <paramref name="listData"/>.FilteringString.
|
||||||
|
/// If all characters are lowercase, the comparison ignores case.
|
||||||
|
/// If there is an uppercase character, the comparison is case sensitive.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filter"></param>
|
||||||
|
/// <param name="listData"></param>
|
||||||
|
/// <returns>Whether <paramref name="filter"/> is contained in <paramref name="listData"/>.FilteringString.</returns>
|
||||||
|
private bool DataFilterCondition(string filter, ListData listData)
|
||||||
|
{
|
||||||
|
if (listData is not PlayerListData {Info: var info, FilteringString: var playerString})
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!_showDisconnected && !info.Connected)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (IsAllLower(filter))
|
||||||
|
{
|
||||||
|
if (!playerString.Contains(filter, StringComparison.CurrentCultureIgnoreCase))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!playerString.Contains(filter))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsAllLower(string input)
|
||||||
|
{
|
||||||
|
foreach (var c in input)
|
||||||
|
{
|
||||||
|
if (char.IsLetter(c) && !char.IsLower(c))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Header
|
||||||
|
|
||||||
|
private void UpdateHeaderSymbols()
|
||||||
|
{
|
||||||
|
ListHeader.ResetHeaderText();
|
||||||
|
ListHeader.GetHeader(_headerClicked).Text += $" {(_ascending ? ArrowUp : ArrowDown)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private int Compare(PlayerInfo x, PlayerInfo y)
|
||||||
|
{
|
||||||
|
if (!_ascending)
|
||||||
|
{
|
||||||
|
(x, y) = (y, x);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _headerClicked switch
|
||||||
|
{
|
||||||
|
Header.Username => Compare(x.Username, y.Username),
|
||||||
|
Header.Character => Compare(x.CharacterName, y.CharacterName),
|
||||||
|
Header.Job => Compare(x.StartingJob, y.StartingJob),
|
||||||
|
Header.Antagonist => x.Antag.CompareTo(y.Antag),
|
||||||
|
Header.Playtime => TimeSpan.Compare(x.OverallPlaytime ?? default, y.OverallPlaytime ?? default),
|
||||||
|
_ => 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private int Compare(string x, string y)
|
||||||
|
{
|
||||||
|
return string.Compare(x, y, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HeaderClicked(Header header)
|
||||||
|
{
|
||||||
|
if (_headerClicked == header)
|
||||||
|
{
|
||||||
|
_ascending = !_ascending;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_headerClicked = header;
|
||||||
|
_ascending = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshPlayerList(_adminSystem.PlayerList);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record PlayerListData(PlayerInfo Info, string FilteringString) : ListData;
|
||||||
|
|||||||
@@ -5,71 +5,70 @@ using Content.Shared.Chat;
|
|||||||
using Robust.Client.Console;
|
using Robust.Client.Console;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
namespace Content.Client.Chat.Managers
|
namespace Content.Client.Chat.Managers;
|
||||||
|
|
||||||
|
internal sealed class ChatManager : IChatManager
|
||||||
{
|
{
|
||||||
internal sealed class ChatManager : IChatManager
|
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
|
||||||
|
[Dependency] private readonly IClientAdminManager _adminMgr = default!;
|
||||||
|
[Dependency] private readonly IEntitySystemManager _systems = default!;
|
||||||
|
|
||||||
|
private ISawmill _sawmill = default!;
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
|
_sawmill = Logger.GetSawmill("chat");
|
||||||
[Dependency] private readonly IClientAdminManager _adminMgr = default!;
|
_sawmill.Level = LogLevel.Info;
|
||||||
[Dependency] private readonly IEntitySystemManager _systems = default!;
|
}
|
||||||
|
|
||||||
private ISawmill _sawmill = default!;
|
public void SendMessage(string text, ChatSelectChannel channel)
|
||||||
|
{
|
||||||
public void Initialize()
|
var str = text.ToString();
|
||||||
|
switch (channel)
|
||||||
{
|
{
|
||||||
_sawmill = Logger.GetSawmill("chat");
|
case ChatSelectChannel.Console:
|
||||||
_sawmill.Level = LogLevel.Info;
|
// run locally
|
||||||
}
|
_consoleHost.ExecuteCommand(text);
|
||||||
|
break;
|
||||||
|
|
||||||
public void SendMessage(string text, ChatSelectChannel channel)
|
case ChatSelectChannel.LOOC:
|
||||||
{
|
_consoleHost.ExecuteCommand($"looc \"{CommandParsing.Escape(str)}\"");
|
||||||
var str = text.ToString();
|
break;
|
||||||
switch (channel)
|
|
||||||
{
|
|
||||||
case ChatSelectChannel.Console:
|
|
||||||
// run locally
|
|
||||||
_consoleHost.ExecuteCommand(text);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ChatSelectChannel.LOOC:
|
case ChatSelectChannel.OOC:
|
||||||
_consoleHost.ExecuteCommand($"looc \"{CommandParsing.Escape(str)}\"");
|
_consoleHost.ExecuteCommand($"ooc \"{CommandParsing.Escape(str)}\"");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ChatSelectChannel.OOC:
|
case ChatSelectChannel.Admin:
|
||||||
_consoleHost.ExecuteCommand($"ooc \"{CommandParsing.Escape(str)}\"");
|
_consoleHost.ExecuteCommand($"asay \"{CommandParsing.Escape(str)}\"");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ChatSelectChannel.Admin:
|
case ChatSelectChannel.Emotes:
|
||||||
_consoleHost.ExecuteCommand($"asay \"{CommandParsing.Escape(str)}\"");
|
_consoleHost.ExecuteCommand($"me \"{CommandParsing.Escape(str)}\"");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ChatSelectChannel.Emotes:
|
case ChatSelectChannel.Dead:
|
||||||
_consoleHost.ExecuteCommand($"me \"{CommandParsing.Escape(str)}\"");
|
if (_systems.GetEntitySystemOrNull<GhostSystem>() is {IsGhost: true})
|
||||||
break;
|
goto case ChatSelectChannel.Local;
|
||||||
|
|
||||||
case ChatSelectChannel.Dead:
|
if (_adminMgr.HasFlag(AdminFlags.Admin))
|
||||||
if (_systems.GetEntitySystemOrNull<GhostSystem>() is {IsGhost: true})
|
_consoleHost.ExecuteCommand($"dsay \"{CommandParsing.Escape(str)}\"");
|
||||||
goto case ChatSelectChannel.Local;
|
else
|
||||||
|
_sawmill.Warning("Tried to speak on deadchat without being ghost or admin.");
|
||||||
|
break;
|
||||||
|
|
||||||
if (_adminMgr.HasFlag(AdminFlags.Admin))
|
// TODO sepearate radio and say into separate commands.
|
||||||
_consoleHost.ExecuteCommand($"dsay \"{CommandParsing.Escape(str)}\"");
|
case ChatSelectChannel.Radio:
|
||||||
else
|
case ChatSelectChannel.Local:
|
||||||
_sawmill.Warning("Tried to speak on deadchat without being ghost or admin.");
|
_consoleHost.ExecuteCommand($"say \"{CommandParsing.Escape(str)}\"");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// TODO sepearate radio and say into separate commands.
|
case ChatSelectChannel.Whisper:
|
||||||
case ChatSelectChannel.Radio:
|
_consoleHost.ExecuteCommand($"whisper \"{CommandParsing.Escape(str)}\"");
|
||||||
case ChatSelectChannel.Local:
|
break;
|
||||||
_consoleHost.ExecuteCommand($"say \"{CommandParsing.Escape(str)}\"");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case ChatSelectChannel.Whisper:
|
default:
|
||||||
_consoleHost.ExecuteCommand($"whisper \"{CommandParsing.Escape(str)}\"");
|
throw new ArgumentOutOfRangeException(nameof(channel), channel, null);
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(channel), channel, null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,426 +31,425 @@ using Robust.Shared.Enums;
|
|||||||
using Robust.Shared.Network;
|
using Robust.Shared.Network;
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
|
|
||||||
namespace Content.Server.Administration.Systems
|
namespace Content.Server.Administration.Systems;
|
||||||
|
|
||||||
|
public sealed class AdminSystem : EntitySystem
|
||||||
{
|
{
|
||||||
public sealed class AdminSystem : EntitySystem
|
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||||
|
[Dependency] private readonly IChatManager _chat = default!;
|
||||||
|
[Dependency] private readonly IConfigurationManager _config = default!;
|
||||||
|
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||||
|
[Dependency] private readonly HandsSystem _hands = default!;
|
||||||
|
[Dependency] private readonly SharedJobSystem _jobs = default!;
|
||||||
|
[Dependency] private readonly InventorySystem _inventory = default!;
|
||||||
|
[Dependency] private readonly MindSystem _minds = default!;
|
||||||
|
[Dependency] private readonly PopupSystem _popup = default!;
|
||||||
|
[Dependency] private readonly PhysicsSystem _physics = default!;
|
||||||
|
[Dependency] private readonly PlayTimeTrackingManager _playTime = default!;
|
||||||
|
[Dependency] private readonly SharedRoleSystem _role = default!;
|
||||||
|
[Dependency] private readonly GameTicker _gameTicker = default!;
|
||||||
|
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||||
|
[Dependency] private readonly StationRecordsSystem _stationRecords = default!;
|
||||||
|
[Dependency] private readonly TransformSystem _transform = default!;
|
||||||
|
|
||||||
|
private readonly Dictionary<NetUserId, PlayerInfo> _playerList = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set of players that have participated in this round.
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlySet<NetUserId> RoundActivePlayers => _roundActivePlayers;
|
||||||
|
|
||||||
|
private readonly HashSet<NetUserId> _roundActivePlayers = new();
|
||||||
|
public readonly PanicBunkerStatus PanicBunker = new();
|
||||||
|
public readonly BabyJailStatus BabyJail = new();
|
||||||
|
|
||||||
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
[Dependency] private readonly IAdminManager _adminManager = default!;
|
base.Initialize();
|
||||||
[Dependency] private readonly IChatManager _chat = default!;
|
|
||||||
[Dependency] private readonly IConfigurationManager _config = default!;
|
|
||||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
|
||||||
[Dependency] private readonly HandsSystem _hands = default!;
|
|
||||||
[Dependency] private readonly SharedJobSystem _jobs = default!;
|
|
||||||
[Dependency] private readonly InventorySystem _inventory = default!;
|
|
||||||
[Dependency] private readonly MindSystem _minds = default!;
|
|
||||||
[Dependency] private readonly PopupSystem _popup = default!;
|
|
||||||
[Dependency] private readonly PhysicsSystem _physics = default!;
|
|
||||||
[Dependency] private readonly PlayTimeTrackingManager _playTime = default!;
|
|
||||||
[Dependency] private readonly SharedRoleSystem _role = default!;
|
|
||||||
[Dependency] private readonly GameTicker _gameTicker = default!;
|
|
||||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
|
||||||
[Dependency] private readonly StationRecordsSystem _stationRecords = default!;
|
|
||||||
[Dependency] private readonly TransformSystem _transform = default!;
|
|
||||||
|
|
||||||
private readonly Dictionary<NetUserId, PlayerInfo> _playerList = new();
|
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||||
|
_adminManager.OnPermsChanged += OnAdminPermsChanged;
|
||||||
|
_playTime.SessionPlayTimeUpdated += OnSessionPlayTimeUpdated;
|
||||||
|
|
||||||
/// <summary>
|
// Panic Bunker Settings
|
||||||
/// Set of players that have participated in this round.
|
Subs.CVar(_config, CCVars.PanicBunkerEnabled, OnPanicBunkerChanged, true);
|
||||||
/// </summary>
|
Subs.CVar(_config, CCVars.PanicBunkerDisableWithAdmins, OnPanicBunkerDisableWithAdminsChanged, true);
|
||||||
public IReadOnlySet<NetUserId> RoundActivePlayers => _roundActivePlayers;
|
Subs.CVar(_config, CCVars.PanicBunkerEnableWithoutAdmins, OnPanicBunkerEnableWithoutAdminsChanged, true);
|
||||||
|
Subs.CVar(_config, CCVars.PanicBunkerCountDeadminnedAdmins, OnPanicBunkerCountDeadminnedAdminsChanged, true);
|
||||||
|
Subs.CVar(_config, CCVars.PanicBunkerShowReason, OnPanicBunkerShowReasonChanged, true);
|
||||||
|
Subs.CVar(_config, CCVars.PanicBunkerMinAccountAge, OnPanicBunkerMinAccountAgeChanged, true);
|
||||||
|
Subs.CVar(_config, CCVars.PanicBunkerMinOverallMinutes, OnPanicBunkerMinOverallMinutesChanged, true);
|
||||||
|
|
||||||
private readonly HashSet<NetUserId> _roundActivePlayers = new();
|
/*
|
||||||
public readonly PanicBunkerStatus PanicBunker = new();
|
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
|
||||||
public readonly BabyJailStatus BabyJail = new();
|
*/
|
||||||
|
|
||||||
public override void Initialize()
|
// Baby Jail Settings
|
||||||
|
Subs.CVar(_config, CCVars.BabyJailEnabled, OnBabyJailChanged, true);
|
||||||
|
Subs.CVar(_config, CCVars.BabyJailShowReason, OnBabyJailShowReasonChanged, true);
|
||||||
|
Subs.CVar(_config, CCVars.BabyJailMaxAccountAge, OnBabyJailMaxAccountAgeChanged, true);
|
||||||
|
Subs.CVar(_config, CCVars.BabyJailMaxOverallMinutes, OnBabyJailMaxOverallMinutesChanged, true);
|
||||||
|
|
||||||
|
SubscribeLocalEvent<IdentityChangedEvent>(OnIdentityChanged);
|
||||||
|
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
|
||||||
|
SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached);
|
||||||
|
SubscribeLocalEvent<RoleAddedEvent>(OnRoleEvent);
|
||||||
|
SubscribeLocalEvent<RoleRemovedEvent>(OnRoleEvent);
|
||||||
|
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestartCleanup);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev)
|
||||||
|
{
|
||||||
|
_roundActivePlayers.Clear();
|
||||||
|
|
||||||
|
foreach (var (id, data) in _playerList)
|
||||||
{
|
{
|
||||||
base.Initialize();
|
if (!data.ActiveThisRound)
|
||||||
|
continue;
|
||||||
|
|
||||||
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
if (!_playerManager.TryGetPlayerData(id, out var playerData))
|
||||||
_adminManager.OnPermsChanged += OnAdminPermsChanged;
|
|
||||||
_playTime.SessionPlayTimeUpdated += OnSessionPlayTimeUpdated;
|
|
||||||
|
|
||||||
// Panic Bunker Settings
|
|
||||||
Subs.CVar(_config, CCVars.PanicBunkerEnabled, OnPanicBunkerChanged, true);
|
|
||||||
Subs.CVar(_config, CCVars.PanicBunkerDisableWithAdmins, OnPanicBunkerDisableWithAdminsChanged, true);
|
|
||||||
Subs.CVar(_config, CCVars.PanicBunkerEnableWithoutAdmins, OnPanicBunkerEnableWithoutAdminsChanged, true);
|
|
||||||
Subs.CVar(_config, CCVars.PanicBunkerCountDeadminnedAdmins, OnPanicBunkerCountDeadminnedAdminsChanged, true);
|
|
||||||
Subs.CVar(_config, CCVars.PanicBunkerShowReason, OnPanicBunkerShowReasonChanged, true);
|
|
||||||
Subs.CVar(_config, CCVars.PanicBunkerMinAccountAge, OnPanicBunkerMinAccountAgeChanged, true);
|
|
||||||
Subs.CVar(_config, CCVars.PanicBunkerMinOverallMinutes, OnPanicBunkerMinOverallMinutesChanged, true);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Baby Jail Settings
|
|
||||||
Subs.CVar(_config, CCVars.BabyJailEnabled, OnBabyJailChanged, true);
|
|
||||||
Subs.CVar(_config, CCVars.BabyJailShowReason, OnBabyJailShowReasonChanged, true);
|
|
||||||
Subs.CVar(_config, CCVars.BabyJailMaxAccountAge, OnBabyJailMaxAccountAgeChanged, true);
|
|
||||||
Subs.CVar(_config, CCVars.BabyJailMaxOverallMinutes, OnBabyJailMaxOverallMinutesChanged, true);
|
|
||||||
|
|
||||||
SubscribeLocalEvent<IdentityChangedEvent>(OnIdentityChanged);
|
|
||||||
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
|
|
||||||
SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached);
|
|
||||||
SubscribeLocalEvent<RoleAddedEvent>(OnRoleEvent);
|
|
||||||
SubscribeLocalEvent<RoleRemovedEvent>(OnRoleEvent);
|
|
||||||
SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestartCleanup);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnRoundRestartCleanup(RoundRestartCleanupEvent ev)
|
|
||||||
{
|
|
||||||
_roundActivePlayers.Clear();
|
|
||||||
|
|
||||||
foreach (var (id, data) in _playerList)
|
|
||||||
{
|
|
||||||
if (!data.ActiveThisRound)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
if (!_playerManager.TryGetPlayerData(id, out var playerData))
|
|
||||||
return;
|
|
||||||
|
|
||||||
_playerManager.TryGetSessionById(id, out var session);
|
|
||||||
_playerList[id] = GetPlayerInfo(playerData, session);
|
|
||||||
}
|
|
||||||
|
|
||||||
var updateEv = new FullPlayerListEvent() { PlayersInfo = _playerList.Values.ToList() };
|
|
||||||
|
|
||||||
foreach (var admin in _adminManager.ActiveAdmins)
|
|
||||||
{
|
|
||||||
RaiseNetworkEvent(updateEv, admin.Channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UpdatePlayerList(ICommonSession player)
|
|
||||||
{
|
|
||||||
_playerList[player.UserId] = GetPlayerInfo(player.Data, player);
|
|
||||||
|
|
||||||
var playerInfoChangedEvent = new PlayerInfoChangedEvent
|
|
||||||
{
|
|
||||||
PlayerInfo = _playerList[player.UserId]
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var admin in _adminManager.ActiveAdmins)
|
|
||||||
{
|
|
||||||
RaiseNetworkEvent(playerInfoChangedEvent, admin.Channel);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public PlayerInfo? GetCachedPlayerInfo(NetUserId? netUserId)
|
|
||||||
{
|
|
||||||
if (netUserId == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
_playerList.TryGetValue(netUserId.Value, out var value);
|
|
||||||
return value ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnIdentityChanged(ref IdentityChangedEvent ev)
|
|
||||||
{
|
|
||||||
if (!TryComp<ActorComponent>(ev.CharacterEntity, out var actor))
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
UpdatePlayerList(actor.PlayerSession);
|
_playerManager.TryGetSessionById(id, out var session);
|
||||||
|
_playerList[id] = GetPlayerInfo(playerData, session);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnRoleEvent(RoleEvent ev)
|
var updateEv = new FullPlayerListEvent() { PlayersInfo = _playerList.Values.ToList() };
|
||||||
|
|
||||||
|
foreach (var admin in _adminManager.ActiveAdmins)
|
||||||
{
|
{
|
||||||
var session = _minds.GetSession(ev.Mind);
|
RaiseNetworkEvent(updateEv, admin.Channel);
|
||||||
if (!ev.Antagonist || session == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
UpdatePlayerList(session);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnAdminPermsChanged(AdminPermsChangedEventArgs obj)
|
|
||||||
{
|
|
||||||
UpdatePanicBunker();
|
|
||||||
|
|
||||||
if (!obj.IsAdmin)
|
|
||||||
{
|
|
||||||
RaiseNetworkEvent(new FullPlayerListEvent(), obj.Player.Channel);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SendFullPlayerList(obj.Player);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPlayerDetached(PlayerDetachedEvent ev)
|
|
||||||
{
|
|
||||||
// If disconnected then the player won't have a connected entity to get character name from.
|
|
||||||
// The disconnected state gets sent by OnPlayerStatusChanged.
|
|
||||||
if (ev.Player.Status == SessionStatus.Disconnected)
|
|
||||||
return;
|
|
||||||
|
|
||||||
UpdatePlayerList(ev.Player);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPlayerAttached(PlayerAttachedEvent ev)
|
|
||||||
{
|
|
||||||
if (ev.Player.Status == SessionStatus.Disconnected)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_roundActivePlayers.Add(ev.Player.UserId);
|
|
||||||
UpdatePlayerList(ev.Player);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Shutdown()
|
|
||||||
{
|
|
||||||
base.Shutdown();
|
|
||||||
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
|
|
||||||
_adminManager.OnPermsChanged -= OnAdminPermsChanged;
|
|
||||||
_playTime.SessionPlayTimeUpdated -= OnSessionPlayTimeUpdated;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
|
|
||||||
{
|
|
||||||
UpdatePlayerList(e.Session);
|
|
||||||
UpdatePanicBunker();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SendFullPlayerList(ICommonSession playerSession)
|
|
||||||
{
|
|
||||||
var ev = new FullPlayerListEvent();
|
|
||||||
|
|
||||||
ev.PlayersInfo = _playerList.Values.ToList();
|
|
||||||
|
|
||||||
RaiseNetworkEvent(ev, playerSession.Channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private PlayerInfo GetPlayerInfo(SessionData data, ICommonSession? session)
|
|
||||||
{
|
|
||||||
var name = data.UserName;
|
|
||||||
var entityName = string.Empty;
|
|
||||||
var identityName = string.Empty;
|
|
||||||
|
|
||||||
if (session?.AttachedEntity != null)
|
|
||||||
{
|
|
||||||
entityName = EntityManager.GetComponent<MetaDataComponent>(session.AttachedEntity.Value).EntityName;
|
|
||||||
identityName = Identity.Name(session.AttachedEntity.Value, EntityManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
var antag = false;
|
|
||||||
var startingRole = string.Empty;
|
|
||||||
if (_minds.TryGetMind(session, out var mindId, out _))
|
|
||||||
{
|
|
||||||
antag = _role.MindIsAntagonist(mindId);
|
|
||||||
startingRole = _jobs.MindTryGetJobName(mindId);
|
|
||||||
}
|
|
||||||
|
|
||||||
var connected = session != null && session.Status is SessionStatus.Connected or SessionStatus.InGame;
|
|
||||||
TimeSpan? overallPlaytime = null;
|
|
||||||
if (session != null &&
|
|
||||||
_playTime.TryGetTrackerTimes(session, out var playTimes) &&
|
|
||||||
playTimes.TryGetValue(PlayTimeTrackingShared.TrackerOverall, out var playTime))
|
|
||||||
{
|
|
||||||
overallPlaytime = playTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new PlayerInfo(name, entityName, identityName, startingRole, antag, GetNetEntity(session?.AttachedEntity), data.UserId,
|
|
||||||
connected, _roundActivePlayers.Contains(data.UserId), overallPlaytime);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPanicBunkerChanged(bool enabled)
|
|
||||||
{
|
|
||||||
PanicBunker.Enabled = enabled;
|
|
||||||
_chat.SendAdminAlert(Loc.GetString(enabled
|
|
||||||
? "admin-ui-panic-bunker-enabled-admin-alert"
|
|
||||||
: "admin-ui-panic-bunker-disabled-admin-alert"
|
|
||||||
));
|
|
||||||
|
|
||||||
SendPanicBunkerStatusAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnBabyJailChanged(bool enabled)
|
|
||||||
{
|
|
||||||
BabyJail.Enabled = enabled;
|
|
||||||
_chat.SendAdminAlert(Loc.GetString(enabled
|
|
||||||
? "admin-ui-baby-jail-enabled-admin-alert"
|
|
||||||
: "admin-ui-baby-jail-disabled-admin-alert"
|
|
||||||
));
|
|
||||||
|
|
||||||
SendBabyJailStatusAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPanicBunkerDisableWithAdminsChanged(bool enabled)
|
|
||||||
{
|
|
||||||
PanicBunker.DisableWithAdmins = enabled;
|
|
||||||
UpdatePanicBunker();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPanicBunkerEnableWithoutAdminsChanged(bool enabled)
|
|
||||||
{
|
|
||||||
PanicBunker.EnableWithoutAdmins = enabled;
|
|
||||||
UpdatePanicBunker();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPanicBunkerCountDeadminnedAdminsChanged(bool enabled)
|
|
||||||
{
|
|
||||||
PanicBunker.CountDeadminnedAdmins = enabled;
|
|
||||||
UpdatePanicBunker();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPanicBunkerShowReasonChanged(bool enabled)
|
|
||||||
{
|
|
||||||
PanicBunker.ShowReason = enabled;
|
|
||||||
SendPanicBunkerStatusAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnBabyJailShowReasonChanged(bool enabled)
|
|
||||||
{
|
|
||||||
BabyJail.ShowReason = enabled;
|
|
||||||
SendBabyJailStatusAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPanicBunkerMinAccountAgeChanged(int minutes)
|
|
||||||
{
|
|
||||||
PanicBunker.MinAccountAgeMinutes = minutes;
|
|
||||||
SendPanicBunkerStatusAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnBabyJailMaxAccountAgeChanged(int minutes)
|
|
||||||
{
|
|
||||||
BabyJail.MaxAccountAgeMinutes = minutes;
|
|
||||||
SendBabyJailStatusAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPanicBunkerMinOverallMinutesChanged(int minutes)
|
|
||||||
{
|
|
||||||
PanicBunker.MinOverallMinutes = minutes;
|
|
||||||
SendPanicBunkerStatusAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnBabyJailMaxOverallMinutesChanged(int minutes)
|
|
||||||
{
|
|
||||||
BabyJail.MaxOverallMinutes = minutes;
|
|
||||||
SendBabyJailStatusAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdatePanicBunker()
|
|
||||||
{
|
|
||||||
var admins = PanicBunker.CountDeadminnedAdmins
|
|
||||||
? _adminManager.AllAdmins
|
|
||||||
: _adminManager.ActiveAdmins;
|
|
||||||
var hasAdmins = admins.Any();
|
|
||||||
|
|
||||||
// TODO Fix order dependent Cvars
|
|
||||||
// Please for the sake of my sanity don't make cvars & order dependent.
|
|
||||||
// Just make a bool field on the system instead of having some cvars automatically modify other cvars.
|
|
||||||
//
|
|
||||||
// I.e., this:
|
|
||||||
// /sudo cvar game.panic_bunker.enabled true
|
|
||||||
// /sudo cvar game.panic_bunker.disable_with_admins true
|
|
||||||
// and this:
|
|
||||||
// /sudo cvar game.panic_bunker.disable_with_admins true
|
|
||||||
// /sudo cvar game.panic_bunker.enabled true
|
|
||||||
//
|
|
||||||
// should have the same effect, but currently setting the disable_with_admins can modify enabled.
|
|
||||||
|
|
||||||
if (hasAdmins && PanicBunker.DisableWithAdmins)
|
|
||||||
{
|
|
||||||
_config.SetCVar(CCVars.PanicBunkerEnabled, false);
|
|
||||||
}
|
|
||||||
else if (!hasAdmins && PanicBunker.EnableWithoutAdmins)
|
|
||||||
{
|
|
||||||
_config.SetCVar(CCVars.PanicBunkerEnabled, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
SendPanicBunkerStatusAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SendPanicBunkerStatusAll()
|
|
||||||
{
|
|
||||||
var ev = new PanicBunkerChangedEvent(PanicBunker);
|
|
||||||
foreach (var admin in _adminManager.AllAdmins)
|
|
||||||
{
|
|
||||||
RaiseNetworkEvent(ev, admin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SendBabyJailStatusAll()
|
|
||||||
{
|
|
||||||
var ev = new BabyJailChangedEvent(BabyJail);
|
|
||||||
foreach (var admin in _adminManager.AllAdmins)
|
|
||||||
{
|
|
||||||
RaiseNetworkEvent(ev, admin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Erases a player from the round.
|
|
||||||
/// This removes them and any trace of them from the round, deleting their
|
|
||||||
/// chat messages and showing a popup to other players.
|
|
||||||
/// Their items are dropped on the ground.
|
|
||||||
/// </summary>
|
|
||||||
public void Erase(ICommonSession player)
|
|
||||||
{
|
|
||||||
var entity = player.AttachedEntity;
|
|
||||||
_chat.DeleteMessagesBy(player);
|
|
||||||
|
|
||||||
if (entity != null && !TerminatingOrDeleted(entity.Value))
|
|
||||||
{
|
|
||||||
if (TryComp(entity.Value, out TransformComponent? transform))
|
|
||||||
{
|
|
||||||
var coordinates = _transform.GetMoverCoordinates(entity.Value, transform);
|
|
||||||
var name = Identity.Entity(entity.Value, EntityManager);
|
|
||||||
_popup.PopupCoordinates(Loc.GetString("admin-erase-popup", ("user", name)), coordinates, PopupType.LargeCaution);
|
|
||||||
var filter = Filter.Pvs(coordinates, 1, EntityManager, _playerManager);
|
|
||||||
var audioParams = new AudioParams().WithVolume(3);
|
|
||||||
_audio.PlayStatic("/Audio/Effects/pop_high.ogg", filter, coordinates, true, audioParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var item in _inventory.GetHandOrInventoryEntities(entity.Value))
|
|
||||||
{
|
|
||||||
if (TryComp(item, out PdaComponent? pda) &&
|
|
||||||
TryComp(pda.ContainedId, out StationRecordKeyStorageComponent? keyStorage) &&
|
|
||||||
keyStorage.Key is { } key &&
|
|
||||||
_stationRecords.TryGetRecord(key, out GeneralStationRecord? record))
|
|
||||||
{
|
|
||||||
if (TryComp(entity, out DnaComponent? dna) &&
|
|
||||||
dna.DNA != record.DNA)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TryComp(entity, out FingerprintComponent? fingerPrint) &&
|
|
||||||
fingerPrint.Fingerprint != record.Fingerprint)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
_stationRecords.RemoveRecord(key);
|
|
||||||
Del(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_inventory.TryGetContainerSlotEnumerator(entity.Value, out var enumerator))
|
|
||||||
{
|
|
||||||
while (enumerator.NextItem(out var item, out var slot))
|
|
||||||
{
|
|
||||||
if (_inventory.TryUnequip(entity.Value, entity.Value, slot.Name, true, true))
|
|
||||||
_physics.ApplyAngularImpulse(item, ThrowingSystem.ThrowAngularImpulse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TryComp(entity.Value, out HandsComponent? hands))
|
|
||||||
{
|
|
||||||
foreach (var hand in _hands.EnumerateHands(entity.Value, hands))
|
|
||||||
{
|
|
||||||
_hands.TryDrop(entity.Value, hand, checkActionBlocker: false, doDropInteraction: false, handsComp: hands);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_minds.WipeMind(player);
|
|
||||||
QueueDel(entity);
|
|
||||||
|
|
||||||
_gameTicker.SpawnObserver(player);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSessionPlayTimeUpdated(ICommonSession session)
|
|
||||||
{
|
|
||||||
UpdatePlayerList(session);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void UpdatePlayerList(ICommonSession player)
|
||||||
|
{
|
||||||
|
_playerList[player.UserId] = GetPlayerInfo(player.Data, player);
|
||||||
|
|
||||||
|
var playerInfoChangedEvent = new PlayerInfoChangedEvent
|
||||||
|
{
|
||||||
|
PlayerInfo = _playerList[player.UserId]
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var admin in _adminManager.ActiveAdmins)
|
||||||
|
{
|
||||||
|
RaiseNetworkEvent(playerInfoChangedEvent, admin.Channel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public PlayerInfo? GetCachedPlayerInfo(NetUserId? netUserId)
|
||||||
|
{
|
||||||
|
if (netUserId == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
_playerList.TryGetValue(netUserId.Value, out var value);
|
||||||
|
return value ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnIdentityChanged(ref IdentityChangedEvent ev)
|
||||||
|
{
|
||||||
|
if (!TryComp<ActorComponent>(ev.CharacterEntity, out var actor))
|
||||||
|
return;
|
||||||
|
|
||||||
|
UpdatePlayerList(actor.PlayerSession);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRoleEvent(RoleEvent ev)
|
||||||
|
{
|
||||||
|
var session = _minds.GetSession(ev.Mind);
|
||||||
|
if (!ev.Antagonist || session == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
UpdatePlayerList(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAdminPermsChanged(AdminPermsChangedEventArgs obj)
|
||||||
|
{
|
||||||
|
UpdatePanicBunker();
|
||||||
|
|
||||||
|
if (!obj.IsAdmin)
|
||||||
|
{
|
||||||
|
RaiseNetworkEvent(new FullPlayerListEvent(), obj.Player.Channel);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SendFullPlayerList(obj.Player);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPlayerDetached(PlayerDetachedEvent ev)
|
||||||
|
{
|
||||||
|
// If disconnected then the player won't have a connected entity to get character name from.
|
||||||
|
// The disconnected state gets sent by OnPlayerStatusChanged.
|
||||||
|
if (ev.Player.Status == SessionStatus.Disconnected)
|
||||||
|
return;
|
||||||
|
|
||||||
|
UpdatePlayerList(ev.Player);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPlayerAttached(PlayerAttachedEvent ev)
|
||||||
|
{
|
||||||
|
if (ev.Player.Status == SessionStatus.Disconnected)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_roundActivePlayers.Add(ev.Player.UserId);
|
||||||
|
UpdatePlayerList(ev.Player);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Shutdown()
|
||||||
|
{
|
||||||
|
base.Shutdown();
|
||||||
|
_playerManager.PlayerStatusChanged -= OnPlayerStatusChanged;
|
||||||
|
_adminManager.OnPermsChanged -= OnAdminPermsChanged;
|
||||||
|
_playTime.SessionPlayTimeUpdated -= OnSessionPlayTimeUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
|
||||||
|
{
|
||||||
|
UpdatePlayerList(e.Session);
|
||||||
|
UpdatePanicBunker();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendFullPlayerList(ICommonSession playerSession)
|
||||||
|
{
|
||||||
|
var ev = new FullPlayerListEvent();
|
||||||
|
|
||||||
|
ev.PlayersInfo = _playerList.Values.ToList();
|
||||||
|
|
||||||
|
RaiseNetworkEvent(ev, playerSession.Channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private PlayerInfo GetPlayerInfo(SessionData data, ICommonSession? session)
|
||||||
|
{
|
||||||
|
var name = data.UserName;
|
||||||
|
var entityName = string.Empty;
|
||||||
|
var identityName = string.Empty;
|
||||||
|
|
||||||
|
if (session?.AttachedEntity != null)
|
||||||
|
{
|
||||||
|
entityName = EntityManager.GetComponent<MetaDataComponent>(session.AttachedEntity.Value).EntityName;
|
||||||
|
identityName = Identity.Name(session.AttachedEntity.Value, EntityManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
var antag = false;
|
||||||
|
var startingRole = string.Empty;
|
||||||
|
if (_minds.TryGetMind(session, out var mindId, out _))
|
||||||
|
{
|
||||||
|
antag = _role.MindIsAntagonist(mindId);
|
||||||
|
startingRole = _jobs.MindTryGetJobName(mindId);
|
||||||
|
}
|
||||||
|
|
||||||
|
var connected = session != null && session.Status is SessionStatus.Connected or SessionStatus.InGame;
|
||||||
|
TimeSpan? overallPlaytime = null;
|
||||||
|
if (session != null &&
|
||||||
|
_playTime.TryGetTrackerTimes(session, out var playTimes) &&
|
||||||
|
playTimes.TryGetValue(PlayTimeTrackingShared.TrackerOverall, out var playTime))
|
||||||
|
{
|
||||||
|
overallPlaytime = playTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new PlayerInfo(name, entityName, identityName, startingRole, antag, GetNetEntity(session?.AttachedEntity), data.UserId,
|
||||||
|
connected, _roundActivePlayers.Contains(data.UserId), overallPlaytime);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPanicBunkerChanged(bool enabled)
|
||||||
|
{
|
||||||
|
PanicBunker.Enabled = enabled;
|
||||||
|
_chat.SendAdminAlert(Loc.GetString(enabled
|
||||||
|
? "admin-ui-panic-bunker-enabled-admin-alert"
|
||||||
|
: "admin-ui-panic-bunker-disabled-admin-alert"
|
||||||
|
));
|
||||||
|
|
||||||
|
SendPanicBunkerStatusAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBabyJailChanged(bool enabled)
|
||||||
|
{
|
||||||
|
BabyJail.Enabled = enabled;
|
||||||
|
_chat.SendAdminAlert(Loc.GetString(enabled
|
||||||
|
? "admin-ui-baby-jail-enabled-admin-alert"
|
||||||
|
: "admin-ui-baby-jail-disabled-admin-alert"
|
||||||
|
));
|
||||||
|
|
||||||
|
SendBabyJailStatusAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPanicBunkerDisableWithAdminsChanged(bool enabled)
|
||||||
|
{
|
||||||
|
PanicBunker.DisableWithAdmins = enabled;
|
||||||
|
UpdatePanicBunker();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPanicBunkerEnableWithoutAdminsChanged(bool enabled)
|
||||||
|
{
|
||||||
|
PanicBunker.EnableWithoutAdmins = enabled;
|
||||||
|
UpdatePanicBunker();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPanicBunkerCountDeadminnedAdminsChanged(bool enabled)
|
||||||
|
{
|
||||||
|
PanicBunker.CountDeadminnedAdmins = enabled;
|
||||||
|
UpdatePanicBunker();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPanicBunkerShowReasonChanged(bool enabled)
|
||||||
|
{
|
||||||
|
PanicBunker.ShowReason = enabled;
|
||||||
|
SendPanicBunkerStatusAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBabyJailShowReasonChanged(bool enabled)
|
||||||
|
{
|
||||||
|
BabyJail.ShowReason = enabled;
|
||||||
|
SendBabyJailStatusAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPanicBunkerMinAccountAgeChanged(int minutes)
|
||||||
|
{
|
||||||
|
PanicBunker.MinAccountAgeMinutes = minutes;
|
||||||
|
SendPanicBunkerStatusAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBabyJailMaxAccountAgeChanged(int minutes)
|
||||||
|
{
|
||||||
|
BabyJail.MaxAccountAgeMinutes = minutes;
|
||||||
|
SendBabyJailStatusAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPanicBunkerMinOverallMinutesChanged(int minutes)
|
||||||
|
{
|
||||||
|
PanicBunker.MinOverallMinutes = minutes;
|
||||||
|
SendPanicBunkerStatusAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBabyJailMaxOverallMinutesChanged(int minutes)
|
||||||
|
{
|
||||||
|
BabyJail.MaxOverallMinutes = minutes;
|
||||||
|
SendBabyJailStatusAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdatePanicBunker()
|
||||||
|
{
|
||||||
|
var admins = PanicBunker.CountDeadminnedAdmins
|
||||||
|
? _adminManager.AllAdmins
|
||||||
|
: _adminManager.ActiveAdmins;
|
||||||
|
var hasAdmins = admins.Any();
|
||||||
|
|
||||||
|
// TODO Fix order dependent Cvars
|
||||||
|
// Please for the sake of my sanity don't make cvars & order dependent.
|
||||||
|
// Just make a bool field on the system instead of having some cvars automatically modify other cvars.
|
||||||
|
//
|
||||||
|
// I.e., this:
|
||||||
|
// /sudo cvar game.panic_bunker.enabled true
|
||||||
|
// /sudo cvar game.panic_bunker.disable_with_admins true
|
||||||
|
// and this:
|
||||||
|
// /sudo cvar game.panic_bunker.disable_with_admins true
|
||||||
|
// /sudo cvar game.panic_bunker.enabled true
|
||||||
|
//
|
||||||
|
// should have the same effect, but currently setting the disable_with_admins can modify enabled.
|
||||||
|
|
||||||
|
if (hasAdmins && PanicBunker.DisableWithAdmins)
|
||||||
|
{
|
||||||
|
_config.SetCVar(CCVars.PanicBunkerEnabled, false);
|
||||||
|
}
|
||||||
|
else if (!hasAdmins && PanicBunker.EnableWithoutAdmins)
|
||||||
|
{
|
||||||
|
_config.SetCVar(CCVars.PanicBunkerEnabled, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
SendPanicBunkerStatusAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendPanicBunkerStatusAll()
|
||||||
|
{
|
||||||
|
var ev = new PanicBunkerChangedEvent(PanicBunker);
|
||||||
|
foreach (var admin in _adminManager.AllAdmins)
|
||||||
|
{
|
||||||
|
RaiseNetworkEvent(ev, admin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendBabyJailStatusAll()
|
||||||
|
{
|
||||||
|
var ev = new BabyJailChangedEvent(BabyJail);
|
||||||
|
foreach (var admin in _adminManager.AllAdmins)
|
||||||
|
{
|
||||||
|
RaiseNetworkEvent(ev, admin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Erases a player from the round.
|
||||||
|
/// This removes them and any trace of them from the round, deleting their
|
||||||
|
/// chat messages and showing a popup to other players.
|
||||||
|
/// Their items are dropped on the ground.
|
||||||
|
/// </summary>
|
||||||
|
public void Erase(ICommonSession player)
|
||||||
|
{
|
||||||
|
var entity = player.AttachedEntity;
|
||||||
|
_chat.DeleteMessagesBy(player);
|
||||||
|
|
||||||
|
if (entity != null && !TerminatingOrDeleted(entity.Value))
|
||||||
|
{
|
||||||
|
if (TryComp(entity.Value, out TransformComponent? transform))
|
||||||
|
{
|
||||||
|
var coordinates = _transform.GetMoverCoordinates(entity.Value, transform);
|
||||||
|
var name = Identity.Entity(entity.Value, EntityManager);
|
||||||
|
_popup.PopupCoordinates(Loc.GetString("admin-erase-popup", ("user", name)), coordinates, PopupType.LargeCaution);
|
||||||
|
var filter = Filter.Pvs(coordinates, 1, EntityManager, _playerManager);
|
||||||
|
var audioParams = new AudioParams().WithVolume(3);
|
||||||
|
_audio.PlayStatic("/Audio/Effects/pop_high.ogg", filter, coordinates, true, audioParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var item in _inventory.GetHandOrInventoryEntities(entity.Value))
|
||||||
|
{
|
||||||
|
if (TryComp(item, out PdaComponent? pda) &&
|
||||||
|
TryComp(pda.ContainedId, out StationRecordKeyStorageComponent? keyStorage) &&
|
||||||
|
keyStorage.Key is { } key &&
|
||||||
|
_stationRecords.TryGetRecord(key, out GeneralStationRecord? record))
|
||||||
|
{
|
||||||
|
if (TryComp(entity, out DnaComponent? dna) &&
|
||||||
|
dna.DNA != record.DNA)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryComp(entity, out FingerprintComponent? fingerPrint) &&
|
||||||
|
fingerPrint.Fingerprint != record.Fingerprint)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_stationRecords.RemoveRecord(key);
|
||||||
|
Del(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_inventory.TryGetContainerSlotEnumerator(entity.Value, out var enumerator))
|
||||||
|
{
|
||||||
|
while (enumerator.NextItem(out var item, out var slot))
|
||||||
|
{
|
||||||
|
if (_inventory.TryUnequip(entity.Value, entity.Value, slot.Name, true, true))
|
||||||
|
_physics.ApplyAngularImpulse(item, ThrowingSystem.ThrowAngularImpulse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryComp(entity.Value, out HandsComponent? hands))
|
||||||
|
{
|
||||||
|
foreach (var hand in _hands.EnumerateHands(entity.Value, hands))
|
||||||
|
{
|
||||||
|
_hands.TryDrop(entity.Value, hand, checkActionBlocker: false, doDropInteraction: false, handsComp: hands);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_minds.WipeMind(player);
|
||||||
|
QueueDel(entity);
|
||||||
|
|
||||||
|
_gameTicker.SpawnObserver(player);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSessionPlayTimeUpdated(ICommonSession session)
|
||||||
|
{
|
||||||
|
UpdatePlayerList(session);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,385 +18,384 @@ using Robust.Shared.Player;
|
|||||||
using Robust.Shared.Replays;
|
using Robust.Shared.Replays;
|
||||||
using Robust.Shared.Utility;
|
using Robust.Shared.Utility;
|
||||||
|
|
||||||
namespace Content.Server.Chat.Managers
|
namespace Content.Server.Chat.Managers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dispatches chat messages to clients.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed partial class ChatManager : IChatManager
|
||||||
{
|
{
|
||||||
/// <summary>
|
private static readonly Dictionary<string, string> PatronOocColors = new()
|
||||||
/// Dispatches chat messages to clients.
|
|
||||||
/// </summary>
|
|
||||||
internal sealed partial class ChatManager : IChatManager
|
|
||||||
{
|
{
|
||||||
private static readonly Dictionary<string, string> PatronOocColors = new()
|
// I had plans for multiple colors and those went nowhere so...
|
||||||
|
{ "nuclear_operative", "#aa00ff" },
|
||||||
|
{ "syndicate_agent", "#aa00ff" },
|
||||||
|
{ "revolutionary", "#aa00ff" }
|
||||||
|
};
|
||||||
|
|
||||||
|
[Dependency] private readonly IReplayRecordingManager _replay = default!;
|
||||||
|
[Dependency] private readonly IServerNetManager _netManager = default!;
|
||||||
|
[Dependency] private readonly IMoMMILink _mommiLink = default!;
|
||||||
|
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||||
|
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||||
|
[Dependency] private readonly IServerPreferencesManager _preferencesManager = default!;
|
||||||
|
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
||||||
|
[Dependency] private readonly INetConfigurationManager _netConfigManager = default!;
|
||||||
|
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||||
|
[Dependency] private readonly PlayerRateLimitManager _rateLimitManager = default!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The maximum length a player-sent message can be sent
|
||||||
|
/// </summary>
|
||||||
|
public int MaxMessageLength => _configurationManager.GetCVar(CCVars.ChatMaxMessageLength);
|
||||||
|
|
||||||
|
private bool _oocEnabled = true;
|
||||||
|
private bool _adminOocEnabled = true;
|
||||||
|
|
||||||
|
private readonly Dictionary<NetUserId, ChatUser> _players = new();
|
||||||
|
|
||||||
|
public void Initialize()
|
||||||
|
{
|
||||||
|
_netManager.RegisterNetMessage<MsgChatMessage>();
|
||||||
|
_netManager.RegisterNetMessage<MsgDeleteChatMessagesBy>();
|
||||||
|
|
||||||
|
_configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true);
|
||||||
|
_configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true);
|
||||||
|
|
||||||
|
RegisterRateLimits();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnOocEnabledChanged(bool val)
|
||||||
|
{
|
||||||
|
if (_oocEnabled == val) return;
|
||||||
|
|
||||||
|
_oocEnabled = val;
|
||||||
|
DispatchServerAnnouncement(Loc.GetString(val ? "chat-manager-ooc-chat-enabled-message" : "chat-manager-ooc-chat-disabled-message"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAdminOocEnabledChanged(bool val)
|
||||||
|
{
|
||||||
|
if (_adminOocEnabled == val) return;
|
||||||
|
|
||||||
|
_adminOocEnabled = val;
|
||||||
|
DispatchServerAnnouncement(Loc.GetString(val ? "chat-manager-admin-ooc-chat-enabled-message" : "chat-manager-admin-ooc-chat-disabled-message"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DeleteMessagesBy(ICommonSession player)
|
||||||
|
{
|
||||||
|
if (!_players.TryGetValue(player.UserId, out var user))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var msg = new MsgDeleteChatMessagesBy { Key = user.Key, Entities = user.Entities };
|
||||||
|
_netManager.ServerSendToAll(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[return: NotNullIfNotNull(nameof(author))]
|
||||||
|
public ChatUser? EnsurePlayer(NetUserId? author)
|
||||||
|
{
|
||||||
|
if (author == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
ref var user = ref CollectionsMarshal.GetValueRefOrAddDefault(_players, author.Value, out var exists);
|
||||||
|
if (!exists || user == null)
|
||||||
|
user = new ChatUser(_players.Count);
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Server Announcements
|
||||||
|
|
||||||
|
public void DispatchServerAnnouncement(string message, Color? colorOverride = null)
|
||||||
|
{
|
||||||
|
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", FormattedMessage.EscapeText(message)));
|
||||||
|
ChatMessageToAll(ChatChannel.Server, message, wrappedMessage, EntityUid.Invalid, hideChat: false, recordReplay: true, colorOverride: colorOverride);
|
||||||
|
Logger.InfoS("SERVER", message);
|
||||||
|
|
||||||
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Server announcement: {message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DispatchServerMessage(ICommonSession player, string message, bool suppressLog = false)
|
||||||
|
{
|
||||||
|
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", FormattedMessage.EscapeText(message)));
|
||||||
|
ChatMessageToOne(ChatChannel.Server, message, wrappedMessage, default, false, player.Channel);
|
||||||
|
|
||||||
|
if (!suppressLog)
|
||||||
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Server message to {player:Player}: {message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendAdminAnnouncement(string message, AdminFlags? flagBlacklist, AdminFlags? flagWhitelist)
|
||||||
|
{
|
||||||
|
var clients = _adminManager.ActiveAdmins.Where(p =>
|
||||||
{
|
{
|
||||||
// I had plans for multiple colors and those went nowhere so...
|
var adminData = _adminManager.GetAdminData(p);
|
||||||
{ "nuclear_operative", "#aa00ff" },
|
|
||||||
{ "syndicate_agent", "#aa00ff" },
|
|
||||||
{ "revolutionary", "#aa00ff" }
|
|
||||||
};
|
|
||||||
|
|
||||||
[Dependency] private readonly IReplayRecordingManager _replay = default!;
|
DebugTools.AssertNotNull(adminData);
|
||||||
[Dependency] private readonly IServerNetManager _netManager = default!;
|
|
||||||
[Dependency] private readonly IMoMMILink _mommiLink = default!;
|
|
||||||
[Dependency] private readonly IAdminManager _adminManager = default!;
|
|
||||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
|
||||||
[Dependency] private readonly IServerPreferencesManager _preferencesManager = default!;
|
|
||||||
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
|
|
||||||
[Dependency] private readonly INetConfigurationManager _netConfigManager = default!;
|
|
||||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
|
||||||
[Dependency] private readonly PlayerRateLimitManager _rateLimitManager = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
if (adminData == null)
|
||||||
/// The maximum length a player-sent message can be sent
|
|
||||||
/// </summary>
|
|
||||||
public int MaxMessageLength => _configurationManager.GetCVar(CCVars.ChatMaxMessageLength);
|
|
||||||
|
|
||||||
private bool _oocEnabled = true;
|
|
||||||
private bool _adminOocEnabled = true;
|
|
||||||
|
|
||||||
private readonly Dictionary<NetUserId, ChatUser> _players = new();
|
|
||||||
|
|
||||||
public void Initialize()
|
|
||||||
{
|
|
||||||
_netManager.RegisterNetMessage<MsgChatMessage>();
|
|
||||||
_netManager.RegisterNetMessage<MsgDeleteChatMessagesBy>();
|
|
||||||
|
|
||||||
_configurationManager.OnValueChanged(CCVars.OocEnabled, OnOocEnabledChanged, true);
|
|
||||||
_configurationManager.OnValueChanged(CCVars.AdminOocEnabled, OnAdminOocEnabledChanged, true);
|
|
||||||
|
|
||||||
RegisterRateLimits();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnOocEnabledChanged(bool val)
|
|
||||||
{
|
|
||||||
if (_oocEnabled == val) return;
|
|
||||||
|
|
||||||
_oocEnabled = val;
|
|
||||||
DispatchServerAnnouncement(Loc.GetString(val ? "chat-manager-ooc-chat-enabled-message" : "chat-manager-ooc-chat-disabled-message"));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnAdminOocEnabledChanged(bool val)
|
|
||||||
{
|
|
||||||
if (_adminOocEnabled == val) return;
|
|
||||||
|
|
||||||
_adminOocEnabled = val;
|
|
||||||
DispatchServerAnnouncement(Loc.GetString(val ? "chat-manager-admin-ooc-chat-enabled-message" : "chat-manager-admin-ooc-chat-disabled-message"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DeleteMessagesBy(ICommonSession player)
|
|
||||||
{
|
|
||||||
if (!_players.TryGetValue(player.UserId, out var user))
|
|
||||||
return;
|
|
||||||
|
|
||||||
var msg = new MsgDeleteChatMessagesBy { Key = user.Key, Entities = user.Entities };
|
|
||||||
_netManager.ServerSendToAll(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
[return: NotNullIfNotNull(nameof(author))]
|
|
||||||
public ChatUser? EnsurePlayer(NetUserId? author)
|
|
||||||
{
|
|
||||||
if (author == null)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
ref var user = ref CollectionsMarshal.GetValueRefOrAddDefault(_players, author.Value, out var exists);
|
|
||||||
if (!exists || user == null)
|
|
||||||
user = new ChatUser(_players.Count);
|
|
||||||
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Server Announcements
|
|
||||||
|
|
||||||
public void DispatchServerAnnouncement(string message, Color? colorOverride = null)
|
|
||||||
{
|
|
||||||
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", FormattedMessage.EscapeText(message)));
|
|
||||||
ChatMessageToAll(ChatChannel.Server, message, wrappedMessage, EntityUid.Invalid, hideChat: false, recordReplay: true, colorOverride: colorOverride);
|
|
||||||
Logger.InfoS("SERVER", message);
|
|
||||||
|
|
||||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Server announcement: {message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DispatchServerMessage(ICommonSession player, string message, bool suppressLog = false)
|
|
||||||
{
|
|
||||||
var wrappedMessage = Loc.GetString("chat-manager-server-wrap-message", ("message", FormattedMessage.EscapeText(message)));
|
|
||||||
ChatMessageToOne(ChatChannel.Server, message, wrappedMessage, default, false, player.Channel);
|
|
||||||
|
|
||||||
if (!suppressLog)
|
|
||||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Server message to {player:Player}: {message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SendAdminAnnouncement(string message, AdminFlags? flagBlacklist, AdminFlags? flagWhitelist)
|
|
||||||
{
|
|
||||||
var clients = _adminManager.ActiveAdmins.Where(p =>
|
|
||||||
{
|
|
||||||
var adminData = _adminManager.GetAdminData(p);
|
|
||||||
|
|
||||||
DebugTools.AssertNotNull(adminData);
|
|
||||||
|
|
||||||
if (adminData == null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (flagBlacklist != null && adminData.HasFlag(flagBlacklist.Value))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return flagWhitelist == null || adminData.HasFlag(flagWhitelist.Value);
|
|
||||||
|
|
||||||
}).Select(p => p.Channel);
|
|
||||||
|
|
||||||
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
|
|
||||||
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")), ("message", FormattedMessage.EscapeText(message)));
|
|
||||||
|
|
||||||
ChatMessageToMany(ChatChannel.Admin, message, wrappedMessage, default, false, true, clients);
|
|
||||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Admin announcement: {message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SendAdminAnnouncementMessage(ICommonSession player, string message, bool suppressLog = true)
|
|
||||||
{
|
|
||||||
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
|
|
||||||
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")),
|
|
||||||
("message", FormattedMessage.EscapeText(message)));
|
|
||||||
ChatMessageToOne(ChatChannel.Admin, message, wrappedMessage, default, false, player.Channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SendAdminAlert(string message)
|
|
||||||
{
|
|
||||||
var clients = _adminManager.ActiveAdmins.Select(p => p.Channel);
|
|
||||||
|
|
||||||
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
|
|
||||||
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")), ("message", FormattedMessage.EscapeText(message)));
|
|
||||||
|
|
||||||
ChatMessageToMany(ChatChannel.AdminAlert, message, wrappedMessage, default, false, true, clients);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SendAdminAlert(EntityUid player, string message)
|
|
||||||
{
|
|
||||||
var mindSystem = _entityManager.System<SharedMindSystem>();
|
|
||||||
if (!mindSystem.TryGetMind(player, out var mindId, out var mind))
|
|
||||||
{
|
|
||||||
SendAdminAlert(message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var adminSystem = _entityManager.System<AdminSystem>();
|
|
||||||
var antag = mind.UserId != null && (adminSystem.GetCachedPlayerInfo(mind.UserId.Value)?.Antag ?? false);
|
|
||||||
|
|
||||||
SendAdminAlert($"{mind.Session?.Name}{(antag ? " (ANTAG)" : "")} {message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SendHookOOC(string sender, string message)
|
|
||||||
{
|
|
||||||
if (!_oocEnabled && _configurationManager.GetCVar(CCVars.DisablingOOCDisablesRelay))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var wrappedMessage = Loc.GetString("chat-manager-send-hook-ooc-wrap-message", ("senderName", sender), ("message", FormattedMessage.EscapeText(message)));
|
|
||||||
ChatMessageToAll(ChatChannel.OOC, message, wrappedMessage, source: EntityUid.Invalid, hideChat: false, recordReplay: true);
|
|
||||||
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Hook OOC from {sender}: {message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Public OOC Chat API
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called for a player to attempt sending an OOC, out-of-game. message.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="player">The player sending the message.</param>
|
|
||||||
/// <param name="message">The message.</param>
|
|
||||||
/// <param name="type">The type of message.</param>
|
|
||||||
public void TrySendOOCMessage(ICommonSession player, string message, OOCChatType type)
|
|
||||||
{
|
|
||||||
if (HandleRateLimit(player) != RateLimitStatus.Allowed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Check if message exceeds the character limit
|
|
||||||
if (message.Length > MaxMessageLength)
|
|
||||||
{
|
|
||||||
DispatchServerMessage(player, Loc.GetString("chat-manager-max-message-length-exceeded-message", ("limit", MaxMessageLength)));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (type)
|
|
||||||
{
|
|
||||||
case OOCChatType.OOC:
|
|
||||||
SendOOC(player, message);
|
|
||||||
break;
|
|
||||||
case OOCChatType.Admin:
|
|
||||||
SendAdminChat(player, message);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Private API
|
|
||||||
|
|
||||||
private void SendOOC(ICommonSession player, string message)
|
|
||||||
{
|
|
||||||
if (_adminManager.IsAdmin(player))
|
|
||||||
{
|
|
||||||
if (!_adminOocEnabled)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (!_oocEnabled)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Color? colorOverride = null;
|
|
||||||
var wrappedMessage = Loc.GetString("chat-manager-send-ooc-wrap-message", ("playerName",player.Name), ("message", FormattedMessage.EscapeText(message)));
|
|
||||||
if (_adminManager.HasAdminFlag(player, AdminFlags.Admin))
|
|
||||||
{
|
|
||||||
var prefs = _preferencesManager.GetPreferences(player.UserId);
|
|
||||||
colorOverride = prefs.AdminOOCColor;
|
|
||||||
}
|
|
||||||
if ( _netConfigManager.GetClientCVar(player.Channel, CCVars.ShowOocPatronColor) && player.Channel.UserData.PatronTier is { } patron && PatronOocColors.TryGetValue(patron, out var patronColor))
|
|
||||||
{
|
|
||||||
wrappedMessage = Loc.GetString("chat-manager-send-ooc-patron-wrap-message", ("patronColor", patronColor),("playerName", player.Name), ("message", FormattedMessage.EscapeText(message)));
|
|
||||||
}
|
|
||||||
|
|
||||||
//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.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}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SendAdminChat(ICommonSession player, string message)
|
|
||||||
{
|
|
||||||
if (!_adminManager.IsAdmin(player))
|
|
||||||
{
|
|
||||||
_adminLogger.Add(LogType.Chat, LogImpact.Extreme, $"{player:Player} attempted to send admin message but was not admin");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var clients = _adminManager.ActiveAdmins.Select(p => p.Channel);
|
|
||||||
var wrappedMessage = Loc.GetString("chat-manager-send-admin-chat-wrap-message",
|
|
||||||
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")),
|
|
||||||
("playerName", player.Name), ("message", FormattedMessage.EscapeText(message)));
|
|
||||||
|
|
||||||
foreach (var client in clients)
|
|
||||||
{
|
|
||||||
var isSource = client != player.Channel;
|
|
||||||
ChatMessageToOne(ChatChannel.AdminChat,
|
|
||||||
message,
|
|
||||||
wrappedMessage,
|
|
||||||
default,
|
|
||||||
false,
|
|
||||||
client,
|
|
||||||
audioPath: isSource ? _netConfigManager.GetClientCVar(client, CCVars.AdminChatSoundPath) : default,
|
|
||||||
audioVolume: isSource ? _netConfigManager.GetClientCVar(client, CCVars.AdminChatSoundVolume) : default,
|
|
||||||
author: player.UserId);
|
|
||||||
}
|
|
||||||
|
|
||||||
_adminLogger.Add(LogType.Chat, $"Admin chat from {player:Player}: {message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Utility
|
|
||||||
|
|
||||||
public void ChatMessageToOne(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, INetChannel client, Color? colorOverride = null, bool recordReplay = false, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
|
|
||||||
{
|
|
||||||
var user = author == null ? null : EnsurePlayer(author);
|
|
||||||
var netSource = _entityManager.GetNetEntity(source);
|
|
||||||
user?.AddEntity(netSource);
|
|
||||||
|
|
||||||
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
|
|
||||||
_netManager.ServerSendMessage(new MsgChatMessage() { Message = msg }, client);
|
|
||||||
|
|
||||||
if (!recordReplay)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if ((channel & ChatChannel.AdminRelated) == 0 ||
|
|
||||||
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
|
|
||||||
{
|
|
||||||
_replay.RecordServerMessage(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ChatMessageToMany(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, IEnumerable<INetChannel> clients, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
|
|
||||||
=> ChatMessageToMany(channel, message, wrappedMessage, source, hideChat, recordReplay, clients.ToList(), colorOverride, audioPath, audioVolume, author);
|
|
||||||
|
|
||||||
public void ChatMessageToMany(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, List<INetChannel> clients, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
|
|
||||||
{
|
|
||||||
var user = author == null ? null : EnsurePlayer(author);
|
|
||||||
var netSource = _entityManager.GetNetEntity(source);
|
|
||||||
user?.AddEntity(netSource);
|
|
||||||
|
|
||||||
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
|
|
||||||
_netManager.ServerSendToMany(new MsgChatMessage() { Message = msg }, clients);
|
|
||||||
|
|
||||||
if (!recordReplay)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if ((channel & ChatChannel.AdminRelated) == 0 ||
|
|
||||||
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
|
|
||||||
{
|
|
||||||
_replay.RecordServerMessage(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ChatMessageToManyFiltered(Filter filter, ChatChannel channel, string message, string wrappedMessage, EntityUid source,
|
|
||||||
bool hideChat, bool recordReplay, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0)
|
|
||||||
{
|
|
||||||
if (!recordReplay && !filter.Recipients.Any())
|
|
||||||
return;
|
|
||||||
|
|
||||||
var clients = new List<INetChannel>();
|
|
||||||
foreach (var recipient in filter.Recipients)
|
|
||||||
{
|
|
||||||
clients.Add(recipient.Channel);
|
|
||||||
}
|
|
||||||
|
|
||||||
ChatMessageToMany(channel, message, wrappedMessage, source, hideChat, recordReplay, clients, colorOverride, audioPath, audioVolume);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ChatMessageToAll(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
|
|
||||||
{
|
|
||||||
var user = author == null ? null : EnsurePlayer(author);
|
|
||||||
var netSource = _entityManager.GetNetEntity(source);
|
|
||||||
user?.AddEntity(netSource);
|
|
||||||
|
|
||||||
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
|
|
||||||
_netManager.ServerSendToAll(new MsgChatMessage() { Message = msg });
|
|
||||||
|
|
||||||
if (!recordReplay)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if ((channel & ChatChannel.AdminRelated) == 0 ||
|
|
||||||
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
|
|
||||||
{
|
|
||||||
_replay.RecordServerMessage(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool MessageCharacterLimit(ICommonSession? player, string message)
|
|
||||||
{
|
|
||||||
var isOverLength = false;
|
|
||||||
|
|
||||||
// Non-players don't need to be checked.
|
|
||||||
if (player == null)
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Check if message exceeds the character limit if the sender is a player
|
if (flagBlacklist != null && adminData.HasFlag(flagBlacklist.Value))
|
||||||
if (message.Length > MaxMessageLength)
|
return false;
|
||||||
{
|
|
||||||
var feedback = Loc.GetString("chat-manager-max-message-length-exceeded-message", ("limit", MaxMessageLength));
|
|
||||||
|
|
||||||
DispatchServerMessage(player, feedback);
|
return flagWhitelist == null || adminData.HasFlag(flagWhitelist.Value);
|
||||||
|
|
||||||
isOverLength = true;
|
}).Select(p => p.Channel);
|
||||||
}
|
|
||||||
|
|
||||||
return isOverLength;
|
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
|
||||||
|
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")), ("message", FormattedMessage.EscapeText(message)));
|
||||||
|
|
||||||
|
ChatMessageToMany(ChatChannel.Admin, message, wrappedMessage, default, false, true, clients);
|
||||||
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Admin announcement: {message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendAdminAnnouncementMessage(ICommonSession player, string message, bool suppressLog = true)
|
||||||
|
{
|
||||||
|
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
|
||||||
|
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")),
|
||||||
|
("message", FormattedMessage.EscapeText(message)));
|
||||||
|
ChatMessageToOne(ChatChannel.Admin, message, wrappedMessage, default, false, player.Channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendAdminAlert(string message)
|
||||||
|
{
|
||||||
|
var clients = _adminManager.ActiveAdmins.Select(p => p.Channel);
|
||||||
|
|
||||||
|
var wrappedMessage = Loc.GetString("chat-manager-send-admin-announcement-wrap-message",
|
||||||
|
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")), ("message", FormattedMessage.EscapeText(message)));
|
||||||
|
|
||||||
|
ChatMessageToMany(ChatChannel.AdminAlert, message, wrappedMessage, default, false, true, clients);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SendAdminAlert(EntityUid player, string message)
|
||||||
|
{
|
||||||
|
var mindSystem = _entityManager.System<SharedMindSystem>();
|
||||||
|
if (!mindSystem.TryGetMind(player, out var mindId, out var mind))
|
||||||
|
{
|
||||||
|
SendAdminAlert(message);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
var adminSystem = _entityManager.System<AdminSystem>();
|
||||||
|
var antag = mind.UserId != null && (adminSystem.GetCachedPlayerInfo(mind.UserId.Value)?.Antag ?? false);
|
||||||
|
|
||||||
|
SendAdminAlert($"{mind.Session?.Name}{(antag ? " (ANTAG)" : "")} {message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum OOCChatType : byte
|
public void SendHookOOC(string sender, string message)
|
||||||
{
|
{
|
||||||
OOC,
|
if (!_oocEnabled && _configurationManager.GetCVar(CCVars.DisablingOOCDisablesRelay))
|
||||||
Admin
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var wrappedMessage = Loc.GetString("chat-manager-send-hook-ooc-wrap-message", ("senderName", sender), ("message", FormattedMessage.EscapeText(message)));
|
||||||
|
ChatMessageToAll(ChatChannel.OOC, message, wrappedMessage, source: EntityUid.Invalid, hideChat: false, recordReplay: true);
|
||||||
|
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Hook OOC from {sender}: {message}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Public OOC Chat API
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called for a player to attempt sending an OOC, out-of-game. message.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="player">The player sending the message.</param>
|
||||||
|
/// <param name="message">The message.</param>
|
||||||
|
/// <param name="type">The type of message.</param>
|
||||||
|
public void TrySendOOCMessage(ICommonSession player, string message, OOCChatType type)
|
||||||
|
{
|
||||||
|
if (HandleRateLimit(player) != RateLimitStatus.Allowed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Check if message exceeds the character limit
|
||||||
|
if (message.Length > MaxMessageLength)
|
||||||
|
{
|
||||||
|
DispatchServerMessage(player, Loc.GetString("chat-manager-max-message-length-exceeded-message", ("limit", MaxMessageLength)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case OOCChatType.OOC:
|
||||||
|
SendOOC(player, message);
|
||||||
|
break;
|
||||||
|
case OOCChatType.Admin:
|
||||||
|
SendAdminChat(player, message);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Private API
|
||||||
|
|
||||||
|
private void SendOOC(ICommonSession player, string message)
|
||||||
|
{
|
||||||
|
if (_adminManager.IsAdmin(player))
|
||||||
|
{
|
||||||
|
if (!_adminOocEnabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (!_oocEnabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Color? colorOverride = null;
|
||||||
|
var wrappedMessage = Loc.GetString("chat-manager-send-ooc-wrap-message", ("playerName",player.Name), ("message", FormattedMessage.EscapeText(message)));
|
||||||
|
if (_adminManager.HasAdminFlag(player, AdminFlags.Admin))
|
||||||
|
{
|
||||||
|
var prefs = _preferencesManager.GetPreferences(player.UserId);
|
||||||
|
colorOverride = prefs.AdminOOCColor;
|
||||||
|
}
|
||||||
|
if ( _netConfigManager.GetClientCVar(player.Channel, CCVars.ShowOocPatronColor) && player.Channel.UserData.PatronTier is { } patron && PatronOocColors.TryGetValue(patron, out var patronColor))
|
||||||
|
{
|
||||||
|
wrappedMessage = Loc.GetString("chat-manager-send-ooc-patron-wrap-message", ("patronColor", patronColor),("playerName", player.Name), ("message", FormattedMessage.EscapeText(message)));
|
||||||
|
}
|
||||||
|
|
||||||
|
//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.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}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SendAdminChat(ICommonSession player, string message)
|
||||||
|
{
|
||||||
|
if (!_adminManager.IsAdmin(player))
|
||||||
|
{
|
||||||
|
_adminLogger.Add(LogType.Chat, LogImpact.Extreme, $"{player:Player} attempted to send admin message but was not admin");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var clients = _adminManager.ActiveAdmins.Select(p => p.Channel);
|
||||||
|
var wrappedMessage = Loc.GetString("chat-manager-send-admin-chat-wrap-message",
|
||||||
|
("adminChannelName", Loc.GetString("chat-manager-admin-channel-name")),
|
||||||
|
("playerName", player.Name), ("message", FormattedMessage.EscapeText(message)));
|
||||||
|
|
||||||
|
foreach (var client in clients)
|
||||||
|
{
|
||||||
|
var isSource = client != player.Channel;
|
||||||
|
ChatMessageToOne(ChatChannel.AdminChat,
|
||||||
|
message,
|
||||||
|
wrappedMessage,
|
||||||
|
default,
|
||||||
|
false,
|
||||||
|
client,
|
||||||
|
audioPath: isSource ? _netConfigManager.GetClientCVar(client, CCVars.AdminChatSoundPath) : default,
|
||||||
|
audioVolume: isSource ? _netConfigManager.GetClientCVar(client, CCVars.AdminChatSoundVolume) : default,
|
||||||
|
author: player.UserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_adminLogger.Add(LogType.Chat, $"Admin chat from {player:Player}: {message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Utility
|
||||||
|
|
||||||
|
public void ChatMessageToOne(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, INetChannel client, Color? colorOverride = null, bool recordReplay = false, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
|
||||||
|
{
|
||||||
|
var user = author == null ? null : EnsurePlayer(author);
|
||||||
|
var netSource = _entityManager.GetNetEntity(source);
|
||||||
|
user?.AddEntity(netSource);
|
||||||
|
|
||||||
|
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
|
||||||
|
_netManager.ServerSendMessage(new MsgChatMessage() { Message = msg }, client);
|
||||||
|
|
||||||
|
if (!recordReplay)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if ((channel & ChatChannel.AdminRelated) == 0 ||
|
||||||
|
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
|
||||||
|
{
|
||||||
|
_replay.RecordServerMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChatMessageToMany(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, IEnumerable<INetChannel> clients, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
|
||||||
|
=> ChatMessageToMany(channel, message, wrappedMessage, source, hideChat, recordReplay, clients.ToList(), colorOverride, audioPath, audioVolume, author);
|
||||||
|
|
||||||
|
public void ChatMessageToMany(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, List<INetChannel> clients, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
|
||||||
|
{
|
||||||
|
var user = author == null ? null : EnsurePlayer(author);
|
||||||
|
var netSource = _entityManager.GetNetEntity(source);
|
||||||
|
user?.AddEntity(netSource);
|
||||||
|
|
||||||
|
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
|
||||||
|
_netManager.ServerSendToMany(new MsgChatMessage() { Message = msg }, clients);
|
||||||
|
|
||||||
|
if (!recordReplay)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if ((channel & ChatChannel.AdminRelated) == 0 ||
|
||||||
|
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
|
||||||
|
{
|
||||||
|
_replay.RecordServerMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChatMessageToManyFiltered(Filter filter, ChatChannel channel, string message, string wrappedMessage, EntityUid source,
|
||||||
|
bool hideChat, bool recordReplay, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0)
|
||||||
|
{
|
||||||
|
if (!recordReplay && !filter.Recipients.Any())
|
||||||
|
return;
|
||||||
|
|
||||||
|
var clients = new List<INetChannel>();
|
||||||
|
foreach (var recipient in filter.Recipients)
|
||||||
|
{
|
||||||
|
clients.Add(recipient.Channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatMessageToMany(channel, message, wrappedMessage, source, hideChat, recordReplay, clients, colorOverride, audioPath, audioVolume);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChatMessageToAll(ChatChannel channel, string message, string wrappedMessage, EntityUid source, bool hideChat, bool recordReplay, Color? colorOverride = null, string? audioPath = null, float audioVolume = 0, NetUserId? author = null)
|
||||||
|
{
|
||||||
|
var user = author == null ? null : EnsurePlayer(author);
|
||||||
|
var netSource = _entityManager.GetNetEntity(source);
|
||||||
|
user?.AddEntity(netSource);
|
||||||
|
|
||||||
|
var msg = new ChatMessage(channel, message, wrappedMessage, netSource, user?.Key, hideChat, colorOverride, audioPath, audioVolume);
|
||||||
|
_netManager.ServerSendToAll(new MsgChatMessage() { Message = msg });
|
||||||
|
|
||||||
|
if (!recordReplay)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if ((channel & ChatChannel.AdminRelated) == 0 ||
|
||||||
|
_configurationManager.GetCVar(CCVars.ReplayRecordAdminChat))
|
||||||
|
{
|
||||||
|
_replay.RecordServerMessage(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MessageCharacterLimit(ICommonSession? player, string message)
|
||||||
|
{
|
||||||
|
var isOverLength = false;
|
||||||
|
|
||||||
|
// Non-players don't need to be checked.
|
||||||
|
if (player == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Check if message exceeds the character limit if the sender is a player
|
||||||
|
if (message.Length > MaxMessageLength)
|
||||||
|
{
|
||||||
|
var feedback = Loc.GetString("chat-manager-max-message-length-exceeded-message", ("limit", MaxMessageLength));
|
||||||
|
|
||||||
|
DispatchServerMessage(player, feedback);
|
||||||
|
|
||||||
|
isOverLength = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isOverLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum OOCChatType : byte
|
||||||
|
{
|
||||||
|
OOC,
|
||||||
|
Admin
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
namespace Content.Server.Ghost
|
namespace Content.Server.Ghost;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is used to mark Observers properly, as they get Minds
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent]
|
||||||
|
public sealed partial class ObserverRoleComponent : Component
|
||||||
{
|
{
|
||||||
/// <summary>
|
public string Name => Loc.GetString("observer-role-name");
|
||||||
/// This is used to mark Observers properly, as they get Minds
|
|
||||||
/// </summary>
|
|
||||||
[RegisterComponent]
|
|
||||||
public sealed partial class ObserverRoleComponent : Component
|
|
||||||
{
|
|
||||||
public string Name => Loc.GetString("observer-role-name");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,102 +2,101 @@
|
|||||||
using Content.Server.Mind.Commands;
|
using Content.Server.Mind.Commands;
|
||||||
using Content.Shared.Roles;
|
using Content.Shared.Roles;
|
||||||
|
|
||||||
namespace Content.Server.Ghost.Roles.Components
|
namespace Content.Server.Ghost.Roles.Components;
|
||||||
|
|
||||||
|
[RegisterComponent]
|
||||||
|
[Access(typeof(GhostRoleSystem))]
|
||||||
|
public sealed partial class GhostRoleComponent : Component
|
||||||
{
|
{
|
||||||
[RegisterComponent]
|
[DataField("name")] private string _roleName = "Unknown";
|
||||||
[Access(typeof(GhostRoleSystem))]
|
|
||||||
public sealed partial class GhostRoleComponent : Component
|
[DataField("description")] private string _roleDescription = "Unknown";
|
||||||
|
|
||||||
|
[DataField("rules")] private string _roleRules = "ghost-role-component-default-rules";
|
||||||
|
|
||||||
|
// TODO ROLE TIMERS
|
||||||
|
// Actually make use of / enforce this requirement?
|
||||||
|
// Why is this even here.
|
||||||
|
// Move to ghost role prototype & respect CCvars.GameRoleTimerOverride
|
||||||
|
[DataField("requirements")]
|
||||||
|
public HashSet<JobRequirement>? Requirements;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the <see cref="MakeSentientCommand"/> should run on the mob.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)] [DataField("makeSentient")]
|
||||||
|
public bool MakeSentient = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The probability that this ghost role will be available after init.
|
||||||
|
/// Used mostly for takeover roles that want some probability of being takeover, but not 100%.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("prob")]
|
||||||
|
public float Probability = 1f;
|
||||||
|
|
||||||
|
// We do this so updating RoleName and RoleDescription in VV updates the open EUIs.
|
||||||
|
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
||||||
|
public string RoleName
|
||||||
{
|
{
|
||||||
[DataField("name")] private string _roleName = "Unknown";
|
get => Loc.GetString(_roleName);
|
||||||
|
set
|
||||||
[DataField("description")] private string _roleDescription = "Unknown";
|
|
||||||
|
|
||||||
[DataField("rules")] private string _roleRules = "ghost-role-component-default-rules";
|
|
||||||
|
|
||||||
// TODO ROLE TIMERS
|
|
||||||
// Actually make use of / enforce this requirement?
|
|
||||||
// Why is this even here.
|
|
||||||
// Move to ghost role prototype & respect CCvars.GameRoleTimerOverride
|
|
||||||
[DataField("requirements")]
|
|
||||||
public HashSet<JobRequirement>? Requirements;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the <see cref="MakeSentientCommand"/> should run on the mob.
|
|
||||||
/// </summary>
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)] [DataField("makeSentient")]
|
|
||||||
public bool MakeSentient = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The probability that this ghost role will be available after init.
|
|
||||||
/// Used mostly for takeover roles that want some probability of being takeover, but not 100%.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("prob")]
|
|
||||||
public float Probability = 1f;
|
|
||||||
|
|
||||||
// We do this so updating RoleName and RoleDescription in VV updates the open EUIs.
|
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
|
||||||
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
|
||||||
public string RoleName
|
|
||||||
{
|
{
|
||||||
get => Loc.GetString(_roleName);
|
_roleName = value;
|
||||||
set
|
IoCManager.Resolve<IEntityManager>().System<GhostRoleSystem>().UpdateAllEui();
|
||||||
{
|
|
||||||
_roleName = value;
|
|
||||||
IoCManager.Resolve<IEntityManager>().System<GhostRoleSystem>().UpdateAllEui();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
|
||||||
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
|
||||||
public string RoleDescription
|
|
||||||
{
|
|
||||||
get => Loc.GetString(_roleDescription);
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_roleDescription = value;
|
|
||||||
IoCManager.Resolve<IEntityManager>().System<GhostRoleSystem>().UpdateAllEui();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
|
||||||
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
|
||||||
public string RoleRules
|
|
||||||
{
|
|
||||||
get => Loc.GetString(_roleRules);
|
|
||||||
set
|
|
||||||
{
|
|
||||||
_roleRules = value;
|
|
||||||
IoCManager.Resolve<IEntityManager>().System<GhostRoleSystem>().UpdateAllEui();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[DataField("allowSpeech")]
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
|
||||||
public bool AllowSpeech { get; set; } = true;
|
|
||||||
|
|
||||||
[DataField("allowMovement")]
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
|
||||||
public bool AllowMovement { get; set; }
|
|
||||||
|
|
||||||
[ViewVariables(VVAccess.ReadOnly)]
|
|
||||||
public bool Taken { get; set; }
|
|
||||||
|
|
||||||
[ViewVariables]
|
|
||||||
public uint Identifier { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Reregisters the ghost role when the current player ghosts.
|
|
||||||
/// </summary>
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
|
||||||
[DataField("reregister")]
|
|
||||||
public bool ReregisterOnGhost { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If set, ghost role is raffled, otherwise it is first-come-first-serve.
|
|
||||||
/// </summary>
|
|
||||||
[DataField("raffle")]
|
|
||||||
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
|
||||||
public GhostRoleRaffleConfig? RaffleConfig { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
||||||
|
public string RoleDescription
|
||||||
|
{
|
||||||
|
get => Loc.GetString(_roleDescription);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_roleDescription = value;
|
||||||
|
IoCManager.Resolve<IEntityManager>().System<GhostRoleSystem>().UpdateAllEui();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
||||||
|
public string RoleRules
|
||||||
|
{
|
||||||
|
get => Loc.GetString(_roleRules);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_roleRules = value;
|
||||||
|
IoCManager.Resolve<IEntityManager>().System<GhostRoleSystem>().UpdateAllEui();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[DataField("allowSpeech")]
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public bool AllowSpeech { get; set; } = true;
|
||||||
|
|
||||||
|
[DataField("allowMovement")]
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
public bool AllowMovement { get; set; }
|
||||||
|
|
||||||
|
[ViewVariables(VVAccess.ReadOnly)]
|
||||||
|
public bool Taken { get; set; }
|
||||||
|
|
||||||
|
[ViewVariables]
|
||||||
|
public uint Identifier { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reregisters the ghost role when the current player ghosts.
|
||||||
|
/// </summary>
|
||||||
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
|
[DataField("reregister")]
|
||||||
|
public bool ReregisterOnGhost { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If set, ghost role is raffled, otherwise it is first-come-first-serve.
|
||||||
|
/// </summary>
|
||||||
|
[DataField("raffle")]
|
||||||
|
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
||||||
|
public GhostRoleRaffleConfig? RaffleConfig { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -37,245 +37,244 @@ using Content.Shared.Traits.Assorted;
|
|||||||
using Robust.Shared.Audio.Systems;
|
using Robust.Shared.Audio.Systems;
|
||||||
using Content.Shared.Ghost.Roles.Components;
|
using Content.Shared.Ghost.Roles.Components;
|
||||||
|
|
||||||
namespace Content.Server.Zombies
|
namespace Content.Server.Zombies;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Handles zombie propagation and inherent zombie traits
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Don't Shitcode Open Inside
|
||||||
|
/// </remarks>
|
||||||
|
public sealed partial class ZombieSystem
|
||||||
{
|
{
|
||||||
|
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
||||||
|
[Dependency] private readonly ServerInventorySystem _inventory = default!;
|
||||||
|
[Dependency] private readonly NpcFactionSystem _faction = default!;
|
||||||
|
[Dependency] private readonly NPCSystem _npc = default!;
|
||||||
|
[Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!;
|
||||||
|
[Dependency] private readonly IdentitySystem _identity = default!;
|
||||||
|
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
|
||||||
|
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
|
||||||
|
[Dependency] private readonly IChatManager _chatMan = default!;
|
||||||
|
[Dependency] private readonly MindSystem _mind = default!;
|
||||||
|
[Dependency] private readonly SharedRoleSystem _roles = default!;
|
||||||
|
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles zombie propagation and inherent zombie traits
|
/// Handles an entity turning into a zombie when they die or go into crit
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
private void OnDamageChanged(EntityUid uid, ZombifyOnDeathComponent component, MobStateChangedEvent args)
|
||||||
/// Don't Shitcode Open Inside
|
|
||||||
/// </remarks>
|
|
||||||
public sealed partial class ZombieSystem
|
|
||||||
{
|
{
|
||||||
[Dependency] private readonly SharedHandsSystem _hands = default!;
|
if (args.NewMobState == MobState.Dead)
|
||||||
[Dependency] private readonly ServerInventorySystem _inventory = default!;
|
|
||||||
[Dependency] private readonly NpcFactionSystem _faction = default!;
|
|
||||||
[Dependency] private readonly NPCSystem _npc = default!;
|
|
||||||
[Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!;
|
|
||||||
[Dependency] private readonly IdentitySystem _identity = default!;
|
|
||||||
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!;
|
|
||||||
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
|
|
||||||
[Dependency] private readonly IChatManager _chatMan = default!;
|
|
||||||
[Dependency] private readonly MindSystem _mind = default!;
|
|
||||||
[Dependency] private readonly SharedRoleSystem _roles = default!;
|
|
||||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Handles an entity turning into a zombie when they die or go into crit
|
|
||||||
/// </summary>
|
|
||||||
private void OnDamageChanged(EntityUid uid, ZombifyOnDeathComponent component, MobStateChangedEvent args)
|
|
||||||
{
|
{
|
||||||
if (args.NewMobState == MobState.Dead)
|
ZombifyEntity(uid, args.Component);
|
||||||
{
|
|
||||||
ZombifyEntity(uid, args.Component);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// This is the general purpose function to call if you want to zombify an entity.
|
|
||||||
/// It handles both humanoid and nonhumanoid transformation and everything should be called through it.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="target">the entity being zombified</param>
|
|
||||||
/// <param name="mobState"></param>
|
|
||||||
/// <remarks>
|
|
||||||
/// ALRIGHT BIG BOYS, GIRLS AND ANYONE ELSE. YOU'VE COME TO THE LAYER OF THE BEAST. THIS IS YOUR WARNING.
|
|
||||||
/// This function is the god function for zombie stuff, and it is cursed. I have
|
|
||||||
/// attempted to label everything thouroughly for your sanity. I have attempted to
|
|
||||||
/// rewrite this, but this is how it shall lie eternal. Turn back now.
|
|
||||||
/// -emo
|
|
||||||
/// </remarks>
|
|
||||||
public void ZombifyEntity(EntityUid target, MobStateComponent? mobState = null)
|
|
||||||
{
|
|
||||||
//Don't zombfiy zombies
|
|
||||||
if (HasComp<ZombieComponent>(target) || HasComp<ZombieImmuneComponent>(target))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (!Resolve(target, ref mobState, logMissing: false))
|
|
||||||
return;
|
|
||||||
|
|
||||||
//you're a real zombie now, son.
|
|
||||||
var zombiecomp = AddComp<ZombieComponent>(target);
|
|
||||||
|
|
||||||
//we need to basically remove all of these because zombies shouldn't
|
|
||||||
//get diseases, breath, be thirst, be hungry, die in space, have offspring or be paraplegic.
|
|
||||||
RemComp<RespiratorComponent>(target);
|
|
||||||
RemComp<BarotraumaComponent>(target);
|
|
||||||
RemComp<HungerComponent>(target);
|
|
||||||
RemComp<ThirstComponent>(target);
|
|
||||||
RemComp<ReproductiveComponent>(target);
|
|
||||||
RemComp<ReproductivePartnerComponent>(target);
|
|
||||||
RemComp<LegsParalyzedComponent>(target);
|
|
||||||
RemComp<ComplexInteractionComponent>(target);
|
|
||||||
|
|
||||||
//funny voice
|
|
||||||
var accentType = "zombie";
|
|
||||||
if (TryComp<ZombieAccentOverrideComponent>(target, out var accent))
|
|
||||||
accentType = accent.Accent;
|
|
||||||
|
|
||||||
EnsureComp<ReplacementAccentComponent>(target).Accent = accentType;
|
|
||||||
|
|
||||||
//This is needed for stupid entities that fuck up combat mode component
|
|
||||||
//in an attempt to make an entity not attack. This is the easiest way to do it.
|
|
||||||
var combat = EnsureComp<CombatModeComponent>(target);
|
|
||||||
RemComp<PacifiedComponent>(target);
|
|
||||||
_combat.SetCanDisarm(target, false, combat);
|
|
||||||
_combat.SetInCombatMode(target, true, combat);
|
|
||||||
|
|
||||||
//This is the actual damage of the zombie. We assign the visual appearance
|
|
||||||
//and range here because of stuff we'll find out later
|
|
||||||
var melee = EnsureComp<MeleeWeaponComponent>(target);
|
|
||||||
melee.Animation = zombiecomp.AttackAnimation;
|
|
||||||
melee.WideAnimation = zombiecomp.AttackAnimation;
|
|
||||||
melee.AltDisarm = false;
|
|
||||||
melee.Range = 1.2f;
|
|
||||||
melee.Angle = 0.0f;
|
|
||||||
melee.HitSound = zombiecomp.BiteSound;
|
|
||||||
|
|
||||||
if (mobState.CurrentState == MobState.Alive)
|
|
||||||
{
|
|
||||||
// Groaning when damaged
|
|
||||||
EnsureComp<EmoteOnDamageComponent>(target);
|
|
||||||
_emoteOnDamage.AddEmote(target, "Scream");
|
|
||||||
|
|
||||||
// Random groaning
|
|
||||||
EnsureComp<AutoEmoteComponent>(target);
|
|
||||||
_autoEmote.AddEmote(target, "ZombieGroan");
|
|
||||||
}
|
|
||||||
|
|
||||||
//We have specific stuff for humanoid zombies because they matter more
|
|
||||||
if (TryComp<HumanoidAppearanceComponent>(target, out var huApComp)) //huapcomp
|
|
||||||
{
|
|
||||||
//store some values before changing them in case the humanoid get cloned later
|
|
||||||
zombiecomp.BeforeZombifiedSkinColor = huApComp.SkinColor;
|
|
||||||
zombiecomp.BeforeZombifiedEyeColor = huApComp.EyeColor;
|
|
||||||
zombiecomp.BeforeZombifiedCustomBaseLayers = new(huApComp.CustomBaseLayers);
|
|
||||||
if (TryComp<BloodstreamComponent>(target, out var stream))
|
|
||||||
zombiecomp.BeforeZombifiedBloodReagent = stream.BloodReagent;
|
|
||||||
|
|
||||||
_humanoidAppearance.SetSkinColor(target, zombiecomp.SkinColor, verify: false, humanoid: huApComp);
|
|
||||||
|
|
||||||
// Messing with the eye layer made it vanish upon cloning, and also it didn't even appear right
|
|
||||||
huApComp.EyeColor = zombiecomp.EyeColor;
|
|
||||||
|
|
||||||
// this might not resync on clone?
|
|
||||||
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.Tail, zombiecomp.BaseLayerExternal, humanoid: huApComp);
|
|
||||||
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadSide, zombiecomp.BaseLayerExternal, humanoid: huApComp);
|
|
||||||
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadTop, zombiecomp.BaseLayerExternal, humanoid: huApComp);
|
|
||||||
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.Snout, zombiecomp.BaseLayerExternal, humanoid: huApComp);
|
|
||||||
|
|
||||||
//This is done here because non-humanoids shouldn't get baller damage
|
|
||||||
//lord forgive me for the hardcoded damage
|
|
||||||
DamageSpecifier dspec = new()
|
|
||||||
{
|
|
||||||
DamageDict = new()
|
|
||||||
{
|
|
||||||
{ "Slash", 13 },
|
|
||||||
{ "Piercing", 7 },
|
|
||||||
{ "Structural", 10 }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
melee.Damage = dspec;
|
|
||||||
|
|
||||||
// humanoid zombies get to pry open doors and shit
|
|
||||||
var pryComp = EnsureComp<PryingComponent>(target);
|
|
||||||
pryComp.SpeedModifier = 0.75f;
|
|
||||||
pryComp.PryPowered = true;
|
|
||||||
pryComp.Force = true;
|
|
||||||
|
|
||||||
Dirty(target, pryComp);
|
|
||||||
}
|
|
||||||
|
|
||||||
Dirty(target, melee);
|
|
||||||
|
|
||||||
//The zombie gets the assigned damage weaknesses and strengths
|
|
||||||
_damageable.SetDamageModifierSetId(target, "Zombie");
|
|
||||||
|
|
||||||
//This makes it so the zombie doesn't take bloodloss damage.
|
|
||||||
//NOTE: they are supposed to bleed, just not take damage
|
|
||||||
_bloodstream.SetBloodLossThreshold(target, 0f);
|
|
||||||
//Give them zombie blood
|
|
||||||
_bloodstream.ChangeBloodReagent(target, zombiecomp.NewBloodReagent);
|
|
||||||
|
|
||||||
//This is specifically here to combat insuls, because frying zombies on grilles is funny as shit.
|
|
||||||
_inventory.TryUnequip(target, "gloves", true, true);
|
|
||||||
//Should prevent instances of zombies using comms for information they shouldnt be able to have.
|
|
||||||
_inventory.TryUnequip(target, "ears", true, true);
|
|
||||||
|
|
||||||
//popup
|
|
||||||
_popup.PopupEntity(Loc.GetString("zombie-transform", ("target", target)), target, PopupType.LargeCaution);
|
|
||||||
|
|
||||||
//Make it sentient if it's an animal or something
|
|
||||||
MakeSentientCommand.MakeSentient(target, EntityManager);
|
|
||||||
|
|
||||||
//Make the zombie not die in the cold. Good for space zombies
|
|
||||||
if (TryComp<TemperatureComponent>(target, out var tempComp))
|
|
||||||
tempComp.ColdDamage.ClampMax(0);
|
|
||||||
|
|
||||||
//Heals the zombie from all the damage it took while human
|
|
||||||
if (TryComp<DamageableComponent>(target, out var damageablecomp))
|
|
||||||
_damageable.SetAllDamage(target, damageablecomp, 0);
|
|
||||||
_mobState.ChangeMobState(target, MobState.Alive);
|
|
||||||
|
|
||||||
_faction.ClearFactions(target, dirty: false);
|
|
||||||
_faction.AddFaction(target, "Zombie");
|
|
||||||
|
|
||||||
//gives it the funny "Zombie ___" name.
|
|
||||||
_nameMod.RefreshNameModifiers(target);
|
|
||||||
|
|
||||||
_identity.QueueIdentityUpdate(target);
|
|
||||||
|
|
||||||
var htn = EnsureComp<HTNComponent>(target);
|
|
||||||
htn.RootTask = new HTNCompoundTask() { Task = "SimpleHostileCompound" };
|
|
||||||
htn.Blackboard.SetValue(NPCBlackboard.Owner, target);
|
|
||||||
_npc.SleepNPC(target, htn);
|
|
||||||
|
|
||||||
//He's gotta have a mind
|
|
||||||
var hasMind = _mind.TryGetMind(target, out var mindId, out _);
|
|
||||||
if (hasMind && _mind.TryGetSession(mindId, out var session))
|
|
||||||
{
|
|
||||||
//Zombie role for player manifest
|
|
||||||
_roles.MindAddRole(mindId, new ZombieRoleComponent { PrototypeId = zombiecomp.ZombieRoleId });
|
|
||||||
|
|
||||||
//Greeting message for new bebe zombers
|
|
||||||
_chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting"));
|
|
||||||
|
|
||||||
// Notificate player about new role assignment
|
|
||||||
_audio.PlayGlobal(zombiecomp.GreetSoundNotification, session);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_npc.WakeNPC(target, htn);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!HasComp<GhostRoleMobSpawnerComponent>(target) && !hasMind) //this specific component gives build test trouble so pop off, ig
|
|
||||||
{
|
|
||||||
//yet more hardcoding. Visit zombie.ftl for more information.
|
|
||||||
var ghostRole = EnsureComp<GhostRoleComponent>(target);
|
|
||||||
EnsureComp<GhostTakeoverAvailableComponent>(target);
|
|
||||||
ghostRole.RoleName = Loc.GetString("zombie-generic");
|
|
||||||
ghostRole.RoleDescription = Loc.GetString("zombie-role-desc");
|
|
||||||
ghostRole.RoleRules = Loc.GetString("zombie-role-rules");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TryComp<HandsComponent>(target, out var handsComp))
|
|
||||||
{
|
|
||||||
_hands.RemoveHands(target);
|
|
||||||
RemComp(target, handsComp);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sloth: What the fuck?
|
|
||||||
// How long until compregistry lmao.
|
|
||||||
RemComp<PullerComponent>(target);
|
|
||||||
|
|
||||||
// No longer waiting to become a zombie:
|
|
||||||
// Requires deferral because this is (probably) the event which called ZombifyEntity in the first place.
|
|
||||||
RemCompDeferred<PendingZombieComponent>(target);
|
|
||||||
|
|
||||||
//zombie gamemode stuff
|
|
||||||
var ev = new EntityZombifiedEvent(target);
|
|
||||||
RaiseLocalEvent(target, ref ev, true);
|
|
||||||
//zombies get slowdown once they convert
|
|
||||||
_movementSpeedModifier.RefreshMovementSpeedModifiers(target);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This is the general purpose function to call if you want to zombify an entity.
|
||||||
|
/// It handles both humanoid and nonhumanoid transformation and everything should be called through it.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="target">the entity being zombified</param>
|
||||||
|
/// <param name="mobState"></param>
|
||||||
|
/// <remarks>
|
||||||
|
/// ALRIGHT BIG BOYS, GIRLS AND ANYONE ELSE. YOU'VE COME TO THE LAYER OF THE BEAST. THIS IS YOUR WARNING.
|
||||||
|
/// This function is the god function for zombie stuff, and it is cursed. I have
|
||||||
|
/// attempted to label everything thouroughly for your sanity. I have attempted to
|
||||||
|
/// rewrite this, but this is how it shall lie eternal. Turn back now.
|
||||||
|
/// -emo
|
||||||
|
/// </remarks>
|
||||||
|
public void ZombifyEntity(EntityUid target, MobStateComponent? mobState = null)
|
||||||
|
{
|
||||||
|
//Don't zombfiy zombies
|
||||||
|
if (HasComp<ZombieComponent>(target) || HasComp<ZombieImmuneComponent>(target))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (!Resolve(target, ref mobState, logMissing: false))
|
||||||
|
return;
|
||||||
|
|
||||||
|
//you're a real zombie now, son.
|
||||||
|
var zombiecomp = AddComp<ZombieComponent>(target);
|
||||||
|
|
||||||
|
//we need to basically remove all of these because zombies shouldn't
|
||||||
|
//get diseases, breath, be thirst, be hungry, die in space, have offspring or be paraplegic.
|
||||||
|
RemComp<RespiratorComponent>(target);
|
||||||
|
RemComp<BarotraumaComponent>(target);
|
||||||
|
RemComp<HungerComponent>(target);
|
||||||
|
RemComp<ThirstComponent>(target);
|
||||||
|
RemComp<ReproductiveComponent>(target);
|
||||||
|
RemComp<ReproductivePartnerComponent>(target);
|
||||||
|
RemComp<LegsParalyzedComponent>(target);
|
||||||
|
RemComp<ComplexInteractionComponent>(target);
|
||||||
|
|
||||||
|
//funny voice
|
||||||
|
var accentType = "zombie";
|
||||||
|
if (TryComp<ZombieAccentOverrideComponent>(target, out var accent))
|
||||||
|
accentType = accent.Accent;
|
||||||
|
|
||||||
|
EnsureComp<ReplacementAccentComponent>(target).Accent = accentType;
|
||||||
|
|
||||||
|
//This is needed for stupid entities that fuck up combat mode component
|
||||||
|
//in an attempt to make an entity not attack. This is the easiest way to do it.
|
||||||
|
var combat = EnsureComp<CombatModeComponent>(target);
|
||||||
|
RemComp<PacifiedComponent>(target);
|
||||||
|
_combat.SetCanDisarm(target, false, combat);
|
||||||
|
_combat.SetInCombatMode(target, true, combat);
|
||||||
|
|
||||||
|
//This is the actual damage of the zombie. We assign the visual appearance
|
||||||
|
//and range here because of stuff we'll find out later
|
||||||
|
var melee = EnsureComp<MeleeWeaponComponent>(target);
|
||||||
|
melee.Animation = zombiecomp.AttackAnimation;
|
||||||
|
melee.WideAnimation = zombiecomp.AttackAnimation;
|
||||||
|
melee.AltDisarm = false;
|
||||||
|
melee.Range = 1.2f;
|
||||||
|
melee.Angle = 0.0f;
|
||||||
|
melee.HitSound = zombiecomp.BiteSound;
|
||||||
|
|
||||||
|
if (mobState.CurrentState == MobState.Alive)
|
||||||
|
{
|
||||||
|
// Groaning when damaged
|
||||||
|
EnsureComp<EmoteOnDamageComponent>(target);
|
||||||
|
_emoteOnDamage.AddEmote(target, "Scream");
|
||||||
|
|
||||||
|
// Random groaning
|
||||||
|
EnsureComp<AutoEmoteComponent>(target);
|
||||||
|
_autoEmote.AddEmote(target, "ZombieGroan");
|
||||||
|
}
|
||||||
|
|
||||||
|
//We have specific stuff for humanoid zombies because they matter more
|
||||||
|
if (TryComp<HumanoidAppearanceComponent>(target, out var huApComp)) //huapcomp
|
||||||
|
{
|
||||||
|
//store some values before changing them in case the humanoid get cloned later
|
||||||
|
zombiecomp.BeforeZombifiedSkinColor = huApComp.SkinColor;
|
||||||
|
zombiecomp.BeforeZombifiedEyeColor = huApComp.EyeColor;
|
||||||
|
zombiecomp.BeforeZombifiedCustomBaseLayers = new(huApComp.CustomBaseLayers);
|
||||||
|
if (TryComp<BloodstreamComponent>(target, out var stream))
|
||||||
|
zombiecomp.BeforeZombifiedBloodReagent = stream.BloodReagent;
|
||||||
|
|
||||||
|
_humanoidAppearance.SetSkinColor(target, zombiecomp.SkinColor, verify: false, humanoid: huApComp);
|
||||||
|
|
||||||
|
// Messing with the eye layer made it vanish upon cloning, and also it didn't even appear right
|
||||||
|
huApComp.EyeColor = zombiecomp.EyeColor;
|
||||||
|
|
||||||
|
// this might not resync on clone?
|
||||||
|
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.Tail, zombiecomp.BaseLayerExternal, humanoid: huApComp);
|
||||||
|
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadSide, zombiecomp.BaseLayerExternal, humanoid: huApComp);
|
||||||
|
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.HeadTop, zombiecomp.BaseLayerExternal, humanoid: huApComp);
|
||||||
|
_humanoidAppearance.SetBaseLayerId(target, HumanoidVisualLayers.Snout, zombiecomp.BaseLayerExternal, humanoid: huApComp);
|
||||||
|
|
||||||
|
//This is done here because non-humanoids shouldn't get baller damage
|
||||||
|
//lord forgive me for the hardcoded damage
|
||||||
|
DamageSpecifier dspec = new()
|
||||||
|
{
|
||||||
|
DamageDict = new()
|
||||||
|
{
|
||||||
|
{ "Slash", 13 },
|
||||||
|
{ "Piercing", 7 },
|
||||||
|
{ "Structural", 10 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
melee.Damage = dspec;
|
||||||
|
|
||||||
|
// humanoid zombies get to pry open doors and shit
|
||||||
|
var pryComp = EnsureComp<PryingComponent>(target);
|
||||||
|
pryComp.SpeedModifier = 0.75f;
|
||||||
|
pryComp.PryPowered = true;
|
||||||
|
pryComp.Force = true;
|
||||||
|
|
||||||
|
Dirty(target, pryComp);
|
||||||
|
}
|
||||||
|
|
||||||
|
Dirty(target, melee);
|
||||||
|
|
||||||
|
//The zombie gets the assigned damage weaknesses and strengths
|
||||||
|
_damageable.SetDamageModifierSetId(target, "Zombie");
|
||||||
|
|
||||||
|
//This makes it so the zombie doesn't take bloodloss damage.
|
||||||
|
//NOTE: they are supposed to bleed, just not take damage
|
||||||
|
_bloodstream.SetBloodLossThreshold(target, 0f);
|
||||||
|
//Give them zombie blood
|
||||||
|
_bloodstream.ChangeBloodReagent(target, zombiecomp.NewBloodReagent);
|
||||||
|
|
||||||
|
//This is specifically here to combat insuls, because frying zombies on grilles is funny as shit.
|
||||||
|
_inventory.TryUnequip(target, "gloves", true, true);
|
||||||
|
//Should prevent instances of zombies using comms for information they shouldnt be able to have.
|
||||||
|
_inventory.TryUnequip(target, "ears", true, true);
|
||||||
|
|
||||||
|
//popup
|
||||||
|
_popup.PopupEntity(Loc.GetString("zombie-transform", ("target", target)), target, PopupType.LargeCaution);
|
||||||
|
|
||||||
|
//Make it sentient if it's an animal or something
|
||||||
|
MakeSentientCommand.MakeSentient(target, EntityManager);
|
||||||
|
|
||||||
|
//Make the zombie not die in the cold. Good for space zombies
|
||||||
|
if (TryComp<TemperatureComponent>(target, out var tempComp))
|
||||||
|
tempComp.ColdDamage.ClampMax(0);
|
||||||
|
|
||||||
|
//Heals the zombie from all the damage it took while human
|
||||||
|
if (TryComp<DamageableComponent>(target, out var damageablecomp))
|
||||||
|
_damageable.SetAllDamage(target, damageablecomp, 0);
|
||||||
|
_mobState.ChangeMobState(target, MobState.Alive);
|
||||||
|
|
||||||
|
_faction.ClearFactions(target, dirty: false);
|
||||||
|
_faction.AddFaction(target, "Zombie");
|
||||||
|
|
||||||
|
//gives it the funny "Zombie ___" name.
|
||||||
|
_nameMod.RefreshNameModifiers(target);
|
||||||
|
|
||||||
|
_identity.QueueIdentityUpdate(target);
|
||||||
|
|
||||||
|
var htn = EnsureComp<HTNComponent>(target);
|
||||||
|
htn.RootTask = new HTNCompoundTask() { Task = "SimpleHostileCompound" };
|
||||||
|
htn.Blackboard.SetValue(NPCBlackboard.Owner, target);
|
||||||
|
_npc.SleepNPC(target, htn);
|
||||||
|
|
||||||
|
//He's gotta have a mind
|
||||||
|
var hasMind = _mind.TryGetMind(target, out var mindId, out _);
|
||||||
|
if (hasMind && _mind.TryGetSession(mindId, out var session))
|
||||||
|
{
|
||||||
|
//Zombie role for player manifest
|
||||||
|
_roles.MindAddRole(mindId, new ZombieRoleComponent { PrototypeId = zombiecomp.ZombieRoleId });
|
||||||
|
|
||||||
|
//Greeting message for new bebe zombers
|
||||||
|
_chatMan.DispatchServerMessage(session, Loc.GetString("zombie-infection-greeting"));
|
||||||
|
|
||||||
|
// Notificate player about new role assignment
|
||||||
|
_audio.PlayGlobal(zombiecomp.GreetSoundNotification, session);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_npc.WakeNPC(target, htn);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!HasComp<GhostRoleMobSpawnerComponent>(target) && !hasMind) //this specific component gives build test trouble so pop off, ig
|
||||||
|
{
|
||||||
|
//yet more hardcoding. Visit zombie.ftl for more information.
|
||||||
|
var ghostRole = EnsureComp<GhostRoleComponent>(target);
|
||||||
|
EnsureComp<GhostTakeoverAvailableComponent>(target);
|
||||||
|
ghostRole.RoleName = Loc.GetString("zombie-generic");
|
||||||
|
ghostRole.RoleDescription = Loc.GetString("zombie-role-desc");
|
||||||
|
ghostRole.RoleRules = Loc.GetString("zombie-role-rules");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryComp<HandsComponent>(target, out var handsComp))
|
||||||
|
{
|
||||||
|
_hands.RemoveHands(target);
|
||||||
|
RemComp(target, handsComp);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sloth: What the fuck?
|
||||||
|
// How long until compregistry lmao.
|
||||||
|
RemComp<PullerComponent>(target);
|
||||||
|
|
||||||
|
// No longer waiting to become a zombie:
|
||||||
|
// Requires deferral because this is (probably) the event which called ZombifyEntity in the first place.
|
||||||
|
RemCompDeferred<PendingZombieComponent>(target);
|
||||||
|
|
||||||
|
//zombie gamemode stuff
|
||||||
|
var ev = new EntityZombifiedEvent(target);
|
||||||
|
RaiseLocalEvent(target, ref ev, true);
|
||||||
|
//zombies get slowdown once they convert
|
||||||
|
_movementSpeedModifier.RefreshMovementSpeedModifiers(target);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
using Robust.Shared.Serialization;
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
namespace Content.Shared.Administration.Events
|
namespace Content.Shared.Administration.Events;
|
||||||
|
|
||||||
|
[NetSerializable, Serializable]
|
||||||
|
public sealed class PlayerInfoChangedEvent : EntityEventArgs
|
||||||
{
|
{
|
||||||
[NetSerializable, Serializable]
|
public PlayerInfo? PlayerInfo;
|
||||||
public sealed class PlayerInfoChangedEvent : EntityEventArgs
|
|
||||||
{
|
|
||||||
public PlayerInfo? PlayerInfo;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,35 @@
|
|||||||
using Robust.Shared.Network;
|
using Robust.Shared.Network;
|
||||||
using Robust.Shared.Serialization;
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
namespace Content.Shared.Administration
|
namespace Content.Shared.Administration;
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed record PlayerInfo(
|
||||||
|
string Username,
|
||||||
|
string CharacterName,
|
||||||
|
string IdentityName,
|
||||||
|
string StartingJob,
|
||||||
|
bool Antag,
|
||||||
|
NetEntity? NetEntity,
|
||||||
|
NetUserId SessionId,
|
||||||
|
bool Connected,
|
||||||
|
bool ActiveThisRound,
|
||||||
|
TimeSpan? OverallPlaytime)
|
||||||
{
|
{
|
||||||
[Serializable, NetSerializable]
|
private string? _playtimeString;
|
||||||
public sealed record PlayerInfo(
|
|
||||||
string Username,
|
public bool IsPinned { get; set; }
|
||||||
string CharacterName,
|
|
||||||
string IdentityName,
|
public string PlaytimeString => _playtimeString ??=
|
||||||
string StartingJob,
|
OverallPlaytime?.ToString("%d':'hh':'mm") ?? Loc.GetString("generic-unknown-title");
|
||||||
bool Antag,
|
|
||||||
NetEntity? NetEntity,
|
public bool Equals(PlayerInfo? other)
|
||||||
NetUserId SessionId,
|
|
||||||
bool Connected,
|
|
||||||
bool ActiveThisRound,
|
|
||||||
TimeSpan? OverallPlaytime)
|
|
||||||
{
|
{
|
||||||
private string? _playtimeString;
|
return other?.SessionId == SessionId;
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsPinned { get; set; }
|
public override int GetHashCode()
|
||||||
|
{
|
||||||
public string PlaytimeString => _playtimeString ??=
|
return SessionId.GetHashCode();
|
||||||
OverallPlaytime?.ToString("%d':'hh':'mm") ?? Loc.GetString("generic-unknown-title");
|
|
||||||
|
|
||||||
public bool Equals(PlayerInfo? other)
|
|
||||||
{
|
|
||||||
return other?.SessionId == SessionId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return SessionId.GetHashCode();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
using Robust.Shared.Serialization;
|
using Robust.Shared.Serialization;
|
||||||
|
|
||||||
namespace Content.Shared.Ghost.Roles
|
namespace Content.Shared.Ghost.Roles;
|
||||||
|
|
||||||
|
[Serializable, NetSerializable]
|
||||||
|
public sealed class GhostRole
|
||||||
{
|
{
|
||||||
[Serializable, NetSerializable]
|
public string Name { get; set; } = string.Empty;
|
||||||
public sealed class GhostRole
|
public string Description { get; set; } = string.Empty;
|
||||||
{
|
public NetEntity Id;
|
||||||
public string Name { get; set; } = string.Empty;
|
|
||||||
public string Description { get; set; } = string.Empty;
|
|
||||||
public NetEntity Id;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,97 +1,96 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Robust.Shared.GameStates;
|
using Robust.Shared.GameStates;
|
||||||
|
|
||||||
namespace Content.Shared.Mind.Components
|
namespace Content.Shared.Mind.Components;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This component indicates that this entity may have mind, which is simply an entity with a <see cref="MindComponent"/>.
|
||||||
|
/// The mind entity is not actually stored in a "container", but is simply stored in nullspace.
|
||||||
|
/// </summary>
|
||||||
|
[RegisterComponent, Access(typeof(SharedMindSystem)), NetworkedComponent, AutoGenerateComponentState]
|
||||||
|
public sealed partial class MindContainerComponent : Component
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This component indicates that this entity may have mind, which is simply an entity with a <see cref="MindComponent"/>.
|
/// The mind controlling this mob. Can be null.
|
||||||
/// The mind entity is not actually stored in a "container", but is simply stored in nullspace.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[RegisterComponent, Access(typeof(SharedMindSystem)), NetworkedComponent, AutoGenerateComponentState]
|
[DataField, AutoNetworkedField]
|
||||||
public sealed partial class MindContainerComponent : Component
|
[Access(typeof(SharedMindSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
||||||
{
|
public EntityUid? Mind { get; set; }
|
||||||
/// <summary>
|
|
||||||
/// The mind controlling this mob. Can be null.
|
|
||||||
/// </summary>
|
|
||||||
[DataField, AutoNetworkedField]
|
|
||||||
[Access(typeof(SharedMindSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
|
||||||
public EntityUid? Mind { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// True if we have a mind, false otherwise.
|
|
||||||
/// </summary>
|
|
||||||
[MemberNotNullWhen(true, nameof(Mind))]
|
|
||||||
public bool HasMind => Mind != null;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether examining should show information about the mind or not.
|
|
||||||
/// </summary>
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
|
||||||
[DataField("showExamineInfo"), AutoNetworkedField]
|
|
||||||
public bool ShowExamineInfo { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the mind will be put on a ghost after this component is shutdown.
|
|
||||||
/// </summary>
|
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
|
||||||
[DataField("ghostOnShutdown")]
|
|
||||||
[Access(typeof(SharedMindSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
|
||||||
public bool GhostOnShutdown { get; set; } = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract class MindEvent : EntityEventArgs
|
|
||||||
{
|
|
||||||
public readonly Entity<MindComponent> Mind;
|
|
||||||
public readonly Entity<MindContainerComponent> Container;
|
|
||||||
|
|
||||||
public MindEvent(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
|
|
||||||
{
|
|
||||||
Mind = mind;
|
|
||||||
Container = container;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event raised directed at a mind-container when a mind gets removed.
|
/// True if we have a mind, false otherwise.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class MindRemovedMessage : MindEvent
|
[MemberNotNullWhen(true, nameof(Mind))]
|
||||||
{
|
public bool HasMind => Mind != null;
|
||||||
public MindRemovedMessage(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
|
|
||||||
: base(mind, container)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event raised directed at a mind when it gets removed from a mind-container.
|
/// Whether examining should show information about the mind or not.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class MindGotRemovedEvent : MindEvent
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
{
|
[DataField("showExamineInfo"), AutoNetworkedField]
|
||||||
public MindGotRemovedEvent(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
|
public bool ShowExamineInfo { get; set; }
|
||||||
: base(mind, container)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event raised directed at a mind-container when a mind gets added.
|
/// Whether the mind will be put on a ghost after this component is shutdown.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class MindAddedMessage : MindEvent
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
{
|
[DataField("ghostOnShutdown")]
|
||||||
public MindAddedMessage(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
|
[Access(typeof(SharedMindSystem), Other = AccessPermissions.ReadWriteExecute)] // FIXME Friends
|
||||||
: base(mind, container)
|
public bool GhostOnShutdown { get; set; } = true;
|
||||||
{
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
public abstract class MindEvent : EntityEventArgs
|
||||||
/// Event raised directed at a mind when it gets added to a mind-container.
|
{
|
||||||
/// </summary>
|
public readonly Entity<MindComponent> Mind;
|
||||||
public sealed class MindGotAddedEvent : MindEvent
|
public readonly Entity<MindContainerComponent> Container;
|
||||||
|
|
||||||
|
public MindEvent(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
|
||||||
|
{
|
||||||
|
Mind = mind;
|
||||||
|
Container = container;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event raised directed at a mind-container when a mind gets removed.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MindRemovedMessage : MindEvent
|
||||||
|
{
|
||||||
|
public MindRemovedMessage(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
|
||||||
|
: base(mind, container)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event raised directed at a mind when it gets removed from a mind-container.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MindGotRemovedEvent : MindEvent
|
||||||
|
{
|
||||||
|
public MindGotRemovedEvent(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
|
||||||
|
: base(mind, container)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event raised directed at a mind-container when a mind gets added.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MindAddedMessage : MindEvent
|
||||||
|
{
|
||||||
|
public MindAddedMessage(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
|
||||||
|
: base(mind, container)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event raised directed at a mind when it gets added to a mind-container.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MindGotAddedEvent : MindEvent
|
||||||
|
{
|
||||||
|
public MindGotAddedEvent(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
|
||||||
|
: base(mind, container)
|
||||||
{
|
{
|
||||||
public MindGotAddedEvent(Entity<MindComponent> mind, Entity<MindContainerComponent> container)
|
|
||||||
: base(mind, container)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,105 +5,104 @@ using Robust.Shared.GameStates;
|
|||||||
using Robust.Shared.Network;
|
using Robust.Shared.Network;
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
|
|
||||||
namespace Content.Shared.Mind
|
namespace Content.Shared.Mind;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This component stores information about a player/mob mind. The component will be attached to a mind-entity
|
||||||
|
/// which is stored in null-space. The entity that is currently "possessed" by the mind will have a
|
||||||
|
/// <see cref="MindContainerComponent"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Roles are attached as components on the mind-entity entity.
|
||||||
|
/// Think of it like this: if a player is supposed to have their memories,
|
||||||
|
/// their mind follows along.
|
||||||
|
///
|
||||||
|
/// Things such as respawning do not follow, because you're a new character.
|
||||||
|
/// Getting borged, cloned, turned into a catbeast, etc... will keep it following you.
|
||||||
|
///
|
||||||
|
/// Minds are stored in null-space, and are thus generally not set to players unless that player is the owner
|
||||||
|
/// of the mind. As a result it should be safe to network "secret" information like roles & objectives
|
||||||
|
/// </remarks>
|
||||||
|
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
|
||||||
|
public sealed partial class MindComponent : Component
|
||||||
{
|
{
|
||||||
|
[DataField, AutoNetworkedField]
|
||||||
|
public List<EntityUid> Objectives = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This component stores information about a player/mob mind. The component will be attached to a mind-entity
|
/// The session ID of the player owning this mind.
|
||||||
/// which is stored in null-space. The entity that is currently "possessed" by the mind will have a
|
|
||||||
/// <see cref="MindContainerComponent"/>.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
[DataField, AutoNetworkedField, Access(typeof(SharedMindSystem))]
|
||||||
/// Roles are attached as components on the mind-entity entity.
|
public NetUserId? UserId { get; set; }
|
||||||
/// Think of it like this: if a player is supposed to have their memories,
|
|
||||||
/// their mind follows along.
|
|
||||||
///
|
|
||||||
/// Things such as respawning do not follow, because you're a new character.
|
|
||||||
/// Getting borged, cloned, turned into a catbeast, etc... will keep it following you.
|
|
||||||
///
|
|
||||||
/// Minds are stored in null-space, and are thus generally not set to players unless that player is the owner
|
|
||||||
/// of the mind. As a result it should be safe to network "secret" information like roles & objectives
|
|
||||||
/// </remarks>
|
|
||||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
|
|
||||||
public sealed partial class MindComponent : Component
|
|
||||||
{
|
|
||||||
[DataField, AutoNetworkedField]
|
|
||||||
public List<EntityUid> Objectives = new();
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The session ID of the player owning this mind.
|
/// The session ID of the original owner, if any.
|
||||||
/// </summary>
|
/// May end up used for round-end information (as the owner may have abandoned Mind since)
|
||||||
[DataField, AutoNetworkedField, Access(typeof(SharedMindSystem))]
|
/// </summary>
|
||||||
public NetUserId? UserId { get; set; }
|
[DataField, AutoNetworkedField, Access(typeof(SharedMindSystem))]
|
||||||
|
public NetUserId? OriginalOwnerUserId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The session ID of the original owner, if any.
|
/// The first entity that this mind controlled. Used for round end information.
|
||||||
/// May end up used for round-end information (as the owner may have abandoned Mind since)
|
/// Might be relevant if the player has ghosted since.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField, AutoNetworkedField, Access(typeof(SharedMindSystem))]
|
[DataField, AutoNetworkedField]
|
||||||
public NetUserId? OriginalOwnerUserId { get; set; }
|
public NetEntity? OriginalOwnedEntity;
|
||||||
|
// This is a net entity, because this field currently ddoes not get set to null when this entity is deleted.
|
||||||
|
// This is a lazy way to ensure that people check that the entity still exists.
|
||||||
|
// TODO MIND Fix this properly by adding an OriginalMindContainerComponent or something like that.
|
||||||
|
|
||||||
/// <summary>
|
[ViewVariables]
|
||||||
/// The first entity that this mind controlled. Used for round end information.
|
public bool IsVisitingEntity => VisitingEntity != null;
|
||||||
/// Might be relevant if the player has ghosted since.
|
|
||||||
/// </summary>
|
|
||||||
[DataField, AutoNetworkedField]
|
|
||||||
public NetEntity? OriginalOwnedEntity;
|
|
||||||
// This is a net entity, because this field currently ddoes not get set to null when this entity is deleted.
|
|
||||||
// This is a lazy way to ensure that people check that the entity still exists.
|
|
||||||
// TODO MIND Fix this properly by adding an OriginalMindContainerComponent or something like that.
|
|
||||||
|
|
||||||
[ViewVariables]
|
[DataField, AutoNetworkedField, Access(typeof(SharedMindSystem))]
|
||||||
public bool IsVisitingEntity => VisitingEntity != null;
|
public EntityUid? VisitingEntity { get; set; }
|
||||||
|
|
||||||
[DataField, AutoNetworkedField, Access(typeof(SharedMindSystem))]
|
[ViewVariables]
|
||||||
public EntityUid? VisitingEntity { get; set; }
|
public EntityUid? CurrentEntity => VisitingEntity ?? OwnedEntity;
|
||||||
|
|
||||||
[ViewVariables]
|
[DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)]
|
||||||
public EntityUid? CurrentEntity => VisitingEntity ?? OwnedEntity;
|
public string? CharacterName { get; set; }
|
||||||
|
|
||||||
[DataField, AutoNetworkedField, ViewVariables(VVAccess.ReadWrite)]
|
/// <summary>
|
||||||
public string? CharacterName { get; set; }
|
/// The time of death for this Mind.
|
||||||
|
/// Can be null - will be null if the Mind is not considered "dead".
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public TimeSpan? TimeOfDeath { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The time of death for this Mind.
|
/// The entity currently owned by this mind.
|
||||||
/// Can be null - will be null if the Mind is not considered "dead".
|
/// Can be null.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField]
|
[DataField, AutoNetworkedField, Access(typeof(SharedMindSystem))]
|
||||||
public TimeSpan? TimeOfDeath { get; set; }
|
public EntityUid? OwnedEntity { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The entity currently owned by this mind.
|
/// An enumerable over all the objective entities this mind has.
|
||||||
/// Can be null.
|
/// </summary>
|
||||||
/// </summary>
|
[ViewVariables, Obsolete("Use Objectives field")]
|
||||||
[DataField, AutoNetworkedField, Access(typeof(SharedMindSystem))]
|
public IEnumerable<EntityUid> AllObjectives => Objectives;
|
||||||
public EntityUid? OwnedEntity { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An enumerable over all the objective entities this mind has.
|
/// Prevents user from ghosting out
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ViewVariables, Obsolete("Use Objectives field")]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
public IEnumerable<EntityUid> AllObjectives => Objectives;
|
[DataField("preventGhosting")]
|
||||||
|
public bool PreventGhosting { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Prevents user from ghosting out
|
/// Prevents user from suiciding
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
[ViewVariables(VVAccess.ReadWrite)]
|
||||||
[DataField("preventGhosting")]
|
[DataField("preventSuicide")]
|
||||||
public bool PreventGhosting { get; set; }
|
public bool PreventSuicide { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Prevents user from suiciding
|
/// The session of the player owning this mind.
|
||||||
/// </summary>
|
/// Can be null, in which case the player is currently not logged in.
|
||||||
[ViewVariables(VVAccess.ReadWrite)]
|
/// </summary>
|
||||||
[DataField("preventSuicide")]
|
[ViewVariables, Access(typeof(SharedMindSystem), typeof(SharedGameTicker))]
|
||||||
public bool PreventSuicide { get; set; }
|
// TODO remove this after moving IPlayerManager functions to shared
|
||||||
|
public ICommonSession? Session { get; set; }
|
||||||
/// <summary>
|
|
||||||
/// The session of the player owning this mind.
|
|
||||||
/// Can be null, in which case the player is currently not logged in.
|
|
||||||
/// </summary>
|
|
||||||
[ViewVariables, Access(typeof(SharedMindSystem), typeof(SharedGameTicker))]
|
|
||||||
// TODO remove this after moving IPlayerManager functions to shared
|
|
||||||
public ICommonSession? Session { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user